Implemented requirements system

This commit is contained in:
thepaperpilot 2022-12-31 14:57:09 -06:00
parent f5a25b2c2d
commit 3a4b15bd8f
5 changed files with 241 additions and 131 deletions

View file

@ -1,17 +1,17 @@
import { isArray } from "@vue/shared";
import ClickableComponent from "features/clickables/Clickable.vue"; import ClickableComponent from "features/clickables/Clickable.vue";
import type { import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
CoercableComponent,
GenericComponent,
OptionsFunc,
Replace,
StyleValue
} from "features/feature";
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
import type { Resource } from "features/resources/resource"; import { DefaultValue, Persistent, persistent } from "game/persistence";
import { DefaultValue, Persistent } from "game/persistence"; import {
import { persistent } from "game/persistence"; createVisibilityRequirement,
displayRequirements,
payRequirements,
Requirements,
requirementsMet
} from "game/requirements";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatWhole } from "util/bignum"; import Decimal, { formatWhole } from "util/bignum";
import type { import type {
Computable, Computable,
GetComputableType, GetComputableType,
@ -37,9 +37,7 @@ export type BuyableDisplay =
export interface BuyableOptions { export interface BuyableOptions {
visibility?: Computable<Visibility>; visibility?: Computable<Visibility>;
cost?: Computable<DecimalSource>; requirements: Requirements;
resource?: Resource;
canPurchase?: Computable<boolean>;
purchaseLimit?: Computable<DecimalSource>; purchaseLimit?: Computable<DecimalSource>;
initialValue?: DecimalSource; initialValue?: DecimalSource;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
@ -47,14 +45,13 @@ export interface BuyableOptions {
mark?: Computable<boolean | string>; mark?: Computable<boolean | string>;
small?: Computable<boolean>; small?: Computable<boolean>;
display?: Computable<BuyableDisplay>; display?: Computable<BuyableDisplay>;
onPurchase?: (cost: DecimalSource | undefined) => void; onPurchase?: VoidFunction;
} }
export interface BaseBuyable { export interface BaseBuyable {
id: string; id: string;
amount: Persistent<DecimalSource>; amount: Persistent<DecimalSource>;
maxed: Ref<boolean>; maxed: Ref<boolean>;
canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>; canClick: ProcessedComputable<boolean>;
onClick: VoidFunction; onClick: VoidFunction;
purchase: VoidFunction; purchase: VoidFunction;
@ -67,9 +64,7 @@ export type Buyable<T extends BuyableOptions> = Replace<
T & BaseBuyable, T & BaseBuyable,
{ {
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>; visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
cost: GetComputableType<T["cost"]>; requirements: GetComputableType<T["requirements"]>;
resource: GetComputableType<T["resource"]>;
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>; purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>;
classes: GetComputableType<T["classes"]>; classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>; style: GetComputableType<T["style"]>;
@ -83,7 +78,6 @@ export type GenericBuyable = Replace<
Buyable<BuyableOptions>, Buyable<BuyableOptions>,
{ {
visibility: ProcessedComputable<Visibility>; visibility: ProcessedComputable<Visibility>;
canPurchase: ProcessedComputable<boolean>;
purchaseLimit: ProcessedComputable<DecimalSource>; purchaseLimit: ProcessedComputable<DecimalSource>;
} }
>; >;
@ -95,40 +89,31 @@ export function createBuyable<T extends BuyableOptions>(
return createLazyProxy(() => { return createLazyProxy(() => {
const buyable = optionsFunc(); const buyable = optionsFunc();
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn(
"Cannot create buyable without a canPurchase property or a resource and cost property",
buyable
);
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
}
buyable.id = getUniqueID("buyable-"); buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType; buyable.type = BuyableType;
buyable[Component] = ClickableComponent; buyable[Component] = ClickableComponent;
buyable.amount = amount; buyable.amount = amount;
buyable.amount[DefaultValue] = buyable.initialValue ?? 0; buyable.amount[DefaultValue] = buyable.initialValue ?? 0;
buyable.canAfford = computed(() => {
const genericBuyable = buyable as GenericBuyable; const limitRequirement = {
const cost = unref(genericBuyable.cost); requirementMet: computed(() =>
return (
genericBuyable.resource != null &&
cost != null &&
Decimal.gte(genericBuyable.resource.value, cost)
);
});
if (buyable.canPurchase == null) {
buyable.canPurchase = computed(
() =>
unref((buyable as GenericBuyable).visibility) === Visibility.Visible &&
unref((buyable as GenericBuyable).canAfford) &&
Decimal.lt( Decimal.lt(
(buyable as GenericBuyable).amount.value, (buyable as GenericBuyable).amount.value,
unref((buyable as GenericBuyable).purchaseLimit) unref((buyable as GenericBuyable).purchaseLimit)
) )
); ),
requiresPay: false,
visibility: Visibility.None
} as const;
const visibilityRequirement = createVisibilityRequirement(buyable as GenericBuyable);
if (isArray(buyable.requirements)) {
buyable.requirements.unshift(visibilityRequirement);
buyable.requirements.push(limitRequirement);
} else {
buyable.requirements = [visibilityRequirement, buyable.requirements, limitRequirement];
} }
buyable.maxed = computed(() => buyable.maxed = computed(() =>
Decimal.gte( Decimal.gte(
(buyable as GenericBuyable).amount.value, (buyable as GenericBuyable).amount.value,
@ -144,26 +129,18 @@ export function createBuyable<T extends BuyableOptions>(
} }
return currClasses; return currClasses;
}); });
processComputable(buyable as T, "canPurchase"); buyable.canClick = computed(() => requirementsMet(buyable.requirements));
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
buyable.onClick = buyable.purchase = buyable.onClick = buyable.purchase =
buyable.onClick ?? buyable.onClick ??
buyable.purchase ?? buyable.purchase ??
function (this: GenericBuyable) { function (this: GenericBuyable) {
const genericBuyable = buyable as GenericBuyable; const genericBuyable = buyable as GenericBuyable;
if (!unref(genericBuyable.canPurchase)) { if (!unref(genericBuyable.canClick)) {
return; return;
} }
const cost = unref(genericBuyable.cost); payRequirements(buyable.requirements);
if (genericBuyable.cost != null && genericBuyable.resource != null) {
genericBuyable.resource.value = Decimal.sub(
genericBuyable.resource.value,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
cost!
);
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
} genericBuyable.onPurchase?.();
genericBuyable.onPurchase?.(cost);
}; };
processComputable(buyable as T, "display"); processComputable(buyable as T, "display");
const display = buyable.display; const display = buyable.display;
@ -174,7 +151,7 @@ export function createBuyable<T extends BuyableOptions>(
const CurrDisplay = coerceComponent(currDisplay); const CurrDisplay = coerceComponent(currDisplay);
return <CurrDisplay />; return <CurrDisplay />;
} }
if (currDisplay != null && buyable.cost != null && buyable.resource != null) { if (currDisplay != null) {
const genericBuyable = buyable as GenericBuyable; const genericBuyable = buyable as GenericBuyable;
const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Title = coerceComponent(currDisplay.title ?? "", "h3");
const Description = coerceComponent(currDisplay.description ?? ""); const Description = coerceComponent(currDisplay.description ?? "");
@ -207,13 +184,12 @@ export function createBuyable<T extends BuyableOptions>(
Currently: <EffectDisplay /> Currently: <EffectDisplay />
</div> </div>
)} )}
{genericBuyable.cost != null && !genericBuyable.maxed.value ? ( {genericBuyable.maxed.value ? null : (
<div> <div>
<br /> <br />
Cost: {format(unref(genericBuyable.cost))}{" "} {displayRequirements(genericBuyable.requirements)}
{buyable.resource.displayName}
</div> </div>
) : null} )}
</span> </span>
); );
} }
@ -222,8 +198,6 @@ export function createBuyable<T extends BuyableOptions>(
processComputable(buyable as T, "visibility"); processComputable(buyable as T, "visibility");
setDefault(buyable, "visibility", Visibility.Visible); setDefault(buyable, "visibility", Visibility.Visible);
processComputable(buyable as T, "cost");
processComputable(buyable as T, "resource");
processComputable(buyable as T, "purchaseLimit"); processComputable(buyable as T, "purchaseLimit");
setDefault(buyable, "purchaseLimit", Decimal.dInf); setDefault(buyable, "purchaseLimit", Decimal.dInf);
processComputable(buyable as T, "style"); processComputable(buyable as T, "style");

View file

@ -30,10 +30,8 @@ import MarkNode from "components/MarkNode.vue";
import Node from "components/Node.vue"; import Node from "components/Node.vue";
import type { StyleValue } from "features/feature"; import type { StyleValue } from "features/feature";
import { jsx, Visibility } from "features/feature"; import { jsx, Visibility } from "features/feature";
import type { Resource } from "features/resources/resource";
import { displayResource } from "features/resources/resource";
import type { GenericUpgrade } from "features/upgrades/upgrade"; import type { GenericUpgrade } from "features/upgrades/upgrade";
import type { DecimalSource } from "util/bignum"; import { displayRequirements, Requirements } from "game/requirements";
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
import type { Component, PropType, UnwrapRef } from "vue"; import type { Component, PropType, UnwrapRef } from "vue";
import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue"; import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
@ -50,8 +48,10 @@ export default defineComponent({
}, },
style: processedPropType<StyleValue>(String, Object, Array), style: processedPropType<StyleValue>(String, Object, Array),
classes: processedPropType<Record<string, boolean>>(Object), classes: processedPropType<Record<string, boolean>>(Object),
resource: Object as PropType<Resource>, requirements: {
cost: processedPropType<DecimalSource>(String, Object, Number), type: Object as PropType<Requirements>,
required: true
},
canPurchase: { canPurchase: {
type: processedPropType<boolean>(Boolean), type: processedPropType<boolean>(Boolean),
required: true required: true
@ -75,7 +75,7 @@ export default defineComponent({
MarkNode MarkNode
}, },
setup(props) { setup(props) {
const { display, cost } = toRefs(props); const { display, requirements, bought } = toRefs(props);
const component = shallowRef<Component | string>(""); const component = shallowRef<Component | string>("");
@ -89,7 +89,6 @@ export default defineComponent({
component.value = coerceComponent(currDisplay); component.value = coerceComponent(currDisplay);
return; return;
} }
const currCost = unwrapRef(cost);
const Title = coerceComponent(currDisplay.title || "", "h3"); const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description, "div"); const Description = coerceComponent(currDisplay.description, "div");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || ""); const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
@ -107,14 +106,7 @@ export default defineComponent({
Currently: <EffectDisplay /> Currently: <EffectDisplay />
</div> </div>
) : null} ) : null}
{props.resource != null ? ( {bought.value ? null : <><br />{displayRequirements(requirements.value)}</>}
<>
<br />
Cost: {props.resource &&
displayResource(props.resource, currCost)}{" "}
{props.resource?.displayName}
</>
) : null}
</span> </span>
)) ))
); );

