Profectus-Demo/src/game/modifiers.tsx

282 lines
12 KiB
TypeScript

import "components/common/modifiers.css";
import type { CoercableComponent } from "features/feature";
import { jsx } from "features/feature";
import type { DecimalSource } from "util/bignum";
import Decimal, { format } from "util/bignum";
import type { WithRequired } from "util/common";
import type { Computable, ProcessedComputable } from "util/computed";
import { convertComputable } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { renderJSX } from "util/vue";
import { computed, unref } from "vue";
/**
* An object that can be used to apply or unapply some modification to a number.
* Being reversible requires the operation being invertible, but some features may rely on that.
* Descriptions can be optionally included for displaying them to the player.
* The built-in modifier creators are designed to display the modifiers using.
* {@link createModifierSection}.
*/
export interface Modifier {
/** Applies some operation on the input and returns the result. */
apply: (gain: DecimalSource) => DecimalSource;
/** Reverses the operation applied by the apply property. Required by some features. */
revert?: (gain: DecimalSource) => DecimalSource;
/**
* Whether or not this modifier should be considered enabled.
* Typically for use with modifiers passed into {@link createSequentialModifier}.
*/
enabled?: ProcessedComputable<boolean>;
/**
* A description of this modifier.
* @see {@link createModifierSection}.
*/
description?: ProcessedComputable<CoercableComponent>;
}
/**
* Utility type used to narrow down a modifier type that will have a description and/or enabled property based on optional parameters, T and S (respectively).
*/
export type ModifierFromOptionalParams<T, S> = T extends undefined
? S extends undefined
? Omit<WithRequired<Modifier, "revert">, "description" | "enabled">
: Omit<WithRequired<Modifier, "revert" | "enabled">, "description">
: S extends undefined
? Omit<WithRequired<Modifier, "revert" | "description">, "enabled">
: WithRequired<Modifier, "revert" | "enabled" | "description">;
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
export interface AdditiveModifierOptions<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined
> {
/** The amount to add to the input value. */
addend: Computable<DecimalSource>;
/** Description of what this modifier is doing. */
description?: T;
/** A computable that will be processed and passed directly into the returned modifier. */
enabled?: S;
}
/**
* Create a modifier that adds some value to the input value.
* @param optionsFunc Additive modifier options.
*/
export function createAdditiveModifier<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined,
R = ModifierFromOptionalParams<T, S>
>(optionsFunc: () => AdditiveModifierOptions<T, S>): R {
return createLazyProxy(() => {
const { addend, description, enabled } = optionsFunc();
const processedAddend = convertComputable(addend);
const processedDescription = convertComputable(description);
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
return {
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
revert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)),
enabled: processedEnabled,
description:
description == null
? undefined
: jsx(() => (
<div class="modifier-container">
<span class="modifier-amount">
{Decimal.gte(unref(processedAddend), 0) ? "+" : ""}
{format(unref(processedAddend))}
</span>
{unref(processedDescription) ? (
<span class="modifier-description">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{renderJSX(unref(processedDescription)!)}
</span>
) : null}
</div>
))
};
}) as unknown as R;
}
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
export interface MultiplicativeModifierOptions<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined
> {
/** The amount to multiply the input value by. */
multiplier: Computable<DecimalSource>;
/** Description of what this modifier is doing. */
description?: T;
/** A computable that will be processed and passed directly into the returned modifier. */
enabled?: S;
}
/**
* Create a modifier that multiplies the input value by some value.
* @param optionsFunc Multiplicative modifier options.
*/
export function createMultiplicativeModifier<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined,
R = ModifierFromOptionalParams<T, S>
>(optionsFunc: () => MultiplicativeModifierOptions<T, S>): R {
return createLazyProxy(() => {
const { multiplier, description, enabled } = optionsFunc();
const processedMultiplier = convertComputable(multiplier);
const processedDescription = convertComputable(description);
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
return {
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
revert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)),
enabled: processedEnabled,
description:
description == null
? undefined
: jsx(() => (
<div class="modifier-container">
<span class="modifier-amount">
x{format(unref(processedMultiplier))}
</span>
{unref(processedDescription) ? (
<span class="modifier-description">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{renderJSX(unref(processedDescription)!)}
</span>
) : null}
</div>
))
};
}) as unknown as R;
}
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */
export interface ExponentialModifierOptions<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined
> {
/** The amount to raise the input value to the power of. */
exponent: Computable<DecimalSource>;
/** Description of what this modifier is doing. */
description?: T;
/** A computable that will be processed and passed directly into the returned modifier. */
enabled?: S;
}
/**
* Create a modifier that raises the input value to the power of some value.
* @param optionsFunc Exponential modifier options.
*/
export function createExponentialModifier<
T extends Computable<CoercableComponent> | undefined,
S extends Computable<boolean> | undefined,
R = ModifierFromOptionalParams<T, S>
>(optionsFunc: () => ExponentialModifierOptions<T, S>): R {
return createLazyProxy(() => {
const { exponent, description, enabled } = optionsFunc();
const processedExponent = convertComputable(exponent);
const processedDescription = convertComputable(description);
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
return {
apply: (gain: DecimalSource) => Decimal.pow(gain, unref(processedExponent)),
revert: (gain: DecimalSource) => Decimal.root(gain, unref(processedExponent)),
enabled: processedEnabled,
description:
description == null
? undefined
: jsx(() => (
<div class="modifier-container">
<span class="modifier-amount">
^{format(unref(processedExponent))}
</span>
{unref(processedDescription) ? (
<span class="modifier-description">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{renderJSX(unref(processedDescription)!)}
</span>
) : null}
</div>
))
};
}) as unknown as R;
}
/**
* Takes an array of modifiers and applies and reverses them in order.
* Modifiers that are not enabled will not be applied nor reversed.
* Also joins their descriptions together.
* @param modifiers The modifiers to perform sequentially.
* @see {@link createModifierSection}.
*/
export function createSequentialModifier<
T extends Modifier[],
S = T extends WithRequired<Modifier, "revert">[]
? WithRequired<Modifier, "description" | "revert">
: Omit<WithRequired<Modifier, "description">, "revert">
>(...modifiers: T): S {
return {
apply: (gain: DecimalSource) =>
modifiers
.filter(m => unref(m.enabled) !== false)
.reduce((gain, modifier) => modifier.apply(gain), gain),
revert: modifiers.every(m => m.revert != null)
? (gain: DecimalSource) =>
modifiers
.filter(m => unref(m.enabled) !== false)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.reduceRight((gain, modifier) => modifier.revert!(gain), gain)
: undefined,
enabled: computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0),
description: jsx(() => (
<>
{(
modifiers
.filter(m => unref(m.enabled) !== false)
.map(m => unref(m.description))
.filter(d => d) as CoercableComponent[]
).map(renderJSX)}
</>
))
} as unknown as S;
}
/**
* Create a JSX element that displays a modifier.
* Intended to be used with the output from {@link createSequentialModifier}.
* @param title The header for the section.
* @param subtitle Smaller text that appears in the header after the title.
* @param modifier The modifier to render.
* @param base The base value that'll be passed into the modifier.
* @param unit The unit of the value being modified, if any.
* @param baseText The label to use for the base value.
*/
export function createModifierSection(
title: string,
subtitle: string,
modifier: WithRequired<Modifier, "description">,
base: DecimalSource = 1,
unit = "",
baseText: CoercableComponent = "Base"
) {
return (
<div>
<h3>
{title}
{subtitle ? <span class="subtitle"> ({subtitle})</span> : null}
</h3>
<br />
<div class="modifier-container">
<span class="modifier-amount">
{format(base)}
{unit}
</span>
<span class="modifier-description">{renderJSX(baseText)}</span>
</div>
{renderJSX(unref(modifier.description))}
<hr />
Total: {format(modifier.apply(base))}
{unit}
</div>
);
}