diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ced052..4fa0c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()` - **BREAKING** Visibility properties can now take booleans - Removed showIf util +- **BREAKING** Lazy proxies and options functions now pass the base object in as `this` as well as the first parameter. - Tweaked settings display - setupPassiveGeneration will no longer lower the resource - displayResource now floors resource amounts diff --git a/src/data/common.tsx b/src/data/common.tsx index 304208b..ab33aa6 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -5,11 +5,10 @@ import { createClickable } from "features/clickables/clickable"; import type { GenericConversion } from "features/conversion"; import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature"; import { jsx, setDefault } from "features/feature"; -import { displayResource, Resource } from "features/resources/resource"; +import { Resource, displayResource } from "features/resources/resource"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree"; -import Formula from "game/formulas/formulas"; -import type { FormulaSource, GenericFormula } from "game/formulas/types"; +import type { GenericFormula } from "game/formulas/types"; import type { Modifier } from "game/modifiers"; import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; @@ -99,8 +98,8 @@ export type GenericResetButton = Replace< export function createResetButton( optionsFunc: OptionsFunc ): ResetButton { - return createClickable(() => { - const resetButton = optionsFunc(); + return createClickable(feature => { + const resetButton = optionsFunc.call(feature, feature); processComputable(resetButton as T, "showNextAt"); setDefault(resetButton, "showNextAt", true); @@ -213,8 +212,8 @@ export type GenericLayerTreeNode = Replace< export function createLayerTreeNode( optionsFunc: OptionsFunc ): LayerTreeNode { - return createTreeNode(() => { - const options = optionsFunc(); + return createTreeNode(feature => { + const options = optionsFunc.call(feature, feature); processComputable(options as T, "display"); setDefault(options, "display", options.layerID); processComputable(options as T, "append"); diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 07292a0..3af6650 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -143,8 +143,10 @@ export function createAchievement( ): Achievement { const earned = persistent(false, false); const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const achievement = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const achievement = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); achievement.id = getUniqueID("achievement-"); achievement.type = AchievementType; achievement[Component] = AchievementComponent as GenericComponent; diff --git a/src/features/action.tsx b/src/features/action.tsx index 1f601bb..3b15756 100644 --- a/src/features/action.tsx +++ b/src/features/action.tsx @@ -108,8 +108,10 @@ export function createAction( ): Action { const progress = persistent(0); const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const action = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const action = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); action.id = getUniqueID("action-"); action.type = ActionType; action[Component] = ClickableComponent as GenericComponent; diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts index 025d782..32e45c6 100644 --- a/src/features/bars/bar.ts +++ b/src/features/bars/bar.ts @@ -106,8 +106,8 @@ export function createBar( ...decorators: Decorator[] ): Bar { const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const bar = optionsFunc(); + return createLazyProxy(feature => { + const bar = optionsFunc.call(feature, feature); bar.id = getUniqueID("bar-"); bar.type = BarType; bar[Component] = BarComponent as GenericComponent; diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index b8f66b4..e10bf67 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -297,8 +297,8 @@ export function createBoard( false ); - return createLazyProxy(() => { - const board = optionsFunc(); + return createLazyProxy(feature => { + const board = optionsFunc.call(feature, feature); board.id = getUniqueID("board-"); board.type = BoardType; board[Component] = BoardComponent as GenericComponent; diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index a30bf17..095b15a 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -155,8 +155,8 @@ export function createChallenge( const completions = persistent(0); const active = persistent(false, false); const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const challenge = optionsFunc(); + return createLazyProxy(feature => { + const challenge = optionsFunc.call(feature, feature); challenge.id = getUniqueID("challenge-"); challenge.type = ChallengeType; diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts index d96dd47..0da6fda 100644 --- a/src/features/clickables/clickable.ts +++ b/src/features/clickables/clickable.ts @@ -100,8 +100,10 @@ export function createClickable( ...decorators: Decorator[] ): Clickable { const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const clickable = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const clickable = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); clickable.id = getUniqueID("clickable-"); clickable.type = ClickableType; clickable[Component] = ClickableComponent as GenericComponent; diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 7732c42..41b7f32 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -127,8 +127,8 @@ export function createConversion( optionsFunc: OptionsFunc, ...decorators: Decorator[] ): Conversion { - return createLazyProxy(() => { - const conversion = optionsFunc(); + return createLazyProxy(feature => { + const conversion = optionsFunc.call(feature, feature); for (const decorator of decorators) { decorator.preConstruct?.(conversion); @@ -221,8 +221,8 @@ export function createCumulativeConversion( export function createIndependentConversion( optionsFunc: OptionsFunc ): Conversion { - return createConversion(() => { - const conversion: S = optionsFunc(); + return createConversion(feature => { + const conversion: S = optionsFunc.call(feature, feature); setDefault(conversion, "buyMax", false); diff --git a/src/features/feature.ts b/src/features/feature.ts index 082376c..2bd1fad 100644 --- a/src/features/feature.ts +++ b/src/features/feature.ts @@ -42,9 +42,9 @@ export type Replace = S & Omit; * with "this" bound to what the type will eventually be processed into. * Intended for making lazily evaluated objects. */ -export type OptionsFunc, S = R> = () => OptionsObject; +export type OptionsFunc = (obj: R) => OptionsObject; -export type OptionsObject, S = R> = T & Partial & ThisType; +export type OptionsObject = T & Partial & ThisType; let id = 0; /** diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 57eb399..454bbfc 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -307,8 +307,8 @@ export function createGrid( optionsFunc: OptionsFunc ): Grid { const cellState = persistent>({}, false); - return createLazyProxy(() => { - const grid = optionsFunc(); + return createLazyProxy(feature => { + const grid = optionsFunc.call(feature, feature); grid.id = getUniqueID("grid-"); grid[Component] = GridComponent as GenericComponent; diff --git a/src/features/hotkey.tsx b/src/features/hotkey.tsx index dcfdade..51fafbb 100644 --- a/src/features/hotkey.tsx +++ b/src/features/hotkey.tsx @@ -68,8 +68,8 @@ const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("]; export function createHotkey( optionsFunc: OptionsFunc ): Hotkey { - return createLazyProxy(() => { - const hotkey = optionsFunc(); + return createLazyProxy(feature => { + const hotkey = optionsFunc.call(feature, feature); hotkey.type = HotkeyType; processComputable(hotkey as T, "enabled"); diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts index f5c7a2f..551d86c 100644 --- a/src/features/infoboxes/infobox.ts +++ b/src/features/infoboxes/infobox.ts @@ -91,8 +91,8 @@ export function createInfobox( optionsFunc: OptionsFunc ): Infobox { const collapsed = persistent(false, false); - return createLazyProxy(() => { - const infobox = optionsFunc(); + return createLazyProxy(feature => { + const infobox = optionsFunc.call(feature, feature); infobox.id = getUniqueID("infobox-"); infobox.type = InfoboxType; infobox[Component] = InfoboxComponent as GenericComponent; diff --git a/src/features/links/links.ts b/src/features/links/links.ts index 5b28c46..c5603c5 100644 --- a/src/features/links/links.ts +++ b/src/features/links/links.ts @@ -59,8 +59,8 @@ export type GenericLinks = Replace< export function createLinks( optionsFunc: OptionsFunc ): Links { - return createLazyProxy(() => { - const links = optionsFunc(); + return createLazyProxy(feature => { + const links = optionsFunc.call(feature, feature); links.type = LinksType; links[Component] = LinksComponent as GenericComponent; diff --git a/src/features/particles/particles.tsx b/src/features/particles/particles.tsx index 8edaab1..d2797d5 100644 --- a/src/features/particles/particles.tsx +++ b/src/features/particles/particles.tsx @@ -69,8 +69,10 @@ export type GenericParticles = Particles; export function createParticles( optionsFunc?: OptionsFunc ): Particles { - return createLazyProxy(() => { - const particles = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const particles = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); particles.id = getUniqueID("particles-"); particles.type = ParticlesType; particles[Component] = ParticlesComponent as GenericComponent; diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index edad6bb..1a4774c 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -135,8 +135,8 @@ export function createRepeatable( ): Repeatable { const amount = persistent(0); const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const repeatable = optionsFunc(); + return createLazyProxy(feature => { + const repeatable = optionsFunc.call(feature, feature); repeatable.id = getUniqueID("repeatable-"); repeatable.type = RepeatableType; diff --git a/src/features/reset.ts b/src/features/reset.ts index 0bba943..b7c40ed 100644 --- a/src/features/reset.ts +++ b/src/features/reset.ts @@ -54,8 +54,8 @@ export type GenericReset = Reset; export function createReset( optionsFunc: OptionsFunc ): Reset { - return createLazyProxy(() => { - const reset = optionsFunc(); + return createLazyProxy(feature => { + const reset = optionsFunc.call(feature, feature); reset.id = getUniqueID("reset-"); reset.type = ResetType; diff --git a/src/features/tabs/tab.ts b/src/features/tabs/tab.ts index d19dfdd..111f954 100644 --- a/src/features/tabs/tab.ts +++ b/src/features/tabs/tab.ts @@ -62,8 +62,8 @@ export type GenericTab = Tab; export function createTab( optionsFunc: OptionsFunc ): Tab { - return createLazyProxy(() => { - const tab = optionsFunc(); + return createLazyProxy(feature => { + const tab = optionsFunc.call(feature, feature); tab.id = getUniqueID("tab-"); tab.type = TabType; tab[Component] = TabComponent as GenericComponent; diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index 316b309..2ca544b 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -156,8 +156,10 @@ export function createTabFamily( } const selected = persistent(Object.keys(tabs)[0], false); - return createLazyProxy(() => { - const tabFamily = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const tabFamily = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); tabFamily.id = getUniqueID("tabFamily-"); tabFamily.type = TabFamilyType; diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index de3616c..22aa627 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -106,8 +106,10 @@ export function createTreeNode( ...decorators: Decorator[] ): TreeNode { const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const treeNode = optionsFunc?.() ?? ({} as ReturnType>); + return createLazyProxy(feature => { + const treeNode = + optionsFunc?.call(feature, feature) ?? + ({} as ReturnType>); treeNode.id = getUniqueID("treeNode-"); treeNode.type = TreeNodeType; treeNode[Component] = TreeNodeComponent as GenericComponent; @@ -257,8 +259,8 @@ export type GenericTree = Replace< export function createTree( optionsFunc: OptionsFunc ): Tree { - return createLazyProxy(() => { - const tree = optionsFunc(); + return createLazyProxy(feature => { + const tree = optionsFunc.call(feature, feature); tree.id = getUniqueID("tree-"); tree.type = TreeType; tree[Component] = TreeComponent as GenericComponent; diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 6137317..08fd2e6 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -123,8 +123,8 @@ export function createUpgrade( ): Upgrade { const bought = persistent(false, false); const decoratedData = decorators.reduce((current, next) => Object.assign(current, next.getPersistentData?.()), {}); - return createLazyProxy(() => { - const upgrade = optionsFunc(); + return createLazyProxy(feature => { + const upgrade = optionsFunc.call(feature, feature); upgrade.id = getUniqueID("upgrade-"); upgrade.type = UpgradeType; upgrade[Component] = UpgradeComponent as GenericComponent; diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index d0a0f81..9f6ad5a 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,7 +1,8 @@ import { Resource } from "features/resources/resource"; +import { NonPersistent } from "game/persistence"; import Decimal, { DecimalSource, format } from "util/bignum"; -import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, ref, unref } from "vue"; +import { Computable, ProcessedComputable, convertComputable } from "util/computed"; +import { ComputedRef, Ref, computed, ref, unref } from "vue"; import * as ops from "./operations"; import type { EvaluateFunction, @@ -58,7 +59,15 @@ export default class Formula { constructor(options: FormulaOptions) { let readonlyProperties; + if ("inputs" in options) { + options.inputs = options.inputs.map(input => + typeof input === "object" && NonPersistent in input ? input[NonPersistent] : input + ) as T | [FormulaSource]; + } if ("variable" in options) { + if (typeof options.variable === "object" && NonPersistent in options.variable) { + options.variable = options.variable[NonPersistent] as Ref; + } readonlyProperties = this.setupVariable(options); } else if (!("evaluate" in options)) { readonlyProperties = this.setupConstant(options); @@ -364,21 +373,30 @@ export default class Formula { * @param value The incoming formula value * @param condition Whether or not to apply the modifier * @param formulaModifier The modifier to apply to the incoming formula if the condition is true + * @param elseFormulaModifier An optional modifier to apply to the incoming formula if the condition is false */ public static if( value: FormulaSource, condition: Computable, formulaModifier: ( value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula, + elseFormulaModifier?: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula ) => GenericFormula ): GenericFormula { const lhsRef = ref(0); - const formula = formulaModifier(Formula.variable(lhsRef)); + const variable = Formula.variable(lhsRef); + const formula = formulaModifier(variable); + const elseFormula = elseFormulaModifier?.(variable); const processedCondition = convertComputable(condition); function evalStep(lhs: DecimalSource) { if (unref(processedCondition)) { lhsRef.value = lhs; return formula.evaluate(); + } else if (elseFormula) { + lhsRef.value = lhs; + return elseFormula.evaluate(); } else { return lhs; } @@ -389,6 +407,8 @@ export default class Formula { } if (unref(processedCondition)) { return lhs.invert(formula.invert(value)); + } else if (elseFormula) { + return lhs.invert(elseFormula.invert(value)); } else { return lhs.invert(value); } @@ -399,15 +419,17 @@ export default class Formula { invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined }); } - /** @see {@link if} */ public static conditional( value: FormulaSource, condition: Computable, formulaModifier: ( value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula, + elseFormulaModifier?: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula ) => GenericFormula ) { - return Formula.if(value, condition, formulaModifier); + return Formula.if(value, condition, formulaModifier, elseFormulaModifier); } public static abs(value: FormulaSource): GenericFormula { diff --git a/src/game/layers.tsx b/src/game/layers.tsx index 532daba..6294592 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -220,7 +220,7 @@ export function createLayer( addingLayers.push(id); persistentRefs[id] = new Set(); layer.minimized = persistent(false, false); - Object.assign(layer, optionsFunc.call(layer as BaseLayer)); + Object.assign(layer, optionsFunc.call(layer, layer as BaseLayer)); if ( addingLayers[addingLayers.length - 1] == null || addingLayers[addingLayers.length - 1] !== id diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index 74692b0..cceacd8 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -1,5 +1,5 @@ import "components/common/modifiers.css"; -import type { CoercableComponent } from "features/feature"; +import type { CoercableComponent, OptionsFunc } from "features/feature"; import { jsx } from "features/feature"; import settings from "game/settings"; import type { DecimalSource } from "util/bignum"; @@ -66,10 +66,13 @@ export interface AdditiveModifierOptions { * @param optionsFunc Additive modifier options. */ export function createAdditiveModifier( - optionsFunc: () => T + optionsFunc: OptionsFunc ): ModifierFromOptionalParams { - return createLazyProxy(() => { - const { addend, description, enabled, smallerIsBetter } = optionsFunc(); + return createLazyProxy(feature => { + const { addend, description, enabled, smallerIsBetter } = optionsFunc.call( + feature, + feature + ); const processedAddend = convertComputable(addend); const processedDescription = convertComputable(description); @@ -128,10 +131,13 @@ export interface MultiplicativeModifierOptions { * @param optionsFunc Multiplicative modifier options. */ export function createMultiplicativeModifier( - optionsFunc: () => T + optionsFunc: OptionsFunc ): ModifierFromOptionalParams { - return createLazyProxy(() => { - const { multiplier, description, enabled, smallerIsBetter } = optionsFunc(); + return createLazyProxy(feature => { + const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call( + feature, + feature + ); const processedMultiplier = convertComputable(multiplier); const processedDescription = convertComputable(description); @@ -191,11 +197,11 @@ export interface ExponentialModifierOptions { * @param optionsFunc Exponential modifier options. */ export function createExponentialModifier( - optionsFunc: () => T + optionsFunc: OptionsFunc ): ModifierFromOptionalParams { - return createLazyProxy(() => { + return createLazyProxy(feature => { const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } = - optionsFunc(); + optionsFunc.call(feature, feature); const processedExponent = convertComputable(exponent); const processedDescription = convertComputable(description); diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index acb7f7b..35504b6 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -3,6 +3,7 @@ import { CoercableComponent, isVisible, jsx, + OptionsFunc, Replace, setDefault, Visibility @@ -108,10 +109,10 @@ export type CostRequirement = Replace< * @param optionsFunc Cost requirement options. */ export function createCostRequirement( - optionsFunc: () => T + optionsFunc: OptionsFunc ): CostRequirement { - return createLazyProxy(() => { - const req = optionsFunc() as T & Partial; + return createLazyProxy(feature => { + const req = optionsFunc.call(feature, feature) as T & Partial; req.partialDisplay = amount => ( = NonNullable extends Record // Takes a function that returns an object and pretends to be that object // Note that the object is lazily calculated export function createLazyProxy( - objectFunc: (baseObject: S) => T & S, + objectFunc: (this: S, baseObject: S) => T & S, baseObject: S = {} as S ): T { const obj: S & Partial = baseObject; let calculated = false; function calculateObj(): T { if (!calculated) { - Object.assign(obj, objectFunc(obj)); + Object.assign(obj, objectFunc.call(obj, obj)); calculated = true; } return obj as S & T; @@ -73,7 +73,7 @@ export function createLazyProxy( }, getOwnPropertyDescriptor(target, key) { if (!calculated) { - Object.assign(obj, objectFunc(obj)); + Object.assign(obj, objectFunc.call(obj, obj)); calculated = true; } return Object.getOwnPropertyDescriptor(target, key); diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index ed5adb2..7da6226 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -868,6 +868,26 @@ describe("Conditionals", () => { Formula.if(variable, false, value => Formula.sqrt(value)).invert(10) ).compare_tolerance(10)); }); + describe("Evaluates correctly with condition false and else statement", () => { + test("Evaluates correctly", () => + expect( + Formula.if( + constant, + false, + value => Formula.sqrt(value), + value => value.times(2) + ).evaluate() + ).compare_tolerance(20)); + test("Inverts correctly with variable in input", () => + expect( + Formula.if( + variable, + false, + value => Formula.sqrt(value), + value => value.times(2) + ).invert(20) + ).compare_tolerance(10)); + }); describe("Evaluates correctly with condition true", () => { test("Evaluates correctly", () =>