View file

@ -1,4 +1,11 @@
import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; import { isArray } from "@vue/shared";
import type {
CoercableComponent,
GenericComponent,
OptionsFunc,
Replace,
StyleValue
} from "features/feature";
import { import {
Component, Component,
findFeatures, findFeatures,
@ -7,13 +14,16 @@ import {
setDefault, setDefault,
Visibility Visibility
} from "features/feature"; } from "features/feature";
import type { Resource } from "features/resources/resource";
import UpgradeComponent from "features/upgrades/Upgrade.vue"; import UpgradeComponent from "features/upgrades/Upgrade.vue";
import type { GenericLayer } from "game/layers"; import type { GenericLayer } from "game/layers";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence"; import { persistent } from "game/persistence";
import type { DecimalSource } from "util/bignum"; import {
import Decimal from "util/bignum"; createVisibilityRequirement,
payRequirements,
Requirements,
requirementsMet
} from "game/requirements";
import { isFunction } from "util/common"; import { isFunction } from "util/common";
import type { import type {
Computable, Computable,
@ -40,10 +50,8 @@ export interface UpgradeOptions {
effectDisplay?: CoercableComponent; effectDisplay?: CoercableComponent;
} }
>; >;
requirements: Requirements;
mark?: Computable<boolean | string>; mark?: Computable<boolean | string>;
cost?: Computable<DecimalSource>;
resource?: Resource;
canAfford?: Computable<boolean>;
onPurchase?: VoidFunction; onPurchase?: VoidFunction;
} }
@ -64,9 +72,8 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
classes: GetComputableType<T["classes"]>; classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>; style: GetComputableType<T["style"]>;
display: GetComputableType<T["display"]>; display: GetComputableType<T["display"]>;
requirements: GetComputableType<T["requirements"]>;
mark: GetComputableType<T["mark"]>; mark: GetComputableType<T["mark"]>;
cost: GetComputableType<T["cost"]>;
canAfford: GetComputableTypeWithDefault<T["canAfford"], Ref<boolean>>;
} }
>; >;
@ -74,7 +81,6 @@ export type GenericUpgrade = Replace<
Upgrade<UpgradeOptions>, Upgrade<UpgradeOptions>,
{ {
visibility: ProcessedComputable<Visibility>; visibility: ProcessedComputable<Visibility>;
canPurchase: ProcessedComputable<boolean>;
} }
>; >;
@ -88,55 +94,31 @@ export function createUpgrade<T extends UpgradeOptions>(
upgrade.type = UpgradeType; upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent; upgrade[Component] = UpgradeComponent;
if (upgrade.canAfford == null && (upgrade.resource == null || upgrade.cost == null)) {
console.warn(
"Error: can't create upgrade without a canAfford property or a resource and cost property",
upgrade
);
}
upgrade.bought = bought; upgrade.bought = bought;
if (upgrade.canAfford == null) { upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
upgrade.canAfford = computed(() => {
const genericUpgrade = upgrade as GenericUpgrade;
return (
genericUpgrade.resource != null &&
genericUpgrade.cost != null &&
Decimal.gte(genericUpgrade.resource.value, unref(genericUpgrade.cost))
);
});
} else {
processComputable(upgrade as T, "canAfford");
}
upgrade.canPurchase = computed(
() =>
unref((upgrade as GenericUpgrade).visibility) === Visibility.Visible &&
unref((upgrade as GenericUpgrade).canAfford) &&
!unref(upgrade.bought)
);
upgrade.purchase = function () { upgrade.purchase = function () {
const genericUpgrade = upgrade as GenericUpgrade; const genericUpgrade = upgrade as GenericUpgrade;
if (!unref(genericUpgrade.canPurchase)) { if (!unref(genericUpgrade.canPurchase)) {
return; return;
} }
if (genericUpgrade.resource != null && genericUpgrade.cost != null) { payRequirements(upgrade.requirements);
genericUpgrade.resource.value = Decimal.sub(
genericUpgrade.resource.value,
unref(genericUpgrade.cost)
);
}
bought.value = true; bought.value = true;
genericUpgrade.onPurchase?.(); genericUpgrade.onPurchase?.();
}; };
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
if (isArray(upgrade.requirements)) {
upgrade.requirements.unshift(visibilityRequirement);
} else {
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
}
processComputable(upgrade as T, "visibility"); processComputable(upgrade as T, "visibility");
setDefault(upgrade, "visibility", Visibility.Visible); setDefault(upgrade, "visibility", Visibility.Visible);
processComputable(upgrade as T, "classes"); processComputable(upgrade as T, "classes");
processComputable(upgrade as T, "style"); processComputable(upgrade as T, "style");
processComputable(upgrade as T, "display"); processComputable(upgrade as T, "display");
processComputable(upgrade as T, "mark"); processComputable(upgrade as T, "mark");
processComputable(upgrade as T, "cost");
processComputable(upgrade as T, "resource");
upgrade[GatherProps] = function (this: GenericUpgrade) { upgrade[GatherProps] = function (this: GenericUpgrade) {
const { const {
@ -144,8 +126,7 @@ export function createUpgrade<T extends UpgradeOptions>(
visibility, visibility,
style, style,
classes, classes,
resource, requirements,
cost,
canPurchase, canPurchase,
bought, bought,
mark, mark,
@ -157,8 +138,7 @@ export function createUpgrade<T extends UpgradeOptions>(
visibility, visibility,
style: unref(style), style: unref(style),
classes, classes,
resource, requirements,
cost,
canPurchase, canPurchase,
bought, bought,
mark, mark,

View file

@ -14,8 +14,7 @@ import { computed, unref } from "vue";
* An object that can be used to apply or unapply some modification to a number. * 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. * 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. * Descriptions can be optionally included for displaying them to the player.
* The built-in modifier creators are designed to display the modifiers using. * The built-in modifier creators are designed to display the modifiers using {@link createModifierSection}.
* {@link createModifierSection}.
*/ */
export interface Modifier { export interface Modifier {
/** Applies some operation on the input and returns the result. */ /** Applies some operation on the input and returns the result. */

165
src/game/requirements.tsx Normal file
View file

@ -0,0 +1,165 @@
import { isArray } from "@vue/shared";
import { CoercableComponent, jsx, JSXFunction, setDefault, Visibility } from "features/feature";
import { displayResource, Resource } from "features/resources/resource";
import Decimal, { DecimalSource } from "lib/break_eternity";
import {
Computable,
convertComputable,
processComputable,
ProcessedComputable
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { joinJSX, renderJSX } from "util/vue";
import { computed, unref } from "vue";
/**
* An object that can be used to describe a requirement to perform some purchase or other action.
* @see {@link createCostRequirement}
*/
export interface Requirement {
/** The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. */
partialDisplay?: JSXFunction;
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
display?: JSXFunction;
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
requirementMet: ProcessedComputable<boolean>;
requiresPay: ProcessedComputable<boolean>;
pay?: VoidFunction;
}
export type Requirements = Requirement | Requirement[];
export interface CostRequirementOptions {
resource: Resource;
cost: Computable<DecimalSource>;
visibility?: Computable<Visibility.Visible | Visibility.None>;
requiresPay?: ProcessedComputable<boolean>;
pay?: VoidFunction;
}
export function createCostRequirement<T extends CostRequirementOptions>(
optionsFunc: () => T
): Requirement {
return createLazyProxy(() => {
const req = optionsFunc() as T & Partial<Requirement>;
req.requirementMet = computed(() =>
Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable<DecimalSource>))
);
req.partialDisplay = jsx(() => (
<span
style={
unref(req.requirementMet as ProcessedComputable<boolean>)
? ""
: "color: var(--danger)"
}
>
{displayResource(
req.resource,
unref(req.cost as ProcessedComputable<DecimalSource>)
)}{" "}
{req.resource.displayName}
</span>
));
req.display = jsx(() => (
<div>
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
{displayResource(
req.resource,
unref(req.cost as ProcessedComputable<DecimalSource>)
)}{" "}
{req.resource.displayName}
</div>
));
processComputable(req as T, "visibility");
setDefault(req, "visibility", Visibility.Visible);
processComputable(req as T, "cost");
processComputable(req as T, "requiresPay");
setDefault(req, "requiresPay", true);
setDefault(req, "pay", function () {
req.resource.value = Decimal.sub(
req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>)
).max(0);
});
return req as Requirement;
});
}
export function createVisibilityRequirement(feature: {
visibility: ProcessedComputable<Visibility>;
}): Requirement {
return createLazyProxy(() => ({
requirementMet: computed(() => unref(feature.visibility) === Visibility.Visible),
visibility: Visibility.None,
requiresPay: false
}));
}
export function createBooleanRequirement(
requirement: Computable<boolean>,
display?: CoercableComponent
): Requirement {
return createLazyProxy(() => ({
requirementMet: convertComputable(requirement),
partialDisplay: display == null ? undefined : jsx(() => renderJSX(display)),
display: display == null ? undefined : jsx(() => <>Req: {renderJSX(display)}</>),
visibility: display == null ? Visibility.None : Visibility.Visible,
requiresPay: false
}));
}
export function requirementsMet(requirements: Requirements) {
if (isArray(requirements)) {
return requirements.every(r => unref(r.requirementMet));
}
return unref(requirements.requirementMet);
}
export function displayRequirements(requirements: Requirements) {
if (isArray(requirements)) {
requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible);
if (requirements.length === 1) {
requirements = requirements[0];
}
}
if (isArray(requirements)) {
requirements = requirements.filter(r => "partialDisplay" in r);
const withCosts = requirements.filter(r => unref(r.requiresPay));
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
return (
<>
{withCosts.length > 0 ? (
<div>
Costs:{" "}
{joinJSX(
withCosts.map(r => r.partialDisplay!()),
<>, </>
)}
</div>
) : null}
{withoutCosts.length > 0 ? (
<div>
Requires:{" "}
{joinJSX(
withoutCosts.map(r => r.partialDisplay!()),
<>, </>
)}
</div>
) : null}
</>
);
}
return requirements.display?.() ?? <></>;
}
export function payRequirements(requirements: Requirements) {
if (isArray(requirements)) {
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.());
} else if (unref(requirements.requiresPay)) {
requirements.pay?.();
}
}