forked from profectus/Profectus
Implemented requirements system
This commit is contained in:
parent
f5a25b2c2d
commit
3a4b15bd8f
5 changed files with 241 additions and 131 deletions
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
165
src/game/requirements.tsx
Normal 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?.();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue