From 166c2d34b4c993a1ecd650b2d09b89a150ece8bf Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 25 Dec 2022 18:53:27 -0600 Subject: [PATCH 01/56] Make setupPassiveGeneration not lower the resource --- src/features/conversion.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 476bfb9..97637fa 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -460,7 +460,9 @@ export function setupPassiveGeneration( conversion.gainResource.value = Decimal.add( conversion.gainResource.value, Decimal.times(currRate, diff).times(Decimal.ceil(unref(conversion.actualGain))) - ).min(unref(processedCap) ?? Decimal.dInf); + ) + .min(unref(processedCap) ?? Decimal.dInf) + .max(conversion.gainResource.value); } }); } From 2ece8e5a178168a579f5e7b185c9d60f25ac7155 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 26 Dec 2022 16:51:32 -0600 Subject: [PATCH 02/56] Floor reindeer feed amount --- src/features/clickables/Clickable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/clickables/Clickable.vue b/src/features/clickables/Clickable.vue index 0e04a9f..a2ec971 100644 --- a/src/features/clickables/Clickable.vue +++ b/src/features/clickables/Clickable.vue @@ -92,7 +92,7 @@ export default defineComponent({ comp.value = coerceComponent(currDisplay); return; } - const Title = coerceComponent(currDisplay.title || "", "h3"); + const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Description = coerceComponent(currDisplay.description, "div"); comp.value = coerceComponent( jsx(() => ( From 345f8c1cd8f5a2390b4a145b78487ba3852110b7 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 26 Dec 2022 22:50:11 -0600 Subject: [PATCH 03/56] Implement Action feature --- src/features/action.tsx | 247 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/features/action.tsx diff --git a/src/features/action.tsx b/src/features/action.tsx new file mode 100644 index 0000000..18643b8 --- /dev/null +++ b/src/features/action.tsx @@ -0,0 +1,247 @@ +import { isArray } from "@vue/shared"; +import ClickableComponent from "features/clickables/Clickable.vue"; +import { + Component, + findFeatures, + GatherProps, + GenericComponent, + getUniqueID, + jsx, + JSXFunction, + OptionsFunc, + Replace, + setDefault, + StyleValue, + Visibility +} from "features/feature"; +import { globalBus } from "game/events"; +import { persistent } from "game/persistence"; +import Decimal, { DecimalSource } from "lib/break_eternity"; +import { Unsubscribe } from "nanoevents"; +import { Direction } from "util/common"; +import type { + Computable, + GetComputableType, + GetComputableTypeWithDefault, + ProcessedComputable +} from "util/computed"; +import { processComputable } from "util/computed"; +import { createLazyProxy } from "util/proxies"; +import { coerceComponent, isCoercableComponent, render } from "util/vue"; +import { computed, Ref, ref, unref } from "vue"; +import { BarOptions, createBar, GenericBar } from "./bars/bar"; +import { ClickableOptions } from "./clickables/clickable"; + +export const ActionType = Symbol("Action"); + +export interface ActionOptions extends Omit { + duration: Computable; + autoStart?: Computable; + onClick: (amount: DecimalSource) => void; + barOptions?: Partial; +} + +export interface BaseAction { + id: string; + type: typeof ActionType; + isHolding: Ref; + progress: Ref; + progressBar: GenericBar; + update: (diff: number) => void; + [Component]: GenericComponent; + [GatherProps]: () => Record; +} + +export type Action = Replace< + T & BaseAction, + { + duration: GetComputableType; + autoStart: GetComputableTypeWithDefault; + visibility: GetComputableTypeWithDefault; + canClick: GetComputableTypeWithDefault; + classes: GetComputableType; + style: GetComputableType; + mark: GetComputableType; + display: JSXFunction; + onClick: VoidFunction; + } +>; + +export type GenericAction = Replace< + Action, + { + autoStart: ProcessedComputable; + visibility: ProcessedComputable; + canClick: ProcessedComputable; + } +>; + +export function createAction( + optionsFunc?: OptionsFunc +): Action { + const progress = persistent(0); + return createLazyProxy(() => { + const action = optionsFunc?.() ?? ({} as ReturnType>); + action.id = getUniqueID("action-"); + action.type = ActionType; + action[Component] = ClickableComponent as GenericComponent; + + // Required because of display changing types + const genericAction = action as unknown as GenericAction; + + action.isHolding = ref(false); + action.progress = progress; + + processComputable(action as T, "visibility"); + setDefault(action, "visibility", Visibility.Visible); + processComputable(action as T, "duration"); + processComputable(action as T, "autoStart"); + setDefault(action, "autoStart", false); + processComputable(action as T, "canClick"); + setDefault(action, "canClick", true); + processComputable(action as T, "classes"); + processComputable(action as T, "style"); + processComputable(action as T, "mark"); + processComputable(action as T, "display"); + + const style = action.style as ProcessedComputable; + action.style = computed(() => { + const currStyle: StyleValue[] = [ + { + cursor: Decimal.gte( + progress.value, + unref(action.duration as ProcessedComputable) + ) + ? "pointer" + : "progress", + display: "flex", + flexDirection: "column" + } + ]; + const originalStyle = unref(style); + if (isArray(originalStyle)) { + currStyle.push(...originalStyle); + } else if (originalStyle != null) { + currStyle.push(originalStyle); + } + return currStyle as StyleValue; + }); + + action.progressBar = createBar(() => ({ + direction: Direction.Right, + width: 100, + height: 10, + style: "margin-top: 8px", + borderStyle: "border-color: black", + baseStyle: "margin-top: -1px", + progress: () => Decimal.div(progress.value, unref(genericAction.duration)), + ...action.barOptions + })); + + const canClick = action.canClick as ProcessedComputable; + action.canClick = computed( + () => + unref(canClick) && + Decimal.gte( + progress.value, + unref(action.duration as ProcessedComputable) + ) + ); + + const display = action.display as GetComputableType; + action.display = jsx(() => { + const currDisplay = unref(display); + let Comp: GenericComponent | undefined; + if (isCoercableComponent(currDisplay)) { + Comp = coerceComponent(currDisplay); + } else if (currDisplay != null) { + const Title = coerceComponent(currDisplay.title ?? "", "h3"); + const Description = coerceComponent(currDisplay.description, "div"); + Comp = coerceComponent( + jsx(() => ( + + {currDisplay.title != null ? ( +
+ + </div> + ) : null} + <Description /> + </span> + )) + ); + } + return ( + <> + <div style="flex-grow: 1" /> + {Comp == null ? null : <Comp />} + <div style="flex-grow: 1" /> + {render(genericAction.progressBar)} + </> + ); + }); + + const onClick = action.onClick.bind(action); + action.onClick = function () { + if (unref(action.canClick) === false) { + return; + } + const amount = Decimal.div(progress.value, unref(genericAction.duration)); + onClick?.(amount); + progress.value = 0; + }; + + action.update = function (diff) { + const duration = unref(genericAction.duration); + if (Decimal.gte(progress.value, duration)) { + progress.value = duration; + } else { + progress.value = Decimal.add(progress.value, diff); + if (genericAction.isHolding.value || unref(genericAction.autoStart)) { + genericAction.onClick(); + } + } + }; + + action[GatherProps] = function (this: GenericAction) { + const { + display, + visibility, + style, + classes, + onClick, + isHolding, + canClick, + small, + mark, + id + } = this; + return { + display, + visibility, + style: unref(style), + classes, + onClick, + isHolding, + canClick, + small, + mark, + id + }; + }; + + return action as unknown as Action<T>; + }); +} + +const listeners: Record<string, Unsubscribe | undefined> = {}; +globalBus.on("addLayer", layer => { + const actions: GenericAction[] = findFeatures(layer, ActionType) as GenericAction[]; + listeners[layer.id] = layer.on("postUpdate", diff => { + actions.forEach(action => action.update(diff)); + }); +}); +globalBus.on("removeLayer", layer => { + // unsubscribe from postUpdate + listeners[layer.id]?.(); + listeners[layer.id] = undefined; +}); From c1a66e6666503c5650d8cf8cd680ce7a6cb500df Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Mon, 26 Dec 2022 23:14:12 -0600 Subject: [PATCH 04/56] Tweak isPersistent typing --- src/game/persistence.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/game/persistence.ts b/src/game/persistence.ts index 4f4714f..01e4c96 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -132,8 +132,7 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten * Type guard for whether an arbitrary value is a persistent ref * @param value The value that may or may not be a persistent ref */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isPersistent(value: any): value is Persistent { +export function isPersistent(value: unknown): value is Persistent { return value != null && typeof value === "object" && PersistentState in value; } From 8c8f7f79045679540e1637706b80a735177fe8da Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Mon, 26 Dec 2022 23:17:07 -0600 Subject: [PATCH 05/56] Fix typos --- src/util/bignum.ts | 2 +- src/util/save.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/bignum.ts b/src/util/bignum.ts index 33d180e..d8e3252 100644 --- a/src/util/bignum.ts +++ b/src/util/bignum.ts @@ -18,7 +18,7 @@ export const { export type DecimalSource = RawDecimalSource; declare global { - /** Augment the window object so the big num functions can be access from the console. */ + /** Augment the window object so the big num functions can be accessed from the console. */ interface Window { Decimal: typeof Decimal; exponentialFormat: (num: DecimalSource, precision: number, mantissa: boolean) => string; diff --git a/src/util/save.ts b/src/util/save.ts index 89d7e0b..e43d34b 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -143,7 +143,7 @@ window.onbeforeunload = () => { declare global { /** - * Augment the window object so the save function, and the hard reset function can be access from the console. + * Augment the window object so the save function, and the hard reset function can be accessed from the console. */ interface Window { save: VoidFunction; From f5a25b2c2d74c594c347ddf0f89a225f378b7a0e Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 28 Dec 2022 09:03:51 -0600 Subject: [PATCH 06/56] Fix NaN detection Also removes the proxy around player and cleaned up types --- src/components/NaNScreen.vue | 53 ++++++++++--------- src/components/SavesManager.vue | 13 +++-- src/data/projEntry.tsx | 6 +-- src/game/persistence.ts | 67 +++++++++++++++++------ src/game/player.ts | 94 +++------------------------------ src/game/state.ts | 6 ++- src/util/computed.ts | 16 +++--- src/util/proxies.ts | 16 +++++- src/util/save.ts | 17 +++--- 9 files changed, 130 insertions(+), 158 deletions(-) diff --git a/src/components/NaNScreen.vue b/src/components/NaNScreen.vue index 76a7b75..511d1b6 100644 --- a/src/components/NaNScreen.vue +++ b/src/components/NaNScreen.vue @@ -14,9 +14,12 @@ </div> <br /> <div> - <a :href="discordLink" class="nan-modal-discord-link"> + <a + :href="discordLink || 'https://discord.gg/WzejVAx'" + class="nan-modal-discord-link" + > <span class="material-icons nan-modal-discord">discord</span> - {{ discordName }} + {{ discordName || "The Paper Pilot Community" }} </a> </div> <br /> @@ -50,49 +53,51 @@ import state from "game/state"; import type { DecimalSource } from "util/bignum"; import Decimal, { format } from "util/bignum"; import type { ComponentPublicInstance } from "vue"; -import { computed, ref, toRef } from "vue"; +import { computed, ref, toRef, watch } from "vue"; import Toggle from "./fields/Toggle.vue"; import SavesManager from "./SavesManager.vue"; const { discordName, discordLink } = projInfo; -const autosave = toRef(player, "autosave"); +const autosave = ref(true); +const isPaused = ref(true); const hasNaN = toRef(state, "hasNaN"); const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null); -const path = computed(() => state.NaNPath?.join(".")); -const property = computed(() => state.NaNPath?.slice(-1)[0]); -const previous = computed<DecimalSource | null>(() => { - if (state.NaNReceiver && property.value != null) { - return state.NaNReceiver[property.value] as DecimalSource; - } - return null; -}); -const isPaused = computed({ - get() { - return player.devSpeed === 0; - }, - set(value: boolean) { - player.devSpeed = value ? null : 0; +watch(hasNaN, hasNaN => { + if (hasNaN) { + autosave.value = player.autosave; + isPaused.value = player.devSpeed === 0; + } else { + player.autosave = autosave.value; + player.devSpeed = isPaused.value ? 0 : null; } }); +const path = computed(() => state.NaNPath?.join(".")); +const previous = computed<DecimalSource | null>(() => { + if (state.NaNPersistent != null) { + return state.NaNPersistent.value; + } + return null; +}); + function setZero() { - if (state.NaNReceiver && property.value != null) { - state.NaNReceiver[property.value] = new Decimal(0); + if (state.NaNPersistent != null) { + state.NaNPersistent.value = new Decimal(0); state.hasNaN = false; } } function setOne() { - if (state.NaNReceiver && property.value != null) { - state.NaNReceiver[property.value] = new Decimal(1); + if (state.NaNPersistent) { + state.NaNPersistent.value = new Decimal(1); state.hasNaN = false; } } function ignore() { - if (state.NaNReceiver && property.value != null) { - state.NaNReceiver[property.value] = new Decimal(NaN); + if (state.NaNPersistent) { + state.NaNPersistent.value = new Decimal(NaN); state.hasNaN = false; } } diff --git a/src/components/SavesManager.vue b/src/components/SavesManager.vue index 37b4d5b..91edd40 100644 --- a/src/components/SavesManager.vue +++ b/src/components/SavesManager.vue @@ -59,11 +59,10 @@ <script setup lang="ts"> import Modal from "components/Modal.vue"; import projInfo from "data/projInfo.json"; -import type { PlayerData } from "game/player"; +import type { Player } from "game/player"; import player, { stringifySave } from "game/player"; import settings from "game/settings"; import LZString from "lz-string"; -import { ProxyState } from "util/proxies"; import { getUniqueID, loadSave, newSave, save } from "util/save"; import type { ComponentPublicInstance } from "vue"; import { computed, nextTick, ref, shallowReactive, watch } from "vue"; @@ -72,7 +71,7 @@ import Select from "./fields/Select.vue"; import Text from "./fields/Text.vue"; import Save from "./Save.vue"; -export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }; +export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown }; const isOpen = ref(false); const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null); @@ -195,7 +194,7 @@ const saves = computed(() => function exportSave(id: string) { let saveToExport; if (player.id === id) { - saveToExport = stringifySave(player[ProxyState]); + saveToExport = stringifySave(player); } else { saveToExport = JSON.stringify(saves.value[id]); } @@ -228,7 +227,7 @@ function duplicateSave(id: string) { } const playerData = { ...saves.value[id], id: getUniqueID() }; - save(playerData as PlayerData); + save(playerData as Player); settings.saves.push(playerData.id); } @@ -272,7 +271,7 @@ function newFromPreset(preset: string) { } const playerData = JSON.parse(preset); playerData.id = getUniqueID(); - save(playerData as PlayerData); + save(playerData as Player); settings.saves.push(playerData.id); @@ -287,7 +286,7 @@ function editSave(id: string, newName: string) { player.name = newName; save(); } else { - save(currSave as PlayerData); + save(currSave as Player); cachedSaves[id] = undefined; } } diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index f930d3a..e4640b6 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -6,7 +6,7 @@ import { branchedResetPropagation, createTree } from "features/trees/tree"; import { globalBus } from "game/events"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; -import type { PlayerData } from "game/player"; +import type { Player } from "game/player"; import player from "game/player"; import type { DecimalSource } from "util/bignum"; import Decimal, { format, formatTime } from "util/bignum"; @@ -79,7 +79,7 @@ export const main = createLayer("main", function (this: BaseLayer) { */ export const getInitialLayers = ( /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - player: Partial<PlayerData> + player: Partial<Player> ): Array<GenericLayer> => [main, prestige]; /** @@ -97,7 +97,7 @@ export const hasWon = computed(() => { /* eslint-disable @typescript-eslint/no-unused-vars */ export function fixOldSave( oldVersion: string | undefined, - player: Partial<PlayerData> + player: Partial<Player> // eslint-disable-next-line @typescript-eslint/no-empty-function ): void {} /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/src/game/persistence.ts b/src/game/persistence.ts index 01e4c96..9ad0634 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -7,6 +7,8 @@ import Decimal from "util/bignum"; import { ProxyState } from "util/proxies"; import type { Ref, WritableComputedRef } from "vue"; import { computed, isReactive, isRef, ref } from "vue"; +import player from "./player"; +import state from "./state"; /** * A symbol used in {@link Persistent} objects. @@ -56,6 +58,7 @@ export type State = * A {@link Ref} that has been augmented with properties to allow it to be saved and loaded within the player save data object. */ export type Persistent<T extends State = State> = Ref<T> & { + value: T; /** A flag that this is a persistent property. Typically a circular reference. */ [PersistentState]: Ref<T>; /** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */ @@ -89,31 +92,65 @@ function getStackTrace() { ); } +function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T) { + // Decimal is smart enough to return false on things that aren't supposed to be numbers + if (Decimal.isNaN(value as DecimalSource)) { + if (!state.hasNaN) { + player.autosave = false; + state.hasNaN = true; + state.NaNPath = persistent[SaveDataPath]; + state.NaNPersistent = persistent as Persistent<DecimalSource>; + } + console.error( + `Attempted to save NaN value to`, + persistent[SaveDataPath]?.join("."), + persistent + ); + throw "Attempted to set NaN value. See above for details"; + } + persistent[PersistentState].value = value; +} + /** * Create a persistent ref, which can be saved and loaded. * All (non-deleted) persistent refs must be included somewhere within the layer object returned by that layer's options func. * @param defaultValue The value the persistent ref should start at on fresh saves or when reset. */ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> { - const persistent = ( - isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown) - ) as Persistent<T>; + const persistentState: Ref<T> = isRef(defaultValue) + ? defaultValue + : (ref<T>(defaultValue) as Ref<T>); - persistent[PersistentState] = persistent; - persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; - persistent[StackTrace] = getStackTrace(); - persistent[Deleted] = false; - const nonPersistent: Partial<NonPersistent<T>> = computed({ + if (isRef(defaultValue)) { + defaultValue = defaultValue.value; + } + + const nonPersistent = computed({ get() { - return persistent.value; + return persistentState.value; }, set(value) { - persistent.value = value; + checkNaNAndWrite(persistent, value); } - }); - nonPersistent[DefaultValue] = persistent[DefaultValue]; - persistent[NonPersistent] = nonPersistent as NonPersistent<T>; - persistent[SaveDataPath] = undefined; + }) as NonPersistent<T>; + nonPersistent[DefaultValue] = defaultValue; + + // We're trying to mock a vue ref, which means the type expects a private [RefSymbol] property that we can't access, but the actual implementation of isRef just checks for `__v_isRef` + const persistent = { + get value() { + return persistentState.value as T; + }, + set value(value: T) { + checkNaNAndWrite(persistent, value); + }, + __v_isRef: true, + [PersistentState]: persistentState, + [DefaultValue]: defaultValue, + [StackTrace]: getStackTrace(), + [Deleted]: false, + [NonPersistent]: nonPersistent, + [SaveDataPath]: undefined + } as unknown as Persistent<T>; if (addingLayers.length === 0) { console.warn( @@ -125,7 +162,7 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent); } - return persistent as Persistent<T>; + return persistent; } /** diff --git a/src/game/player.ts b/src/game/player.ts index 6271629..5d1142e 100644 --- a/src/game/player.ts +++ b/src/game/player.ts @@ -1,13 +1,8 @@ -import { isPlainObject } from "is-plain-object"; -import Decimal from "util/bignum"; -import type { ProxiedWithState } from "util/proxies"; -import { ProxyPath, ProxyState } from "util/proxies"; -import { reactive, unref } from "vue"; import type { Ref } from "vue"; -import transientState from "./state"; +import { reactive, unref } from "vue"; /** The player save data object. */ -export interface PlayerData { +export interface Player { /** The ID of this save. */ id: string; /** A multiplier for time passing. Set to 0 when the game is paused. */ @@ -36,9 +31,6 @@ export interface PlayerData { layers: Record<string, LayerData<unknown>>; } -/** The proxied player that is used to track NaN values. */ -export type Player = ProxiedWithState<PlayerData>; - /** A layer's save data. Automatically unwraps refs. */ export type LayerData<T> = { [P in keyof T]?: T[P] extends (infer U)[] @@ -52,7 +44,7 @@ export type LayerData<T> = { : T[P]; }; -const state = reactive<PlayerData>({ +const player = reactive<Player>({ id: "", devSpeed: null, name: "", @@ -68,90 +60,16 @@ const state = reactive<PlayerData>({ layers: {} }); +export default window.player = player; + /** Convert a player save data object into a JSON string. Unwraps refs. */ -export function stringifySave(player: PlayerData): string { +export function stringifySave(player: Player): string { return JSON.stringify(player, (key, value) => unref(value)); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const playerHandler: ProxyHandler<Record<PropertyKey, any>> = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get(target: Record<PropertyKey, any>, key: PropertyKey): any { - if (key === ProxyState || key === ProxyPath) { - return target[key]; - } - - const value = target[ProxyState][key]; - if (key !== "value" && (isPlainObject(value) || Array.isArray(value))) { - if (value !== target[key]?.[ProxyState]) { - const path = [...target[ProxyPath], key]; - target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler); - } - return target[key]; - } - - return value; - }, - set( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - target: Record<PropertyKey, any>, - property: PropertyKey, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any, - receiver: ProxyConstructor - ): boolean { - if ( - !transientState.hasNaN && - ((typeof value === "number" && isNaN(value)) || - (value instanceof Decimal && - (isNaN(value.sign) || isNaN(value.layer) || isNaN(value.mag)))) - ) { - const currentValue = target[ProxyState][property]; - if ( - !( - (typeof currentValue === "number" && isNaN(currentValue)) || - (currentValue instanceof Decimal && - (isNaN(currentValue.sign) || - isNaN(currentValue.layer) || - isNaN(currentValue.mag))) - ) - ) { - state.autosave = false; - transientState.hasNaN = true; - transientState.NaNPath = [...target[ProxyPath], property]; - transientState.NaNReceiver = receiver as unknown as Record<string, unknown>; - console.error( - `Attempted to set NaN value`, - [...target[ProxyPath], property], - target[ProxyState] - ); - throw "Attempted to set NaN value. See above for details"; - } - } - target[ProxyState][property] = value; - return true; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ownKeys(target: Record<PropertyKey, any>) { - return Reflect.ownKeys(target[ProxyState]); - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - has(target: Record<PropertyKey, any>, key: string) { - return Reflect.has(target[ProxyState], key); - }, - getOwnPropertyDescriptor(target, key) { - return Object.getOwnPropertyDescriptor(target[ProxyState], key); - } -}; - declare global { /** Augment the window object so the player can be accessed from the console. */ interface Window { player: Player; } } -/** The player save data object. */ -export default window.player = new Proxy( - { [ProxyState]: state, [ProxyPath]: ["player"] }, - playerHandler -) as Player; diff --git a/src/game/state.ts b/src/game/state.ts index bda0b6a..db71c7b 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -1,4 +1,6 @@ +import type { DecimalSource } from "util/bignum"; import { shallowReactive } from "vue"; +import type { Persistent } from "./persistence"; /** An object of global data that is not persistent. */ export interface Transient { @@ -8,8 +10,8 @@ export interface Transient { hasNaN: boolean; /** The location within the player save data object of the NaN value. */ NaNPath?: string[]; - /** The parent object of the NaN value. */ - NaNReceiver?: Record<string, unknown>; + /** The ref that was being set to NaN. */ + NaNPersistent?: Persistent<DecimalSource>; } declare global { diff --git a/src/util/computed.ts b/src/util/computed.ts index c940b1a..8a9ad62 100644 --- a/src/util/computed.ts +++ b/src/util/computed.ts @@ -1,6 +1,7 @@ +import type { JSXFunction } from "features/feature"; +import { isFunction } from "util/common"; import type { Ref } from "vue"; import { computed } from "vue"; -import { isFunction } from "util/common"; export const DoNotCache = Symbol("DoNotCache"); @@ -32,21 +33,22 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>( key: S ): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } { const computable = obj[key]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (isFunction(computable) && computable.length === 0 && !(computable as any)[DoNotCache]) { + if ( + isFunction(computable) && + computable.length === 0 && + !(computable as unknown as JSXFunction)[DoNotCache] + ) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore obj[key] = computed(computable.bind(obj)); } else if (isFunction(computable)) { obj[key] = computable.bind(obj) as unknown as T[S]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (obj[key] as any)[DoNotCache] = true; + (obj[key] as unknown as JSXFunction)[DoNotCache] = true; } } export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (isFunction(obj) && !(obj as any)[DoNotCache]) { + if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore obj = computed(obj); diff --git a/src/util/proxies.ts b/src/util/proxies.ts index 9dad138..174378b 100644 --- a/src/util/proxies.ts +++ b/src/util/proxies.ts @@ -1,11 +1,11 @@ +import type { Persistent } from "game/persistence"; import { NonPersistent } from "game/persistence"; import Decimal from "util/bignum"; export const ProxyState = Symbol("ProxyState"); export const ProxyPath = Symbol("ProxyPath"); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any> +export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, unknown> ? NonNullable<T> extends Decimal ? T : { @@ -16,6 +16,18 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any } : T; +export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown> + ? NonNullable<T> extends Persistent<infer S> + ? NonPersistent<S> + : NonNullable<T> extends Decimal + ? T + : { + [K in keyof T]: Proxied<T[K]>; + } & { + [ProxyState]: T; + } + : T; + // Takes a function that returns an object and pretends to be that object // Note that the object is lazily calculated export function createLazyProxy<T extends object, S extends T>( diff --git a/src/util/save.ts b/src/util/save.ts index e43d34b..2955fd1 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -1,13 +1,12 @@ import projInfo from "data/projInfo.json"; import { globalBus } from "game/events"; -import type { Player, PlayerData } from "game/player"; +import type { Player } from "game/player"; import player, { stringifySave } from "game/player"; import settings, { loadSettings } from "game/settings"; import LZString from "lz-string"; -import { ProxyState } from "util/proxies"; import { ref } from "vue"; -export function setupInitialStore(player: Partial<PlayerData> = {}): Player { +export function setupInitialStore(player: Partial<Player> = {}): Player { return Object.assign( { id: `${projInfo.id}-0`, @@ -27,11 +26,9 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player { ) as Player; } -export function save(playerData?: PlayerData): string { - const stringifiedSave = LZString.compressToUTF16( - stringifySave(playerData ?? player[ProxyState]) - ); - localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave); +export function save(playerData?: Player): string { + const stringifiedSave = LZString.compressToUTF16(stringifySave(playerData ?? player)); + localStorage.setItem((playerData ?? player).id, stringifiedSave); return stringifiedSave; } @@ -70,7 +67,7 @@ export async function load(): Promise<void> { } } -export function newSave(): PlayerData { +export function newSave(): Player { const id = getUniqueID(); const player = setupInitialStore({ id }); save(player); @@ -91,7 +88,7 @@ export function getUniqueID(): string { export const loadingSave = ref(false); -export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> { +export async function loadSave(playerObj: Partial<Player>): Promise<void> { console.info("Loading save", playerObj); loadingSave.value = true; const { layers, removeLayer, addLayer } = await import("game/layers"); From 3a4b15bd8f2f02f6b0b4dfdfaf1ed4a91b041163 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 31 Dec 2022 14:57:09 -0600 Subject: [PATCH 07/56] Implemented requirements system --- src/features/buyable.tsx | 108 ++++++++----------- src/features/upgrades/Upgrade.vue | 22 ++-- src/features/upgrades/upgrade.ts | 74 +++++--------- src/game/modifiers.tsx | 3 +- src/game/requirements.tsx | 165 ++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 131 deletions(-) create mode 100644 src/game/requirements.tsx diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx index 30edd29..cdff9a0 100644 --- a/src/features/buyable.tsx +++ b/src/features/buyable.tsx @@ -1,17 +1,17 @@ +import { isArray } from "@vue/shared"; import ClickableComponent from "features/clickables/Clickable.vue"; -import type { - CoercableComponent, - GenericComponent, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; +import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; -import type { Resource } from "features/resources/resource"; -import { DefaultValue, Persistent } from "game/persistence"; -import { persistent } from "game/persistence"; +import { DefaultValue, Persistent, persistent } from "game/persistence"; +import { + createVisibilityRequirement, + displayRequirements, + payRequirements, + Requirements, + requirementsMet +} from "game/requirements"; import type { DecimalSource } from "util/bignum"; -import Decimal, { format, formatWhole } from "util/bignum"; +import Decimal, { formatWhole } from "util/bignum"; import type { Computable, GetComputableType, @@ -37,9 +37,7 @@ export type BuyableDisplay = export interface BuyableOptions { visibility?: Computable<Visibility>; - cost?: Computable<DecimalSource>; - resource?: Resource; - canPurchase?: Computable<boolean>; + requirements: Requirements; purchaseLimit?: Computable<DecimalSource>; initialValue?: DecimalSource; classes?: Computable<Record<string, boolean>>; @@ -47,14 +45,13 @@ export interface BuyableOptions { mark?: Computable<boolean | string>; small?: Computable<boolean>; display?: Computable<BuyableDisplay>; - onPurchase?: (cost: DecimalSource | undefined) => void; + onPurchase?: VoidFunction; } export interface BaseBuyable { id: string; amount: Persistent<DecimalSource>; maxed: Ref<boolean>; - canAfford: Ref<boolean>; canClick: ProcessedComputable<boolean>; onClick: VoidFunction; purchase: VoidFunction; @@ -67,9 +64,7 @@ export type Buyable<T extends BuyableOptions> = Replace< T & BaseBuyable, { visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>; - cost: GetComputableType<T["cost"]>; - resource: GetComputableType<T["resource"]>; - canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>; + requirements: GetComputableType<T["requirements"]>; purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>; classes: GetComputableType<T["classes"]>; style: GetComputableType<T["style"]>; @@ -83,7 +78,6 @@ export type GenericBuyable = Replace< Buyable<BuyableOptions>, { visibility: ProcessedComputable<Visibility>; - canPurchase: ProcessedComputable<boolean>; purchaseLimit: ProcessedComputable<DecimalSource>; } >; @@ -95,40 +89,31 @@ export function createBuyable<T extends BuyableOptions>( return createLazyProxy(() => { 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.type = BuyableType; buyable[Component] = ClickableComponent; buyable.amount = amount; buyable.amount[DefaultValue] = buyable.initialValue ?? 0; - buyable.canAfford = computed(() => { - const genericBuyable = buyable as GenericBuyable; - const cost = unref(genericBuyable.cost); - 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( - (buyable as GenericBuyable).amount.value, - unref((buyable as GenericBuyable).purchaseLimit) - ) - ); + + const limitRequirement = { + requirementMet: computed(() => + Decimal.lt( + (buyable as GenericBuyable).amount.value, + 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(() => Decimal.gte( (buyable as GenericBuyable).amount.value, @@ -144,26 +129,18 @@ export function createBuyable<T extends BuyableOptions>( } return currClasses; }); - processComputable(buyable as T, "canPurchase"); - buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>; + buyable.canClick = computed(() => requirementsMet(buyable.requirements)); buyable.onClick = buyable.purchase = buyable.onClick ?? buyable.purchase ?? function (this: GenericBuyable) { const genericBuyable = buyable as GenericBuyable; - if (!unref(genericBuyable.canPurchase)) { + if (!unref(genericBuyable.canClick)) { return; } - const cost = unref(genericBuyable.cost); - 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.onPurchase?.(cost); + payRequirements(buyable.requirements); + genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); + genericBuyable.onPurchase?.(); }; processComputable(buyable as T, "display"); const display = buyable.display; @@ -174,7 +151,7 @@ export function createBuyable<T extends BuyableOptions>( const CurrDisplay = coerceComponent(currDisplay); return <CurrDisplay />; } - if (currDisplay != null && buyable.cost != null && buyable.resource != null) { + if (currDisplay != null) { const genericBuyable = buyable as GenericBuyable; const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Description = coerceComponent(currDisplay.description ?? ""); @@ -207,13 +184,12 @@ export function createBuyable<T extends BuyableOptions>( Currently: <EffectDisplay /> </div> )} - {genericBuyable.cost != null && !genericBuyable.maxed.value ? ( + {genericBuyable.maxed.value ? null : ( <div> <br /> - Cost: {format(unref(genericBuyable.cost))}{" "} - {buyable.resource.displayName} + {displayRequirements(genericBuyable.requirements)} </div> - ) : null} + )} </span> ); } @@ -222,8 +198,6 @@ export function createBuyable<T extends BuyableOptions>( processComputable(buyable as T, "visibility"); setDefault(buyable, "visibility", Visibility.Visible); - processComputable(buyable as T, "cost"); - processComputable(buyable as T, "resource"); processComputable(buyable as T, "purchaseLimit"); setDefault(buyable, "purchaseLimit", Decimal.dInf); processComputable(buyable as T, "style"); diff --git a/src/features/upgrades/Upgrade.vue b/src/features/upgrades/Upgrade.vue index 3f6903c..7c45d75 100644 --- a/src/features/upgrades/Upgrade.vue +++ b/src/features/upgrades/Upgrade.vue @@ -30,10 +30,8 @@ import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { StyleValue } 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 { DecimalSource } from "util/bignum"; +import { displayRequirements, Requirements } from "game/requirements"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue"; import type { Component, PropType, UnwrapRef } from "vue"; import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue"; @@ -50,8 +48,10 @@ export default defineComponent({ }, style: processedPropType<StyleValue>(String, Object, Array), classes: processedPropType<Record<string, boolean>>(Object), - resource: Object as PropType<Resource>, - cost: processedPropType<DecimalSource>(String, Object, Number), + requirements: { + type: Object as PropType<Requirements>, + required: true + }, canPurchase: { type: processedPropType<boolean>(Boolean), required: true @@ -75,7 +75,7 @@ export default defineComponent({ MarkNode }, setup(props) { - const { display, cost } = toRefs(props); + const { display, requirements, bought } = toRefs(props); const component = shallowRef<Component | string>(""); @@ -89,7 +89,6 @@ export default defineComponent({ component.value = coerceComponent(currDisplay); return; } - const currCost = unwrapRef(cost); const Title = coerceComponent(currDisplay.title || "", "h3"); const Description = coerceComponent(currDisplay.description, "div"); const EffectDisplay = coerceComponent(currDisplay.effectDisplay || ""); @@ -107,14 +106,7 @@ export default defineComponent({ Currently: <EffectDisplay /> </div> ) : null} - {props.resource != null ? ( - <> - <br /> - Cost: {props.resource && - displayResource(props.resource, currCost)}{" "} - {props.resource?.displayName} - </> - ) : null} + {bought.value ? null : <><br />{displayRequirements(requirements.value)}</>} </span> )) ); diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 3235e45..656d891 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -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 { Component, findFeatures, @@ -7,13 +14,16 @@ import { setDefault, Visibility } from "features/feature"; -import type { Resource } from "features/resources/resource"; import UpgradeComponent from "features/upgrades/Upgrade.vue"; import type { GenericLayer } from "game/layers"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; -import type { DecimalSource } from "util/bignum"; -import Decimal from "util/bignum"; +import { + createVisibilityRequirement, + payRequirements, + Requirements, + requirementsMet +} from "game/requirements"; import { isFunction } from "util/common"; import type { Computable, @@ -40,10 +50,8 @@ export interface UpgradeOptions { effectDisplay?: CoercableComponent; } >; + requirements: Requirements; mark?: Computable<boolean | string>; - cost?: Computable<DecimalSource>; - resource?: Resource; - canAfford?: Computable<boolean>; onPurchase?: VoidFunction; } @@ -64,9 +72,8 @@ export type Upgrade<T extends UpgradeOptions> = Replace< classes: GetComputableType<T["classes"]>; style: GetComputableType<T["style"]>; display: GetComputableType<T["display"]>; + requirements: GetComputableType<T["requirements"]>; mark: GetComputableType<T["mark"]>; - cost: GetComputableType<T["cost"]>; - canAfford: GetComputableTypeWithDefault<T["canAfford"], Ref<boolean>>; } >; @@ -74,7 +81,6 @@ export type GenericUpgrade = Replace< Upgrade<UpgradeOptions>, { visibility: ProcessedComputable<Visibility>; - canPurchase: ProcessedComputable<boolean>; } >; @@ -88,55 +94,31 @@ export function createUpgrade<T extends UpgradeOptions>( upgrade.type = UpgradeType; 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; - if (upgrade.canAfford == null) { - 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.canPurchase = computed(() => requirementsMet(upgrade.requirements)); upgrade.purchase = function () { const genericUpgrade = upgrade as GenericUpgrade; if (!unref(genericUpgrade.canPurchase)) { return; } - if (genericUpgrade.resource != null && genericUpgrade.cost != null) { - genericUpgrade.resource.value = Decimal.sub( - genericUpgrade.resource.value, - unref(genericUpgrade.cost) - ); - } + payRequirements(upgrade.requirements); bought.value = true; 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"); setDefault(upgrade, "visibility", Visibility.Visible); processComputable(upgrade as T, "classes"); processComputable(upgrade as T, "style"); processComputable(upgrade as T, "display"); processComputable(upgrade as T, "mark"); - processComputable(upgrade as T, "cost"); - processComputable(upgrade as T, "resource"); upgrade[GatherProps] = function (this: GenericUpgrade) { const { @@ -144,8 +126,7 @@ export function createUpgrade<T extends UpgradeOptions>( visibility, style, classes, - resource, - cost, + requirements, canPurchase, bought, mark, @@ -157,8 +138,7 @@ export function createUpgrade<T extends UpgradeOptions>( visibility, style: unref(style), classes, - resource, - cost, + requirements, canPurchase, bought, mark, diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index 851423e..830e937 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -14,8 +14,7 @@ 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}. + * 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. */ diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx new file mode 100644 index 0000000..607800b --- /dev/null +++ b/src/game/requirements.tsx @@ -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?.(); + } +} From f1dc5dd573a9fd239a03479812b8077d2823f5cc Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 3 Jan 2023 20:56:48 -0600 Subject: [PATCH 08/56] Update b_e --- src/lib/break_eternity.ts | 773 ++++++++++++++------------------------ src/lib/lru-cache.ts | 139 +++++++ 2 files changed, 413 insertions(+), 499 deletions(-) create mode 100644 src/lib/lru-cache.ts diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts index e3f61db..e9f7099 100644 --- a/src/lib/break_eternity.ts +++ b/src/lib/break_eternity.ts @@ -1,4 +1,7 @@ /* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable @typescript-eslint/no-loss-of-precision */ +import { LRUCache } from "../lib/lru-cache"; + export type CompareResult = -1 | 0 | 1; const MAX_SIGNIFICANT_DIGITS = 17; //Maximum number of digits of precision to assume in Number @@ -15,10 +18,12 @@ const NUMBER_EXP_MIN = -324; //The smallest exponent that can appear in a Number const MAX_ES_IN_A_ROW = 5; //For default toString behaviour, when to swap from eee... to (e^n) syntax. +const DEFAULT_FROM_STRING_CACHE_SIZE = (1 << 10) - 1; // The default size of the LRU cache used to cache Decimal.fromString. + const IGNORE_COMMAS = true; const COMMAS_ARE_DECIMAL_POINTS = false; -const powerOf10 = (function() { +const powerOf10 = (function () { // We need this lookup table because Math.pow(10, exponent) // when exponent's absolute value is large is slightly inaccurate. // You can fix it with the power of math... or just make a lookup table. @@ -30,7 +35,7 @@ const powerOf10 = (function() { } const indexOf0InPowersOf10 = 323; - return function(power: number) { + return function (power: number) { return powersOf10[power + indexOf0InPowersOf10]; }; })(); @@ -40,160 +45,80 @@ const powerOf10 = (function() { const critical_headers = [2, Math.E, 3, 4, 5, 6, 7, 8, 9, 10]; const critical_tetr_values = [ [ - // Base 2 - 1, - 1.0891168053867777, - 1.1789745164521264, - 1.2701428397304229, - 1.3632066654400328, - 1.4587804913784246, - 1.557523817412741, - 1.660158301473385, - 1.767487542936873, - 1.8804205225512542, - 2 + // Base 2 (using http://myweb.astate.edu/wpaulsen/tetcalc/tetcalc.html ) + 1, 1.0891180521811202527, 1.1789767925673958433, 1.2701455431742086633, + 1.3632090180450091941, 1.4587818160364217007, 1.5575237916251418333, 1.6601571006859253673, + 1.7674858188369780435, 1.8804192098842727359, 2 ], [ - // Base E + // Base E (using http://myweb.astate.edu/wpaulsen/tetcalc/tetcalc.html ) 1, //0.0 - 1.1121114330934, //0.1 - 1.23103892493161, //0.2 - 1.35838369631113, //0.3 - 1.49605193039935, //0.4 - 1.64635423375119, //0.5 - 1.81213853570186, //0.6 - 1.99697132461829, //0.7 - 2.20538955455724, //0.8 - 2.44325744833852, //0.9 + 1.1121114330934078681, //0.1 + 1.2310389249316089299, //0.2 + 1.3583836963111376089, //0.3 + 1.4960519303993531879, //0.4 + 1.646354233751194581, //0.5 + 1.8121385357018724464, //0.6 + 1.9969713246183068478, //0.7 + 2.205389554552754433, //0.8 + 2.4432574483385252544, //0.9 Math.E //1.0 ], [ // Base 3 - 1, - 1.1187738849693603, - 1.2464963939368214, - 1.38527004705667, - 1.5376664685821402, - 1.7068895236551784, - 1.897001227148399, - 2.1132403089001035, - 2.362480153784171, - 2.6539010333870774, - 3 + 1, 1.1187738849693603, 1.2464963939368214, 1.38527004705667, 1.5376664685821402, + 1.7068895236551784, 1.897001227148399, 2.1132403089001035, 2.362480153784171, + 2.6539010333870774, 3 ], [ // Base 4 - 1, - 1.1367350847096405, - 1.2889510672956703, - 1.4606478703324786, - 1.6570295196661111, - 1.8850062585672889, - 2.1539465047453485, - 2.476829779693097, - 2.872061932789197, - 3.3664204535587183, - 4 + 1, 1.1367350847096405, 1.2889510672956703, 1.4606478703324786, 1.6570295196661111, + 1.8850062585672889, 2.1539465047453485, 2.476829779693097, 2.872061932789197, + 3.3664204535587183, 4 ], [ // Base 5 - 1, - 1.1494592900767588, - 1.319708228183931, - 1.5166291280087583, - 1.748171114438024, - 2.0253263297298045, - 2.3636668498288547, - 2.7858359149579424, - 3.3257226212448145, - 4.035730287722532, - 5 + 1, 1.1494592900767588, 1.319708228183931, 1.5166291280087583, 1.748171114438024, + 2.0253263297298045, 2.3636668498288547, 2.7858359149579424, 3.3257226212448145, + 4.035730287722532, 5 ], [ // Base 6 - 1, - 1.159225940787673, - 1.343712473580932, - 1.5611293155111927, - 1.8221199554561318, - 2.14183924486326, - 2.542468319282638, - 3.0574682501653316, - 3.7390572020926873, - 4.6719550537360774, - 6 + 1, 1.159225940787673, 1.343712473580932, 1.5611293155111927, 1.8221199554561318, + 2.14183924486326, 2.542468319282638, 3.0574682501653316, 3.7390572020926873, + 4.6719550537360774, 6 ], [ // Base 7 - 1, - 1.1670905356972596, - 1.3632807444991446, - 1.5979222279405536, - 1.8842640123816674, - 2.2416069644878687, - 2.69893426559423, - 3.3012632110403577, - 4.121250340630164, - 5.281493033448316, - 7 + 1, 1.1670905356972596, 1.3632807444991446, 1.5979222279405536, 1.8842640123816674, + 2.2416069644878687, 2.69893426559423, 3.3012632110403577, 4.121250340630164, + 5.281493033448316, 7 ], [ // Base 8 - 1, - 1.1736630594087796, - 1.379783782386201, - 1.6292821855668218, - 1.9378971836180754, - 2.3289975651071977, - 2.8384347394720835, - 3.5232708454565906, - 4.478242031114584, - 5.868592169644505, - 8 + 1, 1.1736630594087796, 1.379783782386201, 1.6292821855668218, 1.9378971836180754, + 2.3289975651071977, 2.8384347394720835, 3.5232708454565906, 4.478242031114584, + 5.868592169644505, 8 ], [ // Base 9 - 1, - 1.1793017514670474, - 1.394054150657457, - 1.65664127441059, - 1.985170999970283, - 2.4069682290577457, - 2.9647310119960752, - 3.7278665320924946, - 4.814462547283592, - 6.436522247411611, - 9 + 1, 1.1793017514670474, 1.394054150657457, 1.65664127441059, 1.985170999970283, + 2.4069682290577457, 2.9647310119960752, 3.7278665320924946, 4.814462547283592, + 6.436522247411611, 9 ], [ - // Base 10 - 1, - 1.18422737399915, - 1.4066113788546144, - 1.680911177655277, - 2.027492094355525, - 2.4775152854601967, - 3.080455730250329, - 3.918234505962507, - 5.1332705696484595, - 6.9878696918072905, - 10 + // Base 10 (using http://myweb.astate.edu/wpaulsen/tetcalc/tetcalc.html ) + 1, 1.1840100246247336579, 1.4061375836156954169, 1.6802272208863963918, + 2.026757028388618927, 2.477005606344964758, 3.0805252717554819987, 3.9191964192627283911, + 5.135152840833186423, 6.9899611795347148455, 10 ] ]; const critical_slog_values = [ [ // Base 2 - -1, - -0.9194161097107025, - -0.8335625019330468, - -0.7425599821143978, - -0.6466611521029437, - -0.5462617907227869, - -0.4419033816638769, - -0.3342645487554494, - -0.224140440909962, - -0.11241087890006762, - 0 + -1, -0.9194161097107025, -0.8335625019330468, -0.7425599821143978, -0.6466611521029437, + -0.5462617907227869, -0.4419033816638769, -0.3342645487554494, -0.224140440909962, + -0.11241087890006762, 0 ], [ // Base E @@ -211,135 +136,73 @@ const critical_slog_values = [ ], [ // Base 3 - -1, - -0.9021579584316141, - -0.8005762598234203, - -0.6964780623319391, - -0.5911906810998454, - -0.486050182576545, - -0.3823089430815083, - -0.28106046722897615, - -0.1831906535795894, - -0.08935809204418144, - 0 + -1, -0.9021579584316141, -0.8005762598234203, -0.6964780623319391, -0.5911906810998454, + -0.486050182576545, -0.3823089430815083, -0.28106046722897615, -0.1831906535795894, + -0.08935809204418144, 0 ], [ // Base 4 - -1, - -0.8917227442365535, - -0.781258746326964, - -0.6705130326902455, - -0.5612813129406509, - -0.4551067709033134, - -0.35319256652135966, - -0.2563741554088552, - -0.1651412821106526, - -0.0796919581982668, - 0 + -1, -0.8917227442365535, -0.781258746326964, -0.6705130326902455, -0.5612813129406509, + -0.4551067709033134, -0.35319256652135966, -0.2563741554088552, -0.1651412821106526, + -0.0796919581982668, 0 ], [ // Base 5 - -1, - -0.8843387974366064, - -0.7678744063886243, - -0.6529563724510552, - -0.5415870994657841, - -0.4352842206588936, - -0.33504449124791424, - -0.24138853420685147, - -0.15445285440944467, - -0.07409659641336663, - 0 + -1, -0.8843387974366064, -0.7678744063886243, -0.6529563724510552, -0.5415870994657841, + -0.4352842206588936, -0.33504449124791424, -0.24138853420685147, -0.15445285440944467, + -0.07409659641336663, 0 ], [ // Base 6 - -1, - -0.8786709358426346, - -0.7577735191184886, - -0.6399546189952064, - -0.527284921869926, - -0.4211627631006314, - -0.3223479611761232, - -0.23107655627789858, - -0.1472057700818259, - -0.07035171210706326, - 0 + -1, -0.8786709358426346, -0.7577735191184886, -0.6399546189952064, -0.527284921869926, + -0.4211627631006314, -0.3223479611761232, -0.23107655627789858, -0.1472057700818259, + -0.07035171210706326, 0 ], [ // Base 7 - -1, - -0.8740862815291583, - -0.7497032990976209, - -0.6297119746181752, - -0.5161838335958787, - -0.41036238255751956, - -0.31277212146489963, - -0.2233976621705518, - -0.1418697367979619, - -0.06762117662323441, - 0 + -1, -0.8740862815291583, -0.7497032990976209, -0.6297119746181752, -0.5161838335958787, + -0.41036238255751956, -0.31277212146489963, -0.2233976621705518, -0.1418697367979619, + -0.06762117662323441, 0 ], [ // Base 8 - -1, - -0.8702632331800649, - -0.7430366914122081, - -0.6213373075161548, - -0.5072025698095242, - -0.40171437727184167, - -0.30517930701410456, - -0.21736343968190863, - -0.137710238299109, - -0.06550774483471955, - 0 + -1, -0.8702632331800649, -0.7430366914122081, -0.6213373075161548, -0.5072025698095242, + -0.40171437727184167, -0.30517930701410456, -0.21736343968190863, -0.137710238299109, + -0.06550774483471955, 0 ], [ // Base 9 - -1, - -0.8670016295947213, - -0.7373984232432306, - -0.6143173985094293, - -0.49973884395492807, - -0.394584953527678, - -0.2989649949848695, - -0.21245647317021688, - -0.13434688362382652, - -0.0638072667348083, - 0 + -1, -0.8670016295947213, -0.7373984232432306, -0.6143173985094293, -0.49973884395492807, + -0.394584953527678, -0.2989649949848695, -0.21245647317021688, -0.13434688362382652, + -0.0638072667348083, 0 ], [ // Base 10 - -1, - -0.8641642839543857, - -0.732534623168535, - -0.6083127477059322, - -0.4934049257184696, - -0.3885773075899922, - -0.29376029055315767, - -0.2083678561173622, - -0.13155653399373268, - -0.062401588652553186, - 0 + -1, -0.8641642839543857, -0.732534623168535, -0.6083127477059322, -0.4934049257184696, + -0.3885773075899922, -0.29376029055315767, -0.2083678561173622, -0.13155653399373268, + -0.062401588652553186, 0 ] ]; -const D = function D(value: DecimalSource): Decimal { +let D = function D(value: DecimalSource): Readonly<Decimal> { return Decimal.fromValue_noAlloc(value); }; -const FC = function(sign: number, layer: number, mag: number) { +let FC = function (sign: number, layer: number, mag: number) { return Decimal.fromComponents(sign, layer, mag); }; -const FC_NN = function FC_NN(sign: number, layer: number, mag: number) { +let FC_NN = function FC_NN(sign: number, layer: number, mag: number) { return Decimal.fromComponents_noNormalize(sign, layer, mag); }; -const ME = function ME(mantissa: number, exponent: number) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let ME = function ME(mantissa: number, exponent: number) { return Decimal.fromMantissaExponent(mantissa, exponent); }; -const ME_NN = function ME_NN(mantissa: number, exponent: number) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +let ME_NN = function ME_NN(mantissa: number, exponent: number) { return Decimal.fromMantissaExponent_noNormalize(mantissa, exponent); }; @@ -351,12 +214,12 @@ const decimalPlaces = function decimalPlaces(value: number, places: number): num return parseFloat(rounded.toFixed(Math.max(len - numDigits, 0))); }; -const f_maglog10 = function(n: number) { +const f_maglog10 = function (n: number) { return Math.sign(n) * Math.log10(Math.abs(n)); }; //from HyperCalc source code -const f_gamma = function(n: number) { +const f_gamma = function (n: number) { if (!isFinite(n)) { return n; } @@ -403,7 +266,7 @@ const _EXPN1 = 0.36787944117144232159553; // exp(-1) const OMEGA = 0.56714329040978387299997; // W(1, 0) //from https://math.stackexchange.com/a/465183 // The evaluation can become inaccurate very close to the branch point -const f_lambertw = function(z: number, tol = 1e-10): number { +const f_lambertw = function (z: number, tol = 1e-10): number { let w; let wn; @@ -442,38 +305,28 @@ const f_lambertw = function(z: number, tol = 1e-10): number { // fail to converge, or can end up on the wrong branch. function d_lambertw(z: Decimal, tol = 1e-10): Decimal { let w; - let ew, wew, wewz, wn; + let ew, wewz, wn; if (!Number.isFinite(z.mag)) { return z; } - if (z === Decimal.dZero) { + if (z.eq(Decimal.dZero)) { return z; } - if (z === Decimal.dOne) { + if (z.eq(Decimal.dOne)) { //Split out this case because the asymptotic series blows up - return D(OMEGA); + return Decimal.fromNumber(OMEGA); } - const absz = Decimal.abs(z); //Get an initial guess for Halley's method w = Decimal.ln(z); //Halley's method; see 5.9 in [1] for (let i = 0; i < 100; ++i) { - ew = Decimal.exp(-w); + ew = w.neg().exp(); wewz = w.sub(z.mul(ew)); - wn = w.sub( - wewz.div( - w.add(1).sub( - w - .add(2) - .mul(wewz) - .div(Decimal.mul(2, w).add(2)) - ) - ) - ); + wn = w.sub(wewz.div(w.add(1).sub(w.add(2).mul(wewz).div(Decimal.mul(2, w).add(2))))); if (Decimal.abs(wn.sub(w)).lt(Decimal.abs(wn).mul(tol))) { return wn; } else { @@ -502,21 +355,19 @@ export default class Decimal { public static readonly dNumberMax = FC(1, 0, Number.MAX_VALUE); public static readonly dNumberMin = FC(1, 0, Number.MIN_VALUE); - public sign: number = Number.NaN; - public mag: number = Number.NaN; - public layer: number = Number.NaN; + private static fromStringCache = new LRUCache<string, Decimal>(DEFAULT_FROM_STRING_CACHE_SIZE); + + public sign = 0; + public mag = 0; + public layer = 0; constructor(value?: DecimalSource) { - if (value instanceof Decimal || (value != null && typeof value === "object" && "sign" in value && "mag" in value && "layer" in value)) { + if (value instanceof Decimal) { this.fromDecimal(value); } else if (typeof value === "number") { this.fromNumber(value); } else if (typeof value === "string") { this.fromString(value); - } else { - this.sign = 0; - this.layer = 0; - this.mag = 0; } } @@ -549,8 +400,8 @@ export default class Decimal { //don't even pretend mantissa is meaningful this.sign = Math.sign(value); if (this.sign === 0) { - this.layer === 0; - this.exponent === 0; + this.layer = 0; + this.exponent = 0; } } } @@ -633,8 +484,32 @@ export default class Decimal { return new Decimal().fromValue(value); } - public static fromValue_noAlloc(value: DecimalSource): Decimal { - return value instanceof Decimal ? value : new Decimal(value); + /** + * Converts a DecimalSource to a Decimal, without constructing a new Decimal + * if the provided value is already a Decimal. + * + * As the return value could be the provided value itself, this function + * returns a read-only Decimal to prevent accidental mutations of the value. + * Use `new Decimal(value)` to explicitly create a writeable copy if mutation + * is required. + */ + public static fromValue_noAlloc(value: DecimalSource): Readonly<Decimal> { + if (value instanceof Decimal) { + return value; + } else if (typeof value === "string") { + const cached = Decimal.fromStringCache.get(value); + if (cached !== undefined) { + return cached; + } + return Decimal.fromString(value); + } else if (typeof value === "number") { + return Decimal.fromNumber(value); + } else { + // This should never happen... but some users like Prestige Tree Rewritten + // pass undefined values in as DecimalSources, so we should handle this + // case to not break them. + return Decimal.dZero; + } } public static abs(value: DecimalSource): Decimal { @@ -1192,19 +1067,19 @@ export default class Decimal { public normalize(): this { /* - PSEUDOCODE: - Whenever we are partially 0 (sign is 0 or mag and layer is 0), make it fully 0. - Whenever we are at or hit layer 0, extract sign from negative mag. - If layer === 0 and mag < FIRST_NEG_LAYER (1/9e15), shift to 'first negative layer' (add layer, log10 mag). - While abs(mag) > EXP_LIMIT (9e15), layer += 1, mag = maglog10(mag). - While abs(mag) < LAYER_DOWN (15.954) and layer > 0, layer -= 1, mag = pow(10, mag). - - When we're done, all of the following should be true OR one of the numbers is not IsFinite OR layer is not IsInteger (error state): - Any 0 is totally zero (0, 0, 0). - Anything layer 0 has mag 0 OR mag > 1/9e15 and < 9e15. - Anything layer 1 or higher has abs(mag) >= 15.954 and < 9e15. - We will assume in calculations that all Decimals are either erroneous or satisfy these criteria. (Otherwise: Garbage in, garbage out.) - */ + PSEUDOCODE: + Whenever we are partially 0 (sign is 0 or mag and layer is 0), make it fully 0. + Whenever we are at or hit layer 0, extract sign from negative mag. + If layer === 0 and mag < FIRST_NEG_LAYER (1/9e15), shift to 'first negative layer' (add layer, log10 mag). + While abs(mag) > EXP_LIMIT (9e15), layer += 1, mag = maglog10(mag). + While abs(mag) < LAYER_DOWN (15.954) and layer > 0, layer -= 1, mag = pow(10, mag). + + When we're done, all of the following should be true OR one of the numbers is not IsFinite OR layer is not IsInteger (error state): + Any 0 is totally zero (0, 0, 0). + Anything layer 0 has mag 0 OR mag > 1/9e15 and < 9e15. + Anything layer 1 or higher has abs(mag) >= 15.954 and < 9e15. + We will assume in calculations that all Decimals are either erroneous or satisfy these criteria. (Otherwise: Garbage in, garbage out.) + */ if (this.sign === 0 || (this.mag === 0 && this.layer === 0)) { this.sign = 0; this.mag = 0; @@ -1306,6 +1181,11 @@ export default class Decimal { } public fromString(value: string): Decimal { + const originalValue = value; + const cached = Decimal.fromStringCache.get(originalValue); + if (cached !== undefined) { + return this.fromDecimal(cached); + } if (IGNORE_COMMAS) { value = value.replace(",", ""); } else if (COMMAS_ARE_DECIMAL_POINTS) { @@ -1330,6 +1210,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1352,6 +1235,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1366,6 +1252,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1391,6 +1280,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1411,6 +1303,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1422,13 +1317,21 @@ export default class Decimal { if (ecount === 0) { const numberAttempt = parseFloat(value); if (isFinite(numberAttempt)) { - return this.fromNumber(numberAttempt); + this.fromNumber(numberAttempt); + if (Decimal.fromStringCache.size >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } + return this; } } else if (ecount === 1) { //Very small numbers ("2e-3000" and so on) may look like valid floats but round to 0. const numberAttempt = parseFloat(value); if (isFinite(numberAttempt) && numberAttempt !== 0) { - return this.fromNumber(numberAttempt); + this.fromNumber(numberAttempt); + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } + return this; } } @@ -1450,6 +1353,9 @@ export default class Decimal { this.layer = parseFloat(layerstring); this.mag = parseFloat(newparts[1].substr(i + 1)); this.normalize(); + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } } @@ -1459,6 +1365,9 @@ export default class Decimal { this.sign = 0; this.layer = 0; this.mag = 0; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } const mantissa = parseFloat(parts[0]); @@ -1466,6 +1375,9 @@ export default class Decimal { this.sign = 0; this.layer = 0; this.mag = 0; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } let exponent = parseFloat(parts[parts.length - 1]); @@ -1500,6 +1412,9 @@ export default class Decimal { this.sign = result.sign; this.layer = result.layer; this.mag = result.mag; + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } else { //at eee and above, mantissa is too small to be recognizable! @@ -1508,11 +1423,14 @@ export default class Decimal { } this.normalize(); + if (Decimal.fromStringCache.maxSize >= 1) { + Decimal.fromStringCache.set(originalValue, Decimal.fromDecimal(this)); + } return this; } public fromValue(value: DecimalSource): Decimal { - if (value instanceof Decimal || (value != null && typeof value === "object" && "sign" in value && "mag" in value && "layer" in value)) { + if (value instanceof Decimal) { return this.fromDecimal(value); } @@ -1770,7 +1688,7 @@ export default class Decimal { } if (a.layer === 0 && b.layer === 0) { - return D(a.sign * a.mag + b.sign * b.mag); + return Decimal.fromNumber(a.sign * a.mag + b.sign * b.mag); } const layera = a.layer * Math.sign(a.mag); @@ -1869,7 +1787,7 @@ export default class Decimal { } if (a.layer === 0 && b.layer === 0) { - return D(a.sign * b.sign * a.mag * b.mag); + return Decimal.fromNumber(a.sign * b.sign * a.mag * b.mag); } //Special case: If one of the numbers is layer 3 or higher or one of the numbers is 2+ layers bigger than the other, just take the bigger number. @@ -2225,10 +2143,7 @@ export default class Decimal { return a; } - const result = a - .absLog10() - .mul(b) - .pow10(); + const result = a.absLog10().mul(b).pow10(); if (this.sign === -1) { if (Math.abs(b.toNumber() % 2) % 2 === 1) { @@ -2255,7 +2170,7 @@ export default class Decimal { return Decimal.dNaN; } - let a = this; + let a: Decimal = this; //handle layer 0 case - if no precision is lost just use Math.pow, else promote one layer if (a.layer === 0) { @@ -2266,7 +2181,7 @@ export default class Decimal { if (a.sign === 0) { return Decimal.dOne; } else { - a = FC_NN(a.sign, a.layer + 1, Math.log10(a.mag)) as this; + a = FC_NN(a.sign, a.layer + 1, Math.log10(a.mag)); } } } @@ -2309,7 +2224,7 @@ export default class Decimal { return this.recip(); } else if (this.layer === 0) { if (this.lt(FC_NN(1, 0, 24))) { - return D(f_gamma(this.sign * this.mag)); + return Decimal.fromNumber(f_gamma(this.sign * this.mag)); } const t = this.mag - 1; @@ -2360,7 +2275,7 @@ export default class Decimal { return Decimal.dOne; } if (this.layer === 0 && this.mag <= 709.7) { - return D(Math.exp(this.sign * this.mag)); + return Decimal.fromNumber(Math.exp(this.sign * this.mag)); } else if (this.layer === 0) { return FC(1, 1, this.sign * Math.log10(Math.E) * this.mag); } else if (this.layer === 1) { @@ -2376,7 +2291,7 @@ export default class Decimal { public sqrt(): Decimal { if (this.layer === 0) { - return D(Math.sqrt(this.sign * this.mag)); + return Decimal.fromNumber(Math.sqrt(this.sign * this.mag)); } else if (this.layer === 1) { return FC(1, 2, Math.log10(this.mag) - 0.3010299956639812); } else { @@ -2422,14 +2337,15 @@ export default class Decimal { if (this_num <= 1.44466786100976613366 && this_num >= 0.06598803584531253708) { //hotfix for the very edge of the number range not being handled properly if (this_num > 1.444667861009099) { - return new Decimal(Math.E); + return Decimal.fromNumber(Math.E); } //Formula for infinite height power tower. const negln = Decimal.ln(this).neg(); return negln.lambertw().div(negln); } else if (this_num > 1.44466786100976613366) { //explodes to infinity - return new Decimal(Number.POSITIVE_INFINITY); + // TODO: replace this with Decimal.dInf + return Decimal.fromNumber(Number.POSITIVE_INFINITY); } else { //0.06598803584531253708 > this_num >= 0: never converges //this_num < 0: quickly becomes a complex number @@ -2444,7 +2360,7 @@ export default class Decimal { if (result > 1) { result = 2 - result; } - return new Decimal(result); + return Decimal.fromNumber(result); } if (height < 0) { @@ -2481,14 +2397,13 @@ export default class Decimal { if (this.gt(10)) { payload = this.pow(fracheight); } else { - payload = D(Decimal.tetrate_critical(this.toNumber(), fracheight)); + payload = Decimal.fromNumber( + Decimal.tetrate_critical(this.toNumber(), fracheight) + ); //TODO: until the critical section grid can handle numbers below 2, scale them to the base //TODO: maybe once the critical section grid has very large bases, this math can be appropriate for them too? I'll think about it if (this.lt(2)) { - payload = payload - .sub(1) - .mul(this.minus(1)) - .plus(1); + payload = payload.sub(1).mul(this.minus(1)).plus(1); } } } else { @@ -2531,7 +2446,7 @@ export default class Decimal { } base = D(base); - let result = D(this); + let result = Decimal.fromDecimal(this); const fulltimes = times; times = Math.trunc(times); const fraction = fulltimes - times; @@ -2567,7 +2482,36 @@ export default class Decimal { //Super-logarithm, one of tetration's inverses, tells you what size power tower you'd have to tetrate base to to get number. By definition, will never be higher than 1.8e308 in break_eternity.js, since a power tower 1.8e308 numbers tall is the largest representable number. // https://en.wikipedia.org/wiki/Super-logarithm - public slog(base: DecimalSource = 10): Decimal { + // NEW: Accept a number of iterations, and use binary search to, after making an initial guess, hone in on the true value, assuming tetration as the ground truth. + public slog(base: DecimalSource = 10, iterations = 100): Decimal { + let step_size = 0.001; + let has_changed_directions_once = false; + let previously_rose = false; + let result = this.slog_internal(base).toNumber(); + for (let i = 1; i < iterations; ++i) { + const new_decimal = new Decimal(base).tetrate(result); + const currently_rose = new_decimal.gt(this); + if (i > 1) { + if (previously_rose != currently_rose) { + has_changed_directions_once = true; + } + } + previously_rose = currently_rose; + if (has_changed_directions_once) { + step_size /= 2; + } else { + step_size *= 2; + } + step_size = Math.abs(step_size) * (currently_rose ? -1 : 1); + result += step_size; + if (step_size === 0) { + break; + } + } + return Decimal.fromNumber(result); + } + + public slog_internal(base: DecimalSource = 10): Decimal { base = D(base); //special cases: @@ -2598,7 +2542,7 @@ export default class Decimal { } let result = 0; - let copy = D(this); + let copy = Decimal.fromDecimal(this); if (copy.layer - base.layer > 3) { const layerloss = copy.layer - base.layer - 3; result += layerloss; @@ -2610,13 +2554,15 @@ export default class Decimal { copy = Decimal.pow(base, copy); result -= 1; } else if (copy.lte(Decimal.dOne)) { - return D(result + Decimal.slog_critical(base.toNumber(), copy.toNumber())); + return Decimal.fromNumber( + result + Decimal.slog_critical(base.toNumber(), copy.toNumber()) + ); } else { result += 1; copy = Decimal.log(copy, base); } } - return D(result); + return Decimal.fromNumber(result); } //background info and tables of values for critical functions taken here: https://github.com/Patashu/break_eternity.js/issues/22 @@ -2653,6 +2599,7 @@ export default class Decimal { //basically, if we're between bases, we interpolate each bases' relevant values together //then we interpolate based on what the fractional height is. //accuracy could be improved by doing a non-linear interpolation (maybe), by adding more bases and heights (definitely) but this is AFAIK the best you can get without running some pari.gp or mathematica program to calculate exact values + //however, do note http://myweb.astate.edu/wpaulsen/tetcalc/tetcalc.html can do it for arbitrary heights but not for arbitrary bases (2, e, 10 present) for (let i = 0; i < critical_headers.length; ++i) { if (critical_headers[i] == base) { // exact match @@ -2673,15 +2620,25 @@ export default class Decimal { } } const frac = height - Math.floor(height); - const result = lower * (1 - frac) + upper * frac; - return result; + //improvement - you get more accuracy (especially around 0.9-1.0) by doing log, then frac, then powing the result + //(we could pre-log the lookup table, but then fractional bases would get Weird) + //also, use old linear for slog (values 0 or less in critical section). maybe something else is better but haven't thought about what yet + if (lower <= 0 || upper <= 0) { + return lower * (1 - frac) + upper * frac; + } else { + return Math.pow( + base, + (Math.log(lower) / Math.log(base)) * (1 - frac) + + (Math.log(upper) / Math.log(base)) * frac + ); + } } //Function for adding/removing layers from a Decimal, even fractional layers (e.g. its slog10 representation). //Moved this over to use the same critical section as tetrate/slog. public layeradd10(diff: DecimalSource): Decimal { diff = Decimal.fromValue_noAlloc(diff).toNumber(); - const result = D(this); + const result = Decimal.fromDecimal(this); if (diff >= 1) { //bug fix: if result is very smol (mag < 0, layer > 0) turn it into 0 first if (result.mag < 0 && result.layer > 0) { @@ -2767,9 +2724,9 @@ export default class Decimal { if (this.lt(-0.3678794411710499)) { throw Error("lambertw is unimplemented for results less than -1, sorry!"); } else if (this.mag < 0) { - return D(f_lambertw(this.toNumber())); + return Decimal.fromNumber(f_lambertw(this.toNumber())); } else if (this.layer === 0) { - return D(f_lambertw(this.sign * this.mag)); + return Decimal.fromNumber(f_lambertw(this.sign * this.mag)); } else if (this.layer === 1) { return d_lambertw(this); } else if (this.layer === 2) { @@ -2792,182 +2749,6 @@ export default class Decimal { const lnx = this.ln(); return lnx.div(lnx.lambertw()); } - /* - -Unit tests for tetrate/iteratedexp/iteratedlog/layeradd10/layeradd/slog: -(note: these won't be exactly precise with the new slog implementation, but that's okay) - -for (var i = 0; i < 1000; ++i) -{ - var first = Math.random()*100; - var both = Math.random()*100; - var expected = first+both+1; - var result = new Decimal(10).layeradd10(first).layeradd10(both).slog(); - if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) - { - console.log(first + ", " + both); - } -} - -for (var i = 0; i < 1000; ++i) -{ - var first = Math.random()*100; - var both = Math.random()*100; - first += both; - var expected = first-both+1; - var result = new Decimal(10).layeradd10(first).layeradd10(-both).slog(); - if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) - { - console.log(first + ", " + both); - } -} - -for (var i = 0; i < 1000; ++i) -{ - var first = Math.random()*100; - var both = Math.random()*100; - var base = Math.random()*8+2; - var expected = first+both+1; - var result = new Decimal(base).layeradd(first, base).layeradd(both, base).slog(base); - if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) - { - console.log(first + ", " + both); - } -} - -for (var i = 0; i < 1000; ++i) -{ - var first = Math.random()*100; - var both = Math.random()*100; - var base = Math.random()*8+2; - first += both; - var expected = first-both+1; - var result = new Decimal(base).layeradd(first, base).layeradd(-both, base).slog(base); - if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) - { - console.log(first + ", " + both); - } -} - -for (var i = 0; i < 1000; ++i) -{ -var first = Math.round((Math.random()*30))/10; -var both = Math.round((Math.random()*30))/10; -var tetrateonly = Decimal.tetrate(10, first); -var tetrateandlog = Decimal.tetrate(10, first+both).iteratedlog(10, both); -if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) -{ - console.log(first + ", " + both); -} -} - -for (var i = 0; i < 1000; ++i) -{ -var first = Math.round((Math.random()*30))/10; -var both = Math.round((Math.random()*30))/10; -var base = Math.random()*8+2; -var tetrateonly = Decimal.tetrate(base, first); -var tetrateandlog = Decimal.tetrate(base, first+both).iteratedlog(base, both); -if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) -{ - console.log(first + ", " + both); -} -} - -for (var i = 0; i < 1000; ++i) -{ -var first = Math.round((Math.random()*30))/10; -var both = Math.round((Math.random()*30))/10; -var base = Math.random()*8+2; -var tetrateonly = Decimal.tetrate(base, first, base); -var tetrateandlog = Decimal.tetrate(base, first+both, base).iteratedlog(base, both); -if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) -{ - console.log(first + ", " + both); -} -} - -for (var i = 0; i < 1000; ++i) -{ - var xex = new Decimal(-0.3678794411710499+Math.random()*100); - var x = Decimal.lambertw(xex); - if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x)))) - { - console.log(xex); - } -} - -for (var i = 0; i < 1000; ++i) -{ - var xex = new Decimal(-0.3678794411710499+Math.exp(Math.random()*100)); - var x = Decimal.lambertw(xex); - if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x)))) - { - console.log(xex); - } -} - -for (var i = 0; i < 1000; ++i) -{ - var a = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1); - var b = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1); - if (Math.random() > 0.5) { a = a.recip(); } - if (Math.random() > 0.5) { b = b.recip(); } - var c = a.add(b).toNumber(); - if (Number.isFinite(c) && !Decimal.eq_tolerance(c, a.toNumber()+b.toNumber())) - { - console.log(a + ", " + b); - } -} - -for (var i = 0; i < 100; ++i) -{ - var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); - var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); - if (Math.random() > 0.5) { a = a.recip(); } - if (Math.random() > 0.5) { b = b.recip(); } - var c = a.mul(b).toNumber(); - if (Number.isFinite(c) && Number.isFinite(a.toNumber()) && Number.isFinite(b.toNumber()) && a.toNumber() != 0 && b.toNumber() != 0 && c != 0 && !Decimal.eq_tolerance(c, a.toNumber()*b.toNumber())) - { - console.log("Test 1: " + a + ", " + b); - } - else if (!Decimal.mul(a.recip(), b.recip()).eq_tolerance(Decimal.mul(a, b).recip())) - { - console.log("Test 3: " + a + ", " + b); - } -} - -for (var i = 0; i < 10; ++i) -{ - var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); - var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); - if (Math.random() > 0.5 && a.sign !== 0) { a = a.recip(); } - if (Math.random() > 0.5 && b.sign !== 0) { b = b.recip(); } - var c = a.pow(b); - var d = a.root(b.recip()); - var e = a.pow(b.recip()); - var f = a.root(b); - - if (!c.eq_tolerance(d) && a.sign !== 0 && b.sign !== 0) - { - console.log("Test 1: " + a + ", " + b); - } - if (!e.eq_tolerance(f) && a.sign !== 0 && b.sign !== 0) - { - console.log("Test 2: " + a + ", " + b); - } -} - -for (var i = 0; i < 10; ++i) -{ - var a = Math.round(Math.random()*18-9); - var b = Math.round(Math.random()*100-50); - var c = Math.round(Math.random()*18-9); - var d = Math.round(Math.random()*100-50); - console.log("Decimal.pow(Decimal.fromMantissaExponent(" + a + ", " + b + "), Decimal.fromMantissaExponent(" + c + ", " + d + ")).toString()"); -} - -*/ //Pentation/pentate: The result of tetrating 'height' times in a row. An absurdly strong operator - Decimal.pentate(2, 4.28) and Decimal.pentate(10, 2.37) are already too huge for break_eternity.js! // https://en.wikipedia.org/wiki/Pentation @@ -2981,7 +2762,7 @@ for (var i = 0; i < 10; ++i) if (fracheight !== 0) { if (payload.eq(Decimal.dOne)) { ++height; - payload = new Decimal(fracheight); + payload = Decimal.fromNumber(fracheight); } else { if (this.eq(10)) { payload = payload.layeradd10(fracheight); @@ -3012,7 +2793,7 @@ for (var i = 0; i < 10; ++i) return this; } if (this.layer === 0) { - return D(Math.sin(this.sign * this.mag)); + return Decimal.fromNumber(Math.sin(this.sign * this.mag)); } return FC_NN(0, 0, 0); } @@ -3022,7 +2803,7 @@ for (var i = 0; i < 10; ++i) return Decimal.dOne; } if (this.layer === 0) { - return D(Math.cos(this.sign * this.mag)); + return Decimal.fromNumber(Math.cos(this.sign * this.mag)); } return FC_NN(0, 0, 0); } @@ -3032,7 +2813,7 @@ for (var i = 0; i < 10; ++i) return this; } if (this.layer === 0) { - return D(Math.tan(this.sign * this.mag)); + return Decimal.fromNumber(Math.tan(this.sign * this.mag)); } return FC_NN(0, 0, 0); } @@ -3042,17 +2823,17 @@ for (var i = 0; i < 10; ++i) return this; } if (this.layer === 0) { - return D(Math.asin(this.sign * this.mag)); + return Decimal.fromNumber(Math.asin(this.sign * this.mag)); } return FC_NN(Number.NaN, Number.NaN, Number.NaN); } public acos(): Decimal { if (this.mag < 0) { - return D(Math.acos(this.toNumber())); + return Decimal.fromNumber(Math.acos(this.toNumber())); } if (this.layer === 0) { - return D(Math.acos(this.sign * this.mag)); + return Decimal.fromNumber(Math.acos(this.sign * this.mag)); } return FC_NN(Number.NaN, Number.NaN, Number.NaN); } @@ -3062,21 +2843,17 @@ for (var i = 0; i < 10; ++i) return this; } if (this.layer === 0) { - return D(Math.atan(this.sign * this.mag)); + return Decimal.fromNumber(Math.atan(this.sign * this.mag)); } - return D(Math.atan(this.sign * 1.8e308)); + return Decimal.fromNumber(Math.atan(this.sign * 1.8e308)); } public sinh(): Decimal { - return this.exp() - .sub(this.negate().exp()) - .div(2); + return this.exp().sub(this.negate().exp()).div(2); } public cosh(): Decimal { - return this.exp() - .add(this.negate().exp()) - .div(2); + return this.exp().add(this.negate().exp()).div(2); } public tanh(): Decimal { @@ -3084,23 +2861,11 @@ for (var i = 0; i < 10; ++i) } public asinh(): Decimal { - return Decimal.ln( - this.add( - this.sqr() - .add(1) - .sqrt() - ) - ); + return Decimal.ln(this.add(this.sqr().add(1).sqrt())); } public acosh(): Decimal { - return Decimal.ln( - this.add( - this.sqr() - .sub(1) - .sqrt() - ) - ); + return Decimal.ln(this.add(this.sqr().sub(1).sqrt())); } public atanh(): Decimal { @@ -3108,7 +2873,7 @@ for (var i = 0; i < 10; ++i) return FC_NN(Number.NaN, Number.NaN, Number.NaN); } - return Decimal.ln(this.add(1).div(D(1).sub(this))).div(2); + return Decimal.ln(this.add(1).div(Decimal.fromNumber(1).sub(this))).div(2); } /** @@ -3149,3 +2914,13 @@ for (var i = 0; i < 10; ++i) } // return Decimal; + +// Optimise Decimal aliases. +// We can't do this optimisation before Decimal is assigned. +D = Decimal.fromValue_noAlloc; +FC = Decimal.fromComponents; +FC_NN = Decimal.fromComponents_noNormalize; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +ME = Decimal.fromMantissaExponent; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +ME_NN = Decimal.fromMantissaExponent_noNormalize; diff --git a/src/lib/lru-cache.ts b/src/lib/lru-cache.ts new file mode 100644 index 0000000..35363ef --- /dev/null +++ b/src/lib/lru-cache.ts @@ -0,0 +1,139 @@ +/** +* A LRU cache intended for caching pure functions. +*/ +export class LRUCache<K, V> { + private map = new Map<K, ListNode<K, V>>(); + // Invariant: Exactly one of the below is true before and after calling a + // LRUCache method: + // - first and last are both undefined, and map.size() is 0. + // - first and last are the same object, and map.size() is 1. + // - first and last are different objects, and map.size() is greater than 1. + private first: ListNode<K, V> | undefined = undefined; + private last: ListNode<K, V> | undefined = undefined; + maxSize: number; + + /** + * @param maxSize The maximum size for this cache. We recommend setting this + * to be one less than a power of 2, as most hashtables - including V8's + * Object hashtable (https://crsrc.org/c/v8/src/objects/ordered-hash-table.cc) + * - uses powers of two for hashtable sizes. It can't exactly be a power of + * two, as a .set() call could temporarily set the size of the map to be + * maxSize + 1. + */ + constructor(maxSize: number) { + this.maxSize = maxSize; + } + + get size(): number { + return this.map.size; + } + + /** + * Gets the specified key from the cache, or undefined if it is not in the + * cache. + * @param key The key to get. + * @returns The cached value, or undefined if key is not in the cache. + */ + get(key: K): V | undefined { + const node = this.map.get(key); + if (node === undefined) { + return undefined; + } + // It is guaranteed that there is at least one item in the cache. + // Therefore, first and last are guaranteed to be a ListNode... + // but if there is only one item, they might be the same. + + // Update the order of the list to make this node the first node in the + // list. + // This isn't needed if this node is already the first node in the list. + if (node !== this.first) { + // As this node is DIFFERENT from the first node, it is guaranteed that + // there are at least two items in the cache. + // However, this node could possibly be the last item. + if (node === this.last) { + // This node IS the last node. + this.last = node.prev; + // From the invariants, there must be at least two items in the cache, + // so node - which is the original "last node" - must have a defined + // previous node. Therefore, this.last - set above - must be defined + // here. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.last!.next = undefined; + } else { + // This node is somewhere in the middle of the list, so there must be at + // least THREE items in the list, and this node's prev and next must be + // defined here. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node.prev!.next = node.next; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + node.next!.prev = node.prev; + } + node.next = this.first; + // From the invariants, there must be at least two items in the cache, so + // this.first must be a valid ListNode. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.first!.prev = node; + this.first = node; + } + return node.value; + } + + /** + * Sets an entry in the cache. + * + * @param key The key of the entry. + * @param value The value of the entry. + * @throws Error, if the map already contains the key. + */ + set(key: K, value: V): void { + // Ensure that this.maxSize >= 1. + if (this.maxSize < 1) { + return; + } + if (this.map.has(key)) { + throw new Error("Cannot update existing keys in the cache"); + } + const node = new ListNode(key, value); + // Move node to the front of the list. + if (this.first === undefined) { + // If the first is undefined, the last is undefined too. + // Therefore, this cache has no items in it. + this.first = node; + this.last = node; + } else { + // This cache has at least one item in it. + node.next = this.first; + this.first.prev = node; + this.first = node; + } + this.map.set(key, node); + + while (this.map.size > this.maxSize) { + // We are guaranteed that this.maxSize >= 1, + // so this.map.size is guaranteed to be >= 2, + // so this.first and this.last must be different valid ListNodes, + // and this.last.prev must also be a valid ListNode (possibly this.first). + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const last = this.last!; + this.map.delete(last.key); + this.last = last.prev; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.last!.next = undefined; + } + } +} + +/** +* A node in a doubly linked list. +*/ +class ListNode<K, V> { + key: K; + value: V; + next: ListNode<K, V> | undefined = undefined; + prev: ListNode<K, V> | undefined = undefined; + + constructor(key: K, value: V) { + this.key = key; + this.value = value; + } +} \ No newline at end of file From b3b042c27191fd89b8ade1b1b594ed4623fa5891 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Mon, 2 Jan 2023 16:41:50 -0600 Subject: [PATCH 09/56] Add static functions for trig functions in b_e --- src/lib/break_eternity.ts | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts index e9f7099..b2d71be 100644 --- a/src/lib/break_eternity.ts +++ b/src/lib/break_eternity.ts @@ -882,6 +882,54 @@ export default class Decimal { return D(value).pentate(height, payload); } + public static sin(value: DecimalSource): Decimal { + return D(value).sin(); + } + + public static cos(value: DecimalSource): Decimal { + return D(value).cos(); + } + + public static tan(value: DecimalSource): Decimal { + return D(value).tan(); + } + + public static asin(value: DecimalSource): Decimal { + return D(value).asin(); + } + + public static acos(value: DecimalSource): Decimal { + return D(value).acos(); + } + + public static atan(value: DecimalSource): Decimal { + return D(value).atan(); + } + + public static sinh(value: DecimalSource): Decimal { + return D(value).sinh(); + } + + public static cosh(value: DecimalSource): Decimal { + return D(value).cosh(); + } + + public static tanh(value: DecimalSource): Decimal { + return D(value).tanh(); + } + + public static asinh(value: DecimalSource): Decimal { + return D(value).asinh(); + } + + public static acosh(value: DecimalSource): Decimal { + return D(value).acosh(); + } + + public static atanh(value: DecimalSource): Decimal { + return D(value).atanh(); + } + /** * If you're willing to spend 'resourcesAvailable' and want to buy something * with exponentially increasing cost each purchase (start at priceStart, From c8283a7043ef67f3f1dcdba75a6d9e5c0408cfd4 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 3 Jan 2023 20:56:35 -0600 Subject: [PATCH 10/56] Formulas implementation (incomplete) --- package.json | 2 +- src/game/formulas.ts | 1136 +++++++++++++++++++++++++++++++++++ src/lib/break_eternity.ts | 41 +- tests/game/formulas.test.ts | 365 +++++++++++ 4 files changed, 1529 insertions(+), 15 deletions(-) create mode 100644 src/game/formulas.ts create mode 100644 tests/game/formulas.test.ts diff --git a/package.json b/package.json index c680586..4b87061 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.17.1", + "vitest": "^0.26.3", "vue-tsc": "^0.38.1" }, "engines": { diff --git a/src/game/formulas.ts b/src/game/formulas.ts new file mode 100644 index 0000000..e9e96f5 --- /dev/null +++ b/src/game/formulas.ts @@ -0,0 +1,1136 @@ +import Decimal, { DecimalSource } from "util/bignum"; +import { convertComputable, ProcessedComputable } from "util/computed"; +import { unref } from "vue"; + +export type FormulaSource = ProcessedComputable<DecimalSource> | Formula; +export type InvertibleFormulaSource = ProcessedComputable<DecimalSource> | InvertibleFormula; + +export type InvertibleFormula = Formula & { invertible: true }; +export type VariableFormula = InvertibleFormula & { hasVariable: true }; + +function F(value: InvertibleFormulaSource): InvertibleFormula; +function F(value: FormulaSource): Formula; +function F(value: FormulaSource) { + return value instanceof Formula + ? value + : new Formula( + () => new Decimal(unref(value)), + value => new Decimal(value) + ); +} + +function processFormulaSource(value: FormulaSource) { + return value instanceof Formula ? value : convertComputable(value); +} + +function unrefFormulaSource(value: Formula | ProcessedComputable<DecimalSource>) { + return value instanceof Formula ? value.evaluate() : unref(value); +} + +export default class Formula { + public readonly invertible: boolean; + public readonly hasVariable: boolean; + public readonly evaluate: () => Decimal; + public readonly invert: (value: DecimalSource) => Decimal; + + constructor( + evaluate: () => Decimal, + invert?: (value: DecimalSource) => Decimal, + hasVariable = false + ) { + this.invertible = invert != null; + this.hasVariable = hasVariable; + this.evaluate = evaluate; + this.invert = invert ?? (() => Decimal.dNaN); + } + + public static constant(value: InvertibleFormulaSource): InvertibleFormula { + return F(value); + } + + public static variable(value: ProcessedComputable<DecimalSource>): VariableFormula { + return new Formula( + () => new Decimal(unref(value)), + value => new Decimal(value), + true + ) as VariableFormula; + } + + public static abs(value: FormulaSource) { + return F(value).abs(); + } + + public static neg(value: InvertibleFormulaSource): InvertibleFormula; + public static neg(value: FormulaSource): Formula; + public static neg(value: FormulaSource) { + return F(value).neg(); + } + + public static negate(value: InvertibleFormulaSource): InvertibleFormula; + public static negate(value: FormulaSource): Formula; + public static negate(value: FormulaSource) { + return F(value).neg(); + } + + public static negated(value: InvertibleFormulaSource): InvertibleFormula; + public static negated(value: FormulaSource): Formula; + public static negated(value: FormulaSource) { + return F(value).neg(); + } + + public static sign(value: FormulaSource) { + return F(value).sign(); + } + + public static sgn(value: FormulaSource) { + return F(value).sign(); + } + + public static round(value: FormulaSource) { + return F(value).round(); + } + + public static floor(value: FormulaSource) { + return F(value).floor(); + } + + public static ceil(value: FormulaSource) { + return F(value).ceil(); + } + + public static trunc(value: FormulaSource) { + return F(value).trunc(); + } + + public static add( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static add(value: FormulaSource, other: FormulaSource): Formula; + public static add(value: FormulaSource, other: FormulaSource) { + return F(value).add(other); + } + + public static plus( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static plus(value: FormulaSource, other: FormulaSource): Formula; + public static plus(value: FormulaSource, other: FormulaSource) { + return F(value).add(other); + } + + public static sub( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static sub(value: FormulaSource, other: FormulaSource): Formula; + public static sub(value: FormulaSource, other: FormulaSource) { + return F(value).sub(other); + } + + public static subtract( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static subtract(value: FormulaSource, other: FormulaSource): Formula; + public static subtract(value: FormulaSource, other: FormulaSource) { + return F(value).sub(other); + } + + public static minus( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static minus(value: FormulaSource, other: FormulaSource): Formula; + public static minus(value: FormulaSource, other: FormulaSource) { + return F(value).sub(other); + } + + public static mul( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static mul(value: FormulaSource, other: FormulaSource): Formula; + public static mul(value: FormulaSource, other: FormulaSource) { + return F(value).mul(other); + } + + public static multiply( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static multiply(value: FormulaSource, other: FormulaSource): Formula; + public static multiply(value: FormulaSource, other: FormulaSource) { + return F(value).mul(other); + } + + public static times( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static times(value: FormulaSource, other: FormulaSource): Formula; + public static times(value: FormulaSource, other: FormulaSource) { + return F(value).mul(other); + } + + public static div( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static div(value: FormulaSource, other: FormulaSource): Formula; + public static div(value: FormulaSource, other: FormulaSource) { + return F(value).div(other); + } + + public static divide( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static divide(value: FormulaSource, other: FormulaSource): Formula; + public static divide(value: FormulaSource, other: FormulaSource) { + return F(value).div(other); + } + + public static recip(value: InvertibleFormulaSource): InvertibleFormula; + public static recip(value: FormulaSource): Formula; + public static recip(value: FormulaSource) { + return F(value).recip(); + } + + public static reciprocal(value: InvertibleFormulaSource): InvertibleFormula; + public static reciprocal(value: FormulaSource): Formula; + public static reciprocal(value: FormulaSource) { + return F(value).recip(); + } + + public static reciprocate(value: InvertibleFormulaSource): InvertibleFormula; + public static reciprocate(value: FormulaSource): Formula; + public static reciprocate(value: FormulaSource) { + return F(value).reciprocate(); + } + + public static max(value: FormulaSource, other: FormulaSource) { + return F(value).max(other); + } + + public static min(value: FormulaSource, other: FormulaSource) { + return F(value).min(other); + } + + public static minabs(value: FormulaSource, other: FormulaSource) { + return F(value).minabs(other); + } + + public static maxabs(value: FormulaSource, other: FormulaSource) { + return F(value).maxabs(other); + } + + public static clamp(value: FormulaSource, min: FormulaSource, max: FormulaSource) { + return F(value).clamp(min, max); + } + + public static clampMin(value: FormulaSource, min: FormulaSource) { + return F(value).clampMin(min); + } + + public static clampMax(value: FormulaSource, max: FormulaSource) { + return F(value).clampMax(max); + } + + public static pLog10(value: InvertibleFormulaSource): InvertibleFormula; + public static pLog10(value: FormulaSource): Formula; + public static pLog10(value: FormulaSource) { + return F(value).pLog10(); + } + + public static absLog10(value: InvertibleFormulaSource): InvertibleFormula; + public static absLog10(value: FormulaSource): Formula; + public static absLog10(value: FormulaSource) { + return F(value).absLog10(); + } + + public static log10(value: InvertibleFormulaSource): InvertibleFormula; + public static log10(value: FormulaSource): Formula; + public static log10(value: FormulaSource) { + return F(value).log10(); + } + + public static log( + value: InvertibleFormulaSource, + base: InvertibleFormulaSource + ): InvertibleFormula; + public static log(value: FormulaSource, base: FormulaSource): Formula; + public static log(value: FormulaSource, base: FormulaSource) { + return F(value).log(base); + } + + public static logarithm( + value: InvertibleFormulaSource, + base: InvertibleFormulaSource + ): InvertibleFormula; + public static logarithm(value: FormulaSource, base: FormulaSource): Formula; + public static logarithm(value: FormulaSource, base: FormulaSource) { + return F(value).log(base); + } + + public static log2(value: InvertibleFormulaSource): InvertibleFormula; + public static log2(value: FormulaSource): Formula; + public static log2(value: FormulaSource) { + return F(value).log2(); + } + + public static ln(value: InvertibleFormulaSource): InvertibleFormula; + public static ln(value: FormulaSource): Formula; + public static ln(value: FormulaSource) { + return F(value).ln(); + } + + public static pow( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static pow(value: FormulaSource, other: FormulaSource): Formula; + public static pow(value: FormulaSource, other: FormulaSource) { + return F(value).pow(other); + } + + public static pow10(value: InvertibleFormulaSource): InvertibleFormula; + public static pow10(value: FormulaSource): Formula; + public static pow10(value: FormulaSource) { + return F(value).pow10(); + } + + public static pow_base( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static pow_base(value: FormulaSource, other: FormulaSource): Formula; + public static pow_base(value: FormulaSource, other: FormulaSource) { + return F(value).pow_base(other); + } + + public static root( + value: InvertibleFormulaSource, + other: InvertibleFormulaSource + ): InvertibleFormula; + public static root(value: FormulaSource, other: FormulaSource): Formula; + public static root(value: FormulaSource, other: FormulaSource) { + return F(value).root(other); + } + + public static factorial(value: FormulaSource) { + return F(value).factorial(); + } + + public static gamma(value: FormulaSource) { + return F(value).gamma(); + } + + public static lngamma(value: FormulaSource) { + return F(value).lngamma(); + } + + public static exp(value: InvertibleFormulaSource): InvertibleFormula; + public static exp(value: FormulaSource): Formula; + public static exp(value: FormulaSource) { + return F(value).exp(); + } + + public static sqr(value: InvertibleFormulaSource): InvertibleFormula; + public static sqr(value: FormulaSource): Formula; + public static sqr(value: FormulaSource) { + return F(value).sqr(); + } + + public static sqrt(value: InvertibleFormulaSource): InvertibleFormula; + public static sqrt(value: FormulaSource): Formula; + public static sqrt(value: FormulaSource) { + return F(value).sqrt(); + } + + public static cube(value: InvertibleFormulaSource): InvertibleFormula; + public static cube(value: FormulaSource): Formula; + public static cube(value: FormulaSource) { + return F(value).cube(); + } + + public static cbrt(value: InvertibleFormulaSource): InvertibleFormula; + public static cbrt(value: FormulaSource): Formula; + public static cbrt(value: FormulaSource) { + return F(value).cbrt(); + } + + public static tetrate( + value: InvertibleFormulaSource, + height?: InvertibleFormulaSource, + payload?: InvertibleFormulaSource + ): InvertibleFormula; + public static tetrate( + value: FormulaSource, + height?: FormulaSource, + payload?: FormulaSource + ): Formula; + public static tetrate( + value: FormulaSource, + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + return F(value).tetrate(height, payload); + } + + public static iteratedexp( + value: InvertibleFormulaSource, + height?: InvertibleFormulaSource, + payload?: InvertibleFormulaSource + ): InvertibleFormula; + public static iteratedexp( + value: FormulaSource, + height?: FormulaSource, + payload?: FormulaSource + ): Formula; + public static iteratedexp( + value: FormulaSource, + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + return F(value).iteratedexp(height, payload); + } + + public static iteratedlog( + value: FormulaSource, + base: FormulaSource = 10, + times: FormulaSource = 1 + ) { + return F(value).iteratedlog(base, times); + } + + public static slog( + value: InvertibleFormulaSource, + base?: InvertibleFormulaSource + ): InvertibleFormula; + public static slog(value: FormulaSource, base?: FormulaSource): Formula; + public static slog(value: FormulaSource, base: FormulaSource = 10) { + return F(value).slog(base); + } + + public static layeradd10(value: FormulaSource, diff: FormulaSource) { + return F(value).layeradd10(diff); + } + + public static layeradd( + value: InvertibleFormulaSource, + diff: InvertibleFormulaSource, + base?: InvertibleFormulaSource + ): InvertibleFormula; + public static layeradd( + value: FormulaSource, + diff: FormulaSource, + base?: FormulaSource + ): Formula; + public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { + return F(value).layeradd(diff, base); + } + + public static lambertw(value: InvertibleFormulaSource): InvertibleFormula; + public static lambertw(value: FormulaSource): Formula; + public static lambertw(value: FormulaSource) { + return F(value).lambertw(); + } + + public static ssqrt(value: InvertibleFormulaSource): InvertibleFormula; + public static ssqrt(value: FormulaSource): Formula; + public static ssqrt(value: FormulaSource) { + return F(value).ssqrt(); + } + + public static pentate( + value: FormulaSource, + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + return F(value).pentate(height, payload); + } + + public static sin(value: InvertibleFormulaSource): InvertibleFormula; + public static sin(value: FormulaSource): Formula; + public static sin(value: FormulaSource) { + return F(value).sin(); + } + + public static cos(value: InvertibleFormulaSource): InvertibleFormula; + public static cos(value: FormulaSource): Formula; + public static cos(value: FormulaSource) { + return F(value).cos(); + } + + public static tan(value: InvertibleFormulaSource): InvertibleFormula; + public static tan(value: FormulaSource): Formula; + public static tan(value: FormulaSource) { + return F(value).tan(); + } + + public static asin(value: InvertibleFormulaSource): InvertibleFormula; + public static asin(value: FormulaSource): Formula; + public static asin(value: FormulaSource) { + return F(value).asin(); + } + + public static acos(value: InvertibleFormulaSource): InvertibleFormula; + public static acos(value: FormulaSource): Formula; + public static acos(value: FormulaSource) { + return F(value).acos(); + } + + public static atan(value: InvertibleFormulaSource): InvertibleFormula; + public static atan(value: FormulaSource): Formula; + public static atan(value: FormulaSource) { + return F(value).atan(); + } + + public static sinh(value: InvertibleFormulaSource): InvertibleFormula; + public static sinh(value: FormulaSource): Formula; + public static sinh(value: FormulaSource) { + return F(value).sinh(); + } + + public static cosh(value: InvertibleFormulaSource): InvertibleFormula; + public static cosh(value: FormulaSource): Formula; + public static cosh(value: FormulaSource) { + return F(value).cosh(); + } + + public static tanh(value: InvertibleFormulaSource): InvertibleFormula; + public static tanh(value: FormulaSource): Formula; + public static tanh(value: FormulaSource) { + return F(value).tanh(); + } + + public static asinh(value: InvertibleFormulaSource): InvertibleFormula; + public static asinh(value: FormulaSource): Formula; + public static asinh(value: FormulaSource) { + return F(value).asinh(); + } + + public static acosh(value: InvertibleFormulaSource): InvertibleFormula; + public static acosh(value: FormulaSource): Formula; + public static acosh(value: FormulaSource) { + return F(value).acosh(); + } + + public static atanh(value: InvertibleFormulaSource): InvertibleFormula; + public static atanh(value: FormulaSource): Formula; + public static atanh(value: FormulaSource) { + return F(value).atanh(); + } + + public abs() { + return new Formula(() => this.evaluate().abs()); + } + + public neg(this: InvertibleFormula): InvertibleFormula; + public neg(this: Formula): Formula; + public neg() { + return new Formula( + () => this.evaluate().neg(), + value => Decimal.neg(value) + ); + } + + public negate(this: InvertibleFormula): InvertibleFormula; + public negate(this: Formula): Formula; + public negate() { + return this.neg(); + } + + public negated(this: InvertibleFormula): InvertibleFormula; + public negated(this: Formula): Formula; + public negated() { + return this.neg(); + } + + public sign() { + return new Formula(() => new Decimal(this.evaluate().sign)); + } + + public sgn() { + return this.sign(); + } + + public round() { + return new Formula(() => this.evaluate().round()); + } + + public floor() { + return new Formula(() => this.evaluate().floor()); + } + + public ceil() { + return new Formula(() => this.evaluate().ceil()); + } + + public trunc() { + return new Formula(() => this.evaluate().trunc()); + } + + public add(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public add(this: Formula, value: FormulaSource): Formula; + public add(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().add(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.sub(value, unrefFormulaSource(v)) + ); + } + + public plus(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public plus(this: Formula, value: FormulaSource): Formula; + public plus(value: FormulaSource) { + return this.add(value); + } + + public sub(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public sub(this: Formula, value: FormulaSource): Formula; + public sub(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().sub(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.add(value, unrefFormulaSource(v)) + ); + } + + public subtract(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public subtract(this: Formula, value: FormulaSource): Formula; + public subtract(value: FormulaSource) { + return this.sub(value); + } + + public minus(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public minus(this: Formula, value: FormulaSource): Formula; + public minus(value: FormulaSource) { + return this.sub(value); + } + + public mul(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public mul(this: Formula, value: FormulaSource): Formula; + public mul(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().mul(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.div(value, unrefFormulaSource(v)) + ); + } + + public multiply(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public multiply(this: Formula, value: FormulaSource): Formula; + public multiply(value: FormulaSource) { + return this.mul(value); + } + + public times(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public times(this: Formula, value: FormulaSource): Formula; + public times(value: FormulaSource) { + return this.mul(value); + } + + public div(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public div(this: Formula, value: FormulaSource): Formula; + public div(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().div(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.mul(value, unrefFormulaSource(v)) + ); + } + + public divide(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public divide(this: Formula, value: FormulaSource): Formula; + public divide(value: FormulaSource) { + return this.div(value); + } + + public divideBy(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public divideBy(this: Formula, value: FormulaSource): Formula; + public divideBy(value: FormulaSource) { + return this.div(value); + } + + public dividedBy(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public dividedBy(this: Formula, value: FormulaSource): Formula; + public dividedBy(value: FormulaSource) { + return this.div(value); + } + + public recip(this: InvertibleFormula): InvertibleFormula; + public recip(this: Formula): Formula; + public recip() { + return new Formula( + () => this.evaluate().recip(), + !this.invertible ? undefined : value => Decimal.recip(value) + ); + } + + public reciprocal(this: InvertibleFormula): InvertibleFormula; + public reciprocal(this: Formula): Formula; + public reciprocal() { + return this.recip(); + } + + public reciprocate(this: InvertibleFormula): InvertibleFormula; + public reciprocate(this: Formula): Formula; + public reciprocate() { + return this.recip(); + } + + public max(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().max(unrefFormulaSource(v))); + } + + public min(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().min(unrefFormulaSource(v))); + } + + public maxabs(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().maxabs(unrefFormulaSource(v))); + } + + public minabs(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().minabs(unrefFormulaSource(v))); + } + + public clamp(min: FormulaSource, max: FormulaSource) { + const minValue = processFormulaSource(min); + const maxValue = processFormulaSource(max); + return new Formula(() => + this.evaluate().clamp(unrefFormulaSource(minValue), unrefFormulaSource(maxValue)) + ); + } + + public clampMin(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().clampMin(unrefFormulaSource(v))); + } + + public clampMax(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula(() => this.evaluate().clampMax(unrefFormulaSource(v))); + } + + public pLog10(this: InvertibleFormula): InvertibleFormula; + public pLog10(this: Formula): Formula; + public pLog10() { + return new Formula(() => this.evaluate().pLog10()); + } + + public absLog10(this: InvertibleFormula): InvertibleFormula; + public absLog10(this: Formula): Formula; + public absLog10() { + return new Formula(() => this.evaluate().absLog10()); + } + + public log10(this: InvertibleFormula): InvertibleFormula; + public log10(this: Formula): Formula; + public log10() { + return new Formula( + () => this.evaluate().log10(), + !this.invertible ? undefined : value => Decimal.pow10(value) + ); + } + + public log(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public log(this: Formula, value: FormulaSource): Formula; + public log(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().log(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.pow(unrefFormulaSource(v), value) + ); + } + + public logarithm(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public logarithm(this: Formula, value: FormulaSource): Formula; + public logarithm(value: FormulaSource) { + return this.log(value); + } + + public log2(this: InvertibleFormula): InvertibleFormula; + public log2(this: Formula): Formula; + public log2() { + return new Formula( + () => this.evaluate().log2(), + !this.invertible ? undefined : value => Decimal.pow(2, value) + ); + } + + public ln(this: InvertibleFormula): InvertibleFormula; + public ln(this: Formula): Formula; + public ln() { + return new Formula( + () => this.evaluate().ln(), + !this.invertible ? undefined : value => Decimal.exp(value) + ); + } + + public pow(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public pow(this: Formula, value: FormulaSource): Formula; + public pow(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().pow(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.root(value, unrefFormulaSource(v)) + ); + } + + public pow10(this: InvertibleFormula): InvertibleFormula; + public pow10(this: Formula): Formula; + public pow10() { + return new Formula( + () => this.evaluate().pow10(), + !this.invertible ? undefined : value => Decimal.root(value, 10) + ); + } + + public pow_base(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public pow_base(this: Formula, value: FormulaSource): Formula; + public pow_base(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().pow_base(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.root(unrefFormulaSource(v), value) + ); + } + + public root(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; + public root(this: Formula, value: FormulaSource): Formula; + public root(value: FormulaSource) { + const v = processFormulaSource(value); + return new Formula( + () => this.evaluate().root(unrefFormulaSource(v)), + (v instanceof Formula && !v.invertible) || !this.invertible + ? undefined + : value => Decimal.pow(value, unrefFormulaSource(v)) + ); + } + + public factorial() { + return new Formula(() => this.evaluate().factorial()); + } + + public gamma() { + return new Formula(() => this.evaluate().gamma()); + } + public lngamma() { + return new Formula(() => this.evaluate().lngamma()); + } + + public exp(this: InvertibleFormula): InvertibleFormula; + public exp(this: Formula): Formula; + public exp() { + return new Formula( + () => this.evaluate().exp(), + !this.invertible ? undefined : value => Decimal.ln(value) + ); + } + + public sqr(this: InvertibleFormula): InvertibleFormula; + public sqr(this: Formula): Formula; + public sqr() { + return this.pow(2); + } + + public sqrt(this: InvertibleFormula): InvertibleFormula; + public sqrt(this: Formula): Formula; + public sqrt() { + return this.root(2); + } + + public cube(this: InvertibleFormula): InvertibleFormula; + public cube(this: Formula): Formula; + public cube() { + return this.pow(3); + } + + public cbrt(this: InvertibleFormula): InvertibleFormula; + public cbrt(this: Formula): Formula; + public cbrt() { + return this.pow(1 / 3); + } + + public tetrate( + this: InvertibleFormula, + height?: FormulaSource, + payload?: FormulaSource + ): InvertibleFormula; + public tetrate(this: Formula, height?: FormulaSource, payload?: FormulaSource): Formula; + public tetrate( + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + const heightValue = processFormulaSource(height); + const payloadValue = processFormulaSource(payload); + return new Formula( + () => + this.evaluate().tetrate( + Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), + unrefFormulaSource(payloadValue) + ), + (heightValue instanceof Formula && !heightValue.invertible) || + (payloadValue instanceof Formula && !payloadValue.invertible) || + !this.invertible + ? undefined + : value => + Decimal.slog( + value, + Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() + ) + ); + } + + public iteratedexp( + this: InvertibleFormula, + height?: FormulaSource, + payload?: FormulaSource + ): InvertibleFormula; + public iteratedexp(this: Formula, height?: FormulaSource, payload?: FormulaSource): Formula; + public iteratedexp( + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + const heightValue = processFormulaSource(height); + const payloadValue = processFormulaSource(payload); + return new Formula( + () => + this.evaluate().iteratedexp( + Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), + new Decimal(unrefFormulaSource(payloadValue)) + ), + (heightValue instanceof Formula && !heightValue.invertible) || + (payloadValue instanceof Formula && !payloadValue.invertible) || + !this.invertible + ? undefined + : value => + Decimal.iteratedlog( + value, + Math.E, + Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() + ) + ); + } + + public iteratedlog(base: FormulaSource = 10, times: FormulaSource = 1) { + const baseValue = processFormulaSource(base); + const timesValue = processFormulaSource(times); + return new Formula(() => + this.evaluate().iteratedlog( + unrefFormulaSource(baseValue), + Decimal.min(1e308, unrefFormulaSource(timesValue)).toNumber() + ) + ); + } + + public slog(base: FormulaSource = 10) { + const baseValue = processFormulaSource(base); + return new Formula( + () => + this.evaluate().slog(Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber()), + (baseValue instanceof Formula && !baseValue.invertible) || !this.invertible + ? undefined + : value => + Decimal.tetrate( + value, + Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber() + ) + ); + } + + public layeradd10(diff: FormulaSource) { + const diffValue = processFormulaSource(diff); + return new Formula(() => this.evaluate().layeradd10(unrefFormulaSource(diffValue))); + } + + public layeradd( + this: InvertibleFormula, + diff: FormulaSource, + base: FormulaSource + ): InvertibleFormula; + public layeradd(this: Formula, diff: FormulaSource, base: FormulaSource): Formula; + public layeradd(diff: FormulaSource, base: FormulaSource) { + const diffValue = processFormulaSource(diff); + const baseValue = processFormulaSource(base); + return new Formula( + () => + this.evaluate().layeradd( + Decimal.min(1e308, unrefFormulaSource(diffValue)).toNumber(), + unrefFormulaSource(baseValue) + ), + (diffValue instanceof Formula && !diffValue.invertible) || + (diffValue instanceof Formula && !diffValue.invertible) || + !this.invertible + ? undefined + : value => + Decimal.layeradd( + value, + Decimal.min(1e308, unrefFormulaSource(diffValue)).negate().toNumber(), + unrefFormulaSource(baseValue) + ) + ); + } + + public lambertw(this: InvertibleFormula): InvertibleFormula; + public lambertw(this: Formula): Formula; + public lambertw() { + return new Formula( + () => this.evaluate().lambertw(), + !this.invertible ? undefined : value => Decimal.pow(Math.E, value).times(value) + ); + } + + public ssqrt(this: InvertibleFormula): InvertibleFormula; + public ssqrt(this: Formula): Formula; + public ssqrt() { + return new Formula( + () => this.evaluate().ssqrt(), + !this.invertible ? undefined : value => Decimal.tetrate(value, 2) + ); + } + + public pentate( + height: FormulaSource = 2, + payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) + ) { + const heightValue = processFormulaSource(height); + const payloadValue = processFormulaSource(payload); + return new Formula(() => + this.evaluate().pentate( + Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), + unrefFormulaSource(payloadValue) + ) + ); + } + + public sin(this: InvertibleFormula): InvertibleFormula; + public sin(this: Formula): Formula; + public sin() { + return new Formula( + () => this.evaluate().sin(), + !this.invertible ? undefined : value => Decimal.asin(value) + ); + } + + public cos(this: InvertibleFormula): InvertibleFormula; + public cos(this: Formula): Formula; + public cos() { + return new Formula( + () => this.evaluate().cos(), + !this.invertible ? undefined : value => Decimal.acos(value) + ); + } + + public tan(this: InvertibleFormula): InvertibleFormula; + public tan(this: Formula): Formula; + public tan() { + return new Formula( + () => this.evaluate().tan(), + !this.invertible ? undefined : value => Decimal.atan(value) + ); + } + + public asin(this: InvertibleFormula): InvertibleFormula; + public asin(this: Formula): Formula; + public asin() { + return new Formula( + () => this.evaluate().asin(), + !this.invertible ? undefined : value => Decimal.sin(value) + ); + } + + public acos(this: InvertibleFormula): InvertibleFormula; + public acos(this: Formula): Formula; + public acos() { + return new Formula( + () => this.evaluate().acos(), + !this.invertible ? undefined : value => Decimal.cos(value) + ); + } + + public atan(this: InvertibleFormula): InvertibleFormula; + public atan(this: Formula): Formula; + public atan() { + return new Formula( + () => this.evaluate().atan(), + !this.invertible ? undefined : value => Decimal.tan(value) + ); + } + + public sinh(this: InvertibleFormula): InvertibleFormula; + public sinh(this: Formula): Formula; + public sinh() { + return new Formula( + () => this.evaluate().sinh(), + !this.invertible ? undefined : value => Decimal.asinh(value) + ); + } + + public cosh(this: InvertibleFormula): InvertibleFormula; + public cosh(this: Formula): Formula; + public cosh() { + return new Formula( + () => this.evaluate().cosh(), + !this.invertible ? undefined : value => Decimal.acosh(value) + ); + } + + public tanh(this: InvertibleFormula): InvertibleFormula; + public tanh(this: Formula): Formula; + public tanh() { + return new Formula( + () => this.evaluate().tanh(), + !this.invertible ? undefined : value => Decimal.atanh(value) + ); + } + + public asinh(this: InvertibleFormula): InvertibleFormula; + public asinh(this: Formula): Formula; + public asinh() { + return new Formula( + () => this.evaluate().asinh(), + !this.invertible ? undefined : value => Decimal.sinh(value) + ); + } + + public acosh(this: InvertibleFormula): InvertibleFormula; + public acosh(this: Formula): Formula; + public acosh() { + return new Formula( + () => this.evaluate().acosh(), + !this.invertible ? undefined : value => Decimal.cosh(value) + ); + } + + public atanh(this: InvertibleFormula): InvertibleFormula; + public atanh(this: Formula): Formula; + public atanh() { + return new Formula( + () => this.evaluate().atanh(), + !this.invertible ? undefined : value => Decimal.tanh(value) + ); + } +} diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts index b2d71be..a1d957c 100644 --- a/src/lib/break_eternity.ts +++ b/src/lib/break_eternity.ts @@ -344,16 +344,16 @@ export type DecimalSource = Decimal | number | string; * The Decimal's value is simply mantissa * 10^exponent. */ export default class Decimal { - public static readonly dZero = FC_NN(0, 0, 0); - public static readonly dOne = FC_NN(1, 0, 1); - public static readonly dNegOne = FC_NN(-1, 0, 1); - public static readonly dTwo = FC_NN(1, 0, 2); - public static readonly dTen = FC_NN(1, 0, 10); - public static readonly dNaN = FC_NN(Number.NaN, Number.NaN, Number.NaN); - public static readonly dInf = FC_NN(1, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); - public static readonly dNegInf = FC_NN(-1, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); - public static readonly dNumberMax = FC(1, 0, Number.MAX_VALUE); - public static readonly dNumberMin = FC(1, 0, Number.MIN_VALUE); + public static dZero: Decimal; + public static dOne: Decimal; + public static dNegOne: Decimal; + public static dTwo: Decimal; + public static dTen: Decimal; + public static dNaN: Decimal; + public static dInf: Decimal; + public static dNegInf: Decimal; + public static dNumberMax: Decimal; + public static dNumberMin: Decimal; private static fromStringCache = new LRUCache<string, Decimal>(DEFAULT_FROM_STRING_CACHE_SIZE); @@ -705,7 +705,7 @@ export default class Decimal { public static eq_tolerance( value: DecimalSource, other: DecimalSource, - tolerance: number + tolerance?: number ): boolean { return D(value).eq_tolerance(other, tolerance); } @@ -713,7 +713,7 @@ export default class Decimal { public static equals_tolerance( value: DecimalSource, other: DecimalSource, - tolerance: number + tolerance?: number ): boolean { return D(value).eq_tolerance(other, tolerance); } @@ -858,7 +858,7 @@ export default class Decimal { return D(value).layeradd10(diff); } - public static layeradd(value: DecimalSource, diff: number, base = 10): Decimal { + public static layeradd(value: DecimalSource, diff: number, base: DecimalSource = 10): Decimal { return D(value).layeradd(diff, base); } @@ -2037,7 +2037,7 @@ export default class Decimal { * For example, if you put in 1e-9, then any number closer to the * larger number than (larger number)*1e-9 will be considered equal. */ - public eq_tolerance(value: DecimalSource, tolerance: number): boolean { + public eq_tolerance(value: DecimalSource, tolerance?: number): boolean { const decimal = D(value); // https://stackoverflow.com/a/33024979 if (tolerance == null) { tolerance = 1e-7; @@ -2961,6 +2961,19 @@ export default class Decimal { // return Decimal; } +// Assign these after the Decimal is assigned because vitest had issues otherwise +// If we can figure out why, we can make these readonly properties instead +Decimal.dZero = FC_NN(0, 0, 0); +Decimal.dOne = FC_NN(1, 0, 1); +Decimal.dNegOne = FC_NN(-1, 0, 1); +Decimal.dTwo = FC_NN(1, 0, 2); +Decimal.dTen = FC_NN(1, 0, 10); +Decimal.dNaN = FC_NN(Number.NaN, Number.NaN, Number.NaN); +Decimal.dInf = FC_NN(1, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); +Decimal.dNegInf = FC_NN(-1, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); +Decimal.dNumberMax = FC(1, 0, Number.MAX_VALUE); +Decimal.dNumberMin = FC(1, 0, Number.MIN_VALUE); + // return Decimal; // Optimise Decimal aliases. diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts new file mode 100644 index 0000000..565ec76 --- /dev/null +++ b/tests/game/formulas.test.ts @@ -0,0 +1,365 @@ +import Formula, { InvertibleFormula } from "game/formulas"; +import Decimal, { DecimalSource } from "util/bignum"; +import { beforeAll, describe, expect, test } from "vitest"; +import { ref } from "vue"; + +type FormulaFunctions = keyof Formula & keyof typeof Formula & keyof typeof Decimal; + +interface FixedLengthArray<T, L extends number> extends ArrayLike<T> { + length: L; +} + +function compare_tolerance(value: DecimalSource) { + return (other: DecimalSource) => Decimal.eq_tolerance(value, other); +} + +function testConstant( + desc: string, + formulaFunc: () => InvertibleFormula, + expectedValue: DecimalSource = 10 +) { + describe(desc, () => { + let formula: Formula; + beforeAll(() => { + formula = formulaFunc(); + }); + test("evaluates correctly", () => expect(formula.evaluate()).toEqual(expectedValue)); + test("inverts correctly", () => expect(formula.invert(10)).toEqual(expectedValue)); + test("is invertible", () => expect(formula.invertible).toBe(true)); + test("is not marked as having a variable", () => expect(formula.hasVariable).toBe(false)); + }); +} + +// Utility function that will test all the different +// It's a lot of tests, but I'd rather be exhaustive +function testFormula<T extends FormulaFunctions>( + functionNames: readonly T[], + args: Readonly<FixedLengthArray<number, Parameters<typeof Formula[T]>["length"]>>, + invertible = true +) { + let value: Decimal; + + beforeAll(() => { + value = testValueFormulas[args[0]].evaluate(); + }); + + functionNames.forEach(name => { + let testName = name + "("; + for (let i = 0; i < args.length; i++) { + if (i !== 0) { + testName += ", "; + } + testName += testValues[args[i]]; + } + testName += ")"; + describe(testName, () => { + let expectedEvaluation: Decimal | undefined; + let formulaArgs: Formula[]; + let staticFormula: Formula; + let instanceFormula: Formula; + beforeAll(() => { + for (let i = 0; i < args.length; i++) { + formulaArgs.push(testValueFormulas[args[i]]); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + staticFormula = Formula[name](...formulaArgs); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + instanceFormula = formulaArgs[0][name](...formulaArgs.slice(1)); + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expectedEvaluation = Decimal[name](...args); + } catch { + // If this is an invalid Decimal operation, then ignore this test case + return; + } + }); + + test("Static formula is not marked as having a variable", () => + expect(staticFormula.hasVariable).toBe(false)); + test("Static function evaluates correctly", () => + expectedEvaluation != null && + expect(staticFormula.evaluate()).toSatisfy(compare_tolerance(expectedEvaluation))); + test("Static function invertible", () => + expect(staticFormula.invertible).toBe(invertible)); + if (invertible) { + test("Static function inverts correctly", () => + expectedEvaluation != null && + !Decimal.isNaN(expectedEvaluation) && + expect(staticFormula.invert(expectedEvaluation)).toSatisfy( + compare_tolerance(value) + )); + } + + // Do those tests again but for non-static methods + test("Instance formula is not marked as having a variable", () => + expect(instanceFormula.hasVariable).toBe(false)); + test("Instance function evaluates correctly", () => + expectedEvaluation != null && + expect(instanceFormula.evaluate()).toSatisfy( + compare_tolerance(expectedEvaluation) + )); + test("Instance function invertible", () => + expect(instanceFormula.invertible).toBe(invertible)); + if (invertible) { + test("Instance function inverts correctly", () => + expectedEvaluation != null && + !Decimal.isNaN(expectedEvaluation) && + expect(instanceFormula.invert(expectedEvaluation)).toSatisfy( + compare_tolerance(value) + )); + } + }); + }); +} + +const testValues = [-2.5, -1, -0.1, 0, 0.1, 1, 2.5] as const; +let testValueFormulas: InvertibleFormula[] = []; + +const invertibleZeroParamFunctionNames = [ + ["neg", "negate", "negated"], + ["recip", "reciprocal", "reciprocate"], + ["log10"], + ["log2"], + ["ln"], + ["pow10"], + ["exp"], + ["sqr"], + ["sqrt"], + ["cube"], + ["cbrt"], + ["lambertw"], + ["ssqrt"], + ["sin"], + ["cos"], + ["tan"], + ["asin"], + ["acos"], + ["atan"], + ["sinh"], + ["cosh"], + ["tanh"], + ["asinh"], + ["acosh"], + ["atanh"] +] as const; + +const nonInvertibleZeroParamFunctionNames = [ + ["abs"], + ["sign", "sgn"], + ["round"], + ["floor"], + ["ceil"], + ["trunc"], + ["pLog10"], + ["absLog10"], + ["factorial"], + ["gamma"], + ["lngamma"] +] as const; + +const invertibleOneParamFunctionNames = [ + ["add", "plus"], + ["sub", "subtract", "minus"], + ["mul", "multiply", "times"], + ["div", "divide"], + ["log", "logarithm"], + ["pow"], + ["root"], + ["slog"] +] as const; + +const nonInvertibleOneParamFunctionNames = [ + ["max"], + ["min"], + ["maxabs"], + ["minabs"], + ["clampMin"], + ["clampMax"], + ["layeradd10"] +] as const; + +const invertibleTwoParamFunctionNames = [["tetrate"]] as const; + +const nonInvertibleTwoParamFunctionNames = [ + ["clamp"], + ["iteratedexp"], + ["iteratedlog"], + ["layeradd"], + ["pentate"] +] as const; + +describe("Creating Formulas", () => { + beforeAll(() => { + testValueFormulas = testValues.map(v => Formula.constant(v)); + }); + + describe("Constants", () => { + testConstant("number", () => Formula.constant(10)); + testConstant("string", () => Formula.constant("10")); + testConstant("formula", () => Formula.constant(Formula.constant(10))); + testConstant("decimal", () => Formula.constant(new Decimal("1e400")), "1e400"); + testConstant("ref", () => Formula.constant(ref(10))); + }); + + describe("Invertible 0-param", () => { + invertibleZeroParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + testFormula(names, [i] as const); + } + }); + }); + describe("Non-Invertible 0-param", () => { + nonInvertibleZeroParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + testFormula(names, [i] as const, false); + } + }); + }); + describe("Invertible 1-param", () => { + invertibleOneParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + for (let j = 0; j < testValues.length; j++) { + testFormula(names, [i, j] as const); + } + } + }); + }); + describe("Non-Invertible 1-param", () => { + nonInvertibleOneParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + for (let j = 0; j < testValues.length; j++) { + testFormula(names, [i, j] as const, false); + } + } + }); + }); + describe("Invertible 2-param", () => { + invertibleTwoParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + for (let j = 0; j < testValues.length; j++) { + for (let k = 0; k < testValues.length; k++) { + testFormula(names, [i, j, k] as const); + } + } + } + }); + }); + describe("Non-Invertible 2-param", () => { + nonInvertibleTwoParamFunctionNames.forEach(names => { + for (let i = 0; i < testValues.length; i++) { + for (let j = 0; j < testValues.length; j++) { + for (let k = 0; k < testValues.length; k++) { + testFormula(names, [i, j, k] as const, false); + } + } + } + }); + }); + + describe("Variables", () => { + let variable: Formula; + let constant: Formula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("Created variable is marked as a variable", () => + expect(variable.hasVariable).toBe(true)); + test("Evaluate() returns variable's value", () => + expect(variable.evaluate()).toSatisfy(compare_tolerance(10))); + test("Invert() is pass-through", () => + expect(variable.invert(100)).toSatisfy(compare_tolerance(100))); + + test("Nested variable is marked as having a variable", () => + expect(variable.add(10).div(3).pow(2).hasVariable).toBe(false)); + test("Nested non-variable is marked as not having a variable", () => + expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false)); + + describe("Invertible Formulas correctly calculate when they contain a variable", () => { + function checkFormula(formula: Formula, expectedBool = true) { + expect(formula.invertible).toBe(expectedBool); + expect(formula.hasVariable).toBe(expectedBool); + } + invertibleZeroParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable))); + }); + }); + invertibleOneParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable), false)); + }); + }); + invertibleTwoParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, constant), false)); + test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant, variable), false)); + test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable, variable), false)); + test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, variable), false)); + }); + }); + }); + + describe("Non-Invertible Formulas never marked as having a variable", () => { + function checkFormula(formula: Formula) { + expect(formula.invertible).toBe(false); + expect(formula.hasVariable).toBe(false); + } + nonInvertibleZeroParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonInvertibleOneParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonInvertibleTwoParamFunctionNames.flat().forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + }); +}); From 5f3dd1162d7b77c314bc38e1634fabd52c031778 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Fri, 6 Jan 2023 00:38:11 -0600 Subject: [PATCH 11/56] Support variable anywhere in formula --- src/game/formulas.ts | 209 +++++++++++++++++++++++++----------- tests/game/formulas.test.ts | 45 +++++++- 2 files changed, 189 insertions(+), 65 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index e9e96f5..e54ca2e 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -27,6 +27,22 @@ function unrefFormulaSource(value: Formula | ProcessedComputable<DecimalSource>) return value instanceof Formula ? value.evaluate() : unref(value); } +function isVariableFormula(value: FormulaSource): value is VariableFormula { + return value instanceof Formula && value.hasVariable; +} + +function calculateInvertibility(...inputs: FormulaSource[]) { + const invertible = !inputs.some(input => input instanceof Formula && !input.invertible); + const hasVariable = + invertible && + inputs.filter(input => input instanceof Formula && input.invertible && input.hasVariable) + .length === 1; + return { + invertible, + hasVariable + }; +} + export default class Formula { public readonly invertible: boolean; public readonly hasVariable: boolean; @@ -533,7 +549,8 @@ export default class Formula { public neg() { return new Formula( () => this.evaluate().neg(), - value => Decimal.neg(value) + value => Decimal.neg(value), + this.hasVariable ); } @@ -577,11 +594,17 @@ export default class Formula { public add(this: Formula, value: FormulaSource): Formula; public add(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().add(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.sub(value, unrefFormulaSource(v)) + invertible + ? value => + Decimal.sub( + value, + isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() + ) + : undefined, + hasVariable ); } @@ -595,11 +618,17 @@ export default class Formula { public sub(this: Formula, value: FormulaSource): Formula; public sub(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().sub(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.add(value, unrefFormulaSource(v)) + invertible + ? value => + Decimal.add( + value, + isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() + ) + : undefined, + hasVariable ); } @@ -619,11 +648,17 @@ export default class Formula { public mul(this: Formula, value: FormulaSource): Formula; public mul(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().mul(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.div(value, unrefFormulaSource(v)) + invertible + ? value => + Decimal.div( + value, + isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() + ) + : undefined, + hasVariable ); } @@ -643,11 +678,17 @@ export default class Formula { public div(this: Formula, value: FormulaSource): Formula; public div(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().div(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.mul(value, unrefFormulaSource(v)) + invertible + ? value => + Decimal.mul( + value, + isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() + ) + : undefined, + hasVariable ); } @@ -674,7 +715,8 @@ export default class Formula { public recip() { return new Formula( () => this.evaluate().recip(), - !this.invertible ? undefined : value => Decimal.recip(value) + this.invertible ? value => Decimal.recip(value) : undefined, + this.hasVariable ); } @@ -745,7 +787,8 @@ export default class Formula { public log10() { return new Formula( () => this.evaluate().log10(), - !this.invertible ? undefined : value => Decimal.pow10(value) + this.invertible ? value => Decimal.pow10(value) : undefined, + this.hasVariable ); } @@ -753,11 +796,16 @@ export default class Formula { public log(this: Formula, value: FormulaSource): Formula; public log(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().log(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.pow(unrefFormulaSource(v), value) + invertible + ? value => + isVariableFormula(this) + ? Decimal.pow(unrefFormulaSource(v), value) + : Decimal.root(this.evaluate(), value) + : undefined, + hasVariable ); } @@ -772,7 +820,8 @@ export default class Formula { public log2() { return new Formula( () => this.evaluate().log2(), - !this.invertible ? undefined : value => Decimal.pow(2, value) + this.invertible ? value => Decimal.pow(2, value) : undefined, + this.hasVariable ); } @@ -781,7 +830,8 @@ export default class Formula { public ln() { return new Formula( () => this.evaluate().ln(), - !this.invertible ? undefined : value => Decimal.exp(value) + this.invertible ? value => Decimal.exp(value) : undefined, + this.hasVariable ); } @@ -789,11 +839,16 @@ export default class Formula { public pow(this: Formula, value: FormulaSource): Formula; public pow(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().pow(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.root(value, unrefFormulaSource(v)) + invertible + ? value => + isVariableFormula(this) + ? Decimal.root(value, unrefFormulaSource(v)) + : Decimal.ln(value).div(Decimal.ln(this.evaluate())) + : undefined, + hasVariable ); } @@ -802,7 +857,8 @@ export default class Formula { public pow10() { return new Formula( () => this.evaluate().pow10(), - !this.invertible ? undefined : value => Decimal.root(value, 10) + this.invertible ? value => Decimal.root(value, 10) : undefined, + this.hasVariable ); } @@ -810,11 +866,16 @@ export default class Formula { public pow_base(this: Formula, value: FormulaSource): Formula; public pow_base(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().pow_base(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.root(unrefFormulaSource(v), value) + invertible + ? value => + isVariableFormula(this) + ? Decimal.ln(value).div(unrefFormulaSource(v)) + : Decimal.root(unrefFormulaSource(v), value) + : undefined, + hasVariable ); } @@ -822,11 +883,16 @@ export default class Formula { public root(this: Formula, value: FormulaSource): Formula; public root(value: FormulaSource) { const v = processFormulaSource(value); + const { invertible, hasVariable } = calculateInvertibility(this, value); return new Formula( () => this.evaluate().root(unrefFormulaSource(v)), - (v instanceof Formula && !v.invertible) || !this.invertible - ? undefined - : value => Decimal.pow(value, unrefFormulaSource(v)) + invertible + ? value => + isVariableFormula(this) + ? Decimal.root(value, Decimal.recip(unrefFormulaSource(v))) + : Decimal.ln(value).div(Decimal.ln(this.evaluate()).recip()) + : undefined, + hasVariable ); } @@ -846,7 +912,8 @@ export default class Formula { public exp() { return new Formula( () => this.evaluate().exp(), - !this.invertible ? undefined : value => Decimal.ln(value) + this.invertible ? value => Decimal.ln(value) : undefined, + this.hasVariable ); } @@ -886,21 +953,21 @@ export default class Formula { ) { const heightValue = processFormulaSource(height); const payloadValue = processFormulaSource(payload); + const { invertible, hasVariable } = calculateInvertibility(this, height, payload); return new Formula( () => this.evaluate().tetrate( Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), unrefFormulaSource(payloadValue) ), - (heightValue instanceof Formula && !heightValue.invertible) || - (payloadValue instanceof Formula && !payloadValue.invertible) || - !this.invertible - ? undefined - : value => + invertible + ? value => Decimal.slog( value, Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() ) + : undefined, + hasVariable ); } @@ -916,22 +983,22 @@ export default class Formula { ) { const heightValue = processFormulaSource(height); const payloadValue = processFormulaSource(payload); + const { invertible, hasVariable } = calculateInvertibility(this, height, payload); return new Formula( () => this.evaluate().iteratedexp( Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), new Decimal(unrefFormulaSource(payloadValue)) ), - (heightValue instanceof Formula && !heightValue.invertible) || - (payloadValue instanceof Formula && !payloadValue.invertible) || - !this.invertible - ? undefined - : value => + invertible + ? value => Decimal.iteratedlog( value, Math.E, Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() ) + : undefined, + hasVariable ); } @@ -948,16 +1015,18 @@ export default class Formula { public slog(base: FormulaSource = 10) { const baseValue = processFormulaSource(base); + const { invertible, hasVariable } = calculateInvertibility(this, base); return new Formula( () => this.evaluate().slog(Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber()), - (baseValue instanceof Formula && !baseValue.invertible) || !this.invertible - ? undefined - : value => + invertible + ? value => Decimal.tetrate( value, Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber() ) + : undefined, + hasVariable ); } @@ -975,22 +1044,22 @@ export default class Formula { public layeradd(diff: FormulaSource, base: FormulaSource) { const diffValue = processFormulaSource(diff); const baseValue = processFormulaSource(base); + const { invertible, hasVariable } = calculateInvertibility(this, diff, base); return new Formula( () => this.evaluate().layeradd( Decimal.min(1e308, unrefFormulaSource(diffValue)).toNumber(), unrefFormulaSource(baseValue) ), - (diffValue instanceof Formula && !diffValue.invertible) || - (diffValue instanceof Formula && !diffValue.invertible) || - !this.invertible - ? undefined - : value => + invertible + ? value => Decimal.layeradd( value, Decimal.min(1e308, unrefFormulaSource(diffValue)).negate().toNumber(), unrefFormulaSource(baseValue) ) + : undefined, + hasVariable ); } @@ -999,7 +1068,8 @@ export default class Formula { public lambertw() { return new Formula( () => this.evaluate().lambertw(), - !this.invertible ? undefined : value => Decimal.pow(Math.E, value).times(value) + this.invertible ? value => Decimal.pow(Math.E, value).times(value) : undefined, + this.hasVariable ); } @@ -1008,7 +1078,8 @@ export default class Formula { public ssqrt() { return new Formula( () => this.evaluate().ssqrt(), - !this.invertible ? undefined : value => Decimal.tetrate(value, 2) + this.invertible ? value => Decimal.tetrate(value, 2) : undefined, + this.hasVariable ); } @@ -1031,7 +1102,8 @@ export default class Formula { public sin() { return new Formula( () => this.evaluate().sin(), - !this.invertible ? undefined : value => Decimal.asin(value) + this.invertible ? value => Decimal.asin(value) : undefined, + this.hasVariable ); } @@ -1040,7 +1112,8 @@ export default class Formula { public cos() { return new Formula( () => this.evaluate().cos(), - !this.invertible ? undefined : value => Decimal.acos(value) + this.invertible ? value => Decimal.acos(value) : undefined, + this.hasVariable ); } @@ -1049,7 +1122,8 @@ export default class Formula { public tan() { return new Formula( () => this.evaluate().tan(), - !this.invertible ? undefined : value => Decimal.atan(value) + this.invertible ? value => Decimal.atan(value) : undefined, + this.hasVariable ); } @@ -1058,7 +1132,8 @@ export default class Formula { public asin() { return new Formula( () => this.evaluate().asin(), - !this.invertible ? undefined : value => Decimal.sin(value) + this.invertible ? value => Decimal.sin(value) : undefined, + this.hasVariable ); } @@ -1067,7 +1142,8 @@ export default class Formula { public acos() { return new Formula( () => this.evaluate().acos(), - !this.invertible ? undefined : value => Decimal.cos(value) + this.invertible ? value => Decimal.cos(value) : undefined, + this.hasVariable ); } @@ -1076,7 +1152,8 @@ export default class Formula { public atan() { return new Formula( () => this.evaluate().atan(), - !this.invertible ? undefined : value => Decimal.tan(value) + this.invertible ? value => Decimal.tan(value) : undefined, + this.hasVariable ); } @@ -1085,7 +1162,8 @@ export default class Formula { public sinh() { return new Formula( () => this.evaluate().sinh(), - !this.invertible ? undefined : value => Decimal.asinh(value) + this.invertible ? value => Decimal.asinh(value) : undefined, + this.hasVariable ); } @@ -1094,7 +1172,8 @@ export default class Formula { public cosh() { return new Formula( () => this.evaluate().cosh(), - !this.invertible ? undefined : value => Decimal.acosh(value) + this.invertible ? value => Decimal.acosh(value) : undefined, + this.hasVariable ); } @@ -1103,7 +1182,8 @@ export default class Formula { public tanh() { return new Formula( () => this.evaluate().tanh(), - !this.invertible ? undefined : value => Decimal.atanh(value) + this.invertible ? value => Decimal.atanh(value) : undefined, + this.hasVariable ); } @@ -1112,7 +1192,8 @@ export default class Formula { public asinh() { return new Formula( () => this.evaluate().asinh(), - !this.invertible ? undefined : value => Decimal.sinh(value) + this.invertible ? value => Decimal.sinh(value) : undefined, + this.hasVariable ); } @@ -1121,7 +1202,8 @@ export default class Formula { public acosh() { return new Formula( () => this.evaluate().acosh(), - !this.invertible ? undefined : value => Decimal.cosh(value) + this.invertible ? value => Decimal.cosh(value) : undefined, + this.hasVariable ); } @@ -1130,7 +1212,8 @@ export default class Formula { public atanh() { return new Formula( () => this.evaluate().atanh(), - !this.invertible ? undefined : value => Decimal.tanh(value) + this.invertible ? value => Decimal.tanh(value) : undefined, + this.hasVariable ); } } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 565ec76..3fd4f1e 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -182,13 +182,12 @@ const nonInvertibleOneParamFunctionNames = [ ["layeradd10"] ] as const; -const invertibleTwoParamFunctionNames = [["tetrate"]] as const; +const invertibleTwoParamFunctionNames = [["tetrate"], ["layeradd"]] as const; const nonInvertibleTwoParamFunctionNames = [ ["clamp"], ["iteratedexp"], ["iteratedlog"], - ["layeradd"], ["pentate"] ] as const; @@ -361,5 +360,47 @@ describe("Creating Formulas", () => { }); }); }); + + describe("Inverting calculates the value of the variable", () => { + let variable: Formula; + let constant: Formula; + beforeAll(() => { + variable = Formula.variable(2); + constant = Formula.constant(3); + }); + invertibleOneParamFunctionNames.flat().forEach(name => + describe(name, () => { + test(`${name}(var, const).invert()`, () => { + const formula = Formula[name](variable, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); + }); + test(`${name}(const, var).invert()`, () => { + const formula = Formula[name](constant, variable); + const result = formula.evaluate(); + expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); + }); + }) + ); + invertibleTwoParamFunctionNames.flat().forEach(name => + describe(name, () => { + test(`${name}(var, const, const).invert()`, () => { + const formula = Formula[name](variable, constant, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); + }); + test(`${name}(const, var, const).invert()`, () => { + const formula = Formula[name](constant, variable, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); + }); + test(`${name}(const, const, var).invert()`, () => { + const formula = Formula[name](constant, constant, variable); + const result = formula.evaluate(); + expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); + }); + }) + ); + }); }); }); From 7593fea5129b190df6b160c048a438bc693f065e Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 8 Jan 2023 10:15:46 -0600 Subject: [PATCH 12/56] Fix some tests. Boy tests run slow --- src/game/formulas.ts | 26 +- tests/game/formulas.test.ts | 580 +++++++++++++++++++----------------- 2 files changed, 326 insertions(+), 280 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index e54ca2e..1fcd392 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -32,14 +32,18 @@ function isVariableFormula(value: FormulaSource): value is VariableFormula { } function calculateInvertibility(...inputs: FormulaSource[]) { - const invertible = !inputs.some(input => input instanceof Formula && !input.invertible); - const hasVariable = - invertible && - inputs.filter(input => input instanceof Formula && input.invertible && input.hasVariable) - .length === 1; + if (inputs.some(input => input instanceof Formula && !input.invertible)) { + return { + invertible: false, + hasVariable: false + }; + } + const numVariables = inputs.filter( + input => input instanceof Formula && input.invertible && input.hasVariable + ).length; return { - invertible, - hasVariable + invertible: numVariables <= 1, + hasVariable: numVariables === 1 }; } @@ -622,11 +626,9 @@ export default class Formula { return new Formula( () => this.evaluate().sub(unrefFormulaSource(v)), invertible - ? value => - Decimal.add( - value, - isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() - ) + ? isVariableFormula(this) + ? value => Decimal.add(value, unrefFormulaSource(v)) + : value => Decimal.sub(this.evaluate(), value) : undefined, hasVariable ); diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 3fd4f1e..530bfa3 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,6 +1,6 @@ -import Formula, { InvertibleFormula } from "game/formulas"; -import Decimal, { DecimalSource } from "util/bignum"; -import { beforeAll, describe, expect, test } from "vitest"; +import Formula, { FormulaSource, InvertibleFormula } from "game/formulas"; +import Decimal, { DecimalSource, format } from "util/bignum"; +import { beforeAll, describe, expect, test, vi } from "vitest"; import { ref } from "vue"; type FormulaFunctions = keyof Formula & keyof typeof Formula & keyof typeof Decimal; @@ -9,8 +9,34 @@ interface FixedLengthArray<T, L extends number> extends ArrayLike<T> { length: L; } -function compare_tolerance(value: DecimalSource) { - return (other: DecimalSource) => Decimal.eq_tolerance(value, other); +expect.extend({ + compare_tolerance(received, expected) { + const { isNot } = this; + return { + // do not alter your "pass" based on isNot. Vitest does it for you + pass: Decimal.eq_tolerance(received, expected), + message: () => + `Expected ${received} to${ + (isNot as boolean) ? " not" : "" + } be close to ${expected}`, + expected: format(expected), + actual: format(received) + }; + } +}); + +interface CustomMatchers<R = unknown> { + compare_tolerance(expected: DecimalSource): R; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Vi { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Assertion extends CustomMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface AsymmetricMatchersContaining extends CustomMatchers {} + } } function testConstant( @@ -23,17 +49,20 @@ function testConstant( beforeAll(() => { formula = formulaFunc(); }); - test("evaluates correctly", () => expect(formula.evaluate()).toEqual(expectedValue)); - test("inverts correctly", () => expect(formula.invert(10)).toEqual(expectedValue)); - test("is invertible", () => expect(formula.invertible).toBe(true)); - test("is not marked as having a variable", () => expect(formula.hasVariable).toBe(false)); + test("evaluates correctly", async () => + expect(formula.evaluate()).compare_tolerance(expectedValue)); + test("invert is pass-through", async () => + expect(formula.invert(25)).compare_tolerance(25)); + test("is invertible", async () => expect(formula.invertible).toBe(true)); + test("is not marked as having a variable", async () => + expect(formula.hasVariable).toBe(false)); }); } // Utility function that will test all the different // It's a lot of tests, but I'd rather be exhaustive function testFormula<T extends FormulaFunctions>( - functionNames: readonly T[], + functionName: T, args: Readonly<FixedLengthArray<number, Parameters<typeof Formula[T]>["length"]>>, invertible = true ) { @@ -43,155 +72,155 @@ function testFormula<T extends FormulaFunctions>( value = testValueFormulas[args[0]].evaluate(); }); - functionNames.forEach(name => { - let testName = name + "("; - for (let i = 0; i < args.length; i++) { - if (i !== 0) { - testName += ", "; - } - testName += testValues[args[i]]; + let testName = functionName + "("; + for (let i = 0; i < args.length; i++) { + if (i !== 0) { + testName += ", "; } - testName += ")"; - describe(testName, () => { - let expectedEvaluation: Decimal | undefined; - let formulaArgs: Formula[]; - let staticFormula: Formula; - let instanceFormula: Formula; - beforeAll(() => { - for (let i = 0; i < args.length; i++) { - formulaArgs.push(testValueFormulas[args[i]]); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - staticFormula = Formula[name](...formulaArgs); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - instanceFormula = formulaArgs[0][name](...formulaArgs.slice(1)); - - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expectedEvaluation = Decimal[name](...args); - } catch { - // If this is an invalid Decimal operation, then ignore this test case - return; - } - }); - - test("Static formula is not marked as having a variable", () => - expect(staticFormula.hasVariable).toBe(false)); - test("Static function evaluates correctly", () => - expectedEvaluation != null && - expect(staticFormula.evaluate()).toSatisfy(compare_tolerance(expectedEvaluation))); - test("Static function invertible", () => - expect(staticFormula.invertible).toBe(invertible)); - if (invertible) { - test("Static function inverts correctly", () => - expectedEvaluation != null && - !Decimal.isNaN(expectedEvaluation) && - expect(staticFormula.invert(expectedEvaluation)).toSatisfy( - compare_tolerance(value) - )); + testName += testValues[args[i]]; + } + testName += ")"; + describe(testName, () => { + let expectedEvaluation: Decimal | undefined; + const formulaArgs: Formula[] = []; + let staticFormula: Formula; + let instanceFormula: Formula; + beforeAll(() => { + for (let i = 0; i < args.length; i++) { + formulaArgs.push(testValueFormulas[args[i]]); } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + staticFormula = Formula[functionName](...formulaArgs); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + instanceFormula = formulaArgs[0][functionName](...formulaArgs.slice(1)); - // Do those tests again but for non-static methods - test("Instance formula is not marked as having a variable", () => - expect(instanceFormula.hasVariable).toBe(false)); - test("Instance function evaluates correctly", () => - expectedEvaluation != null && - expect(instanceFormula.evaluate()).toSatisfy( - compare_tolerance(expectedEvaluation) - )); - test("Instance function invertible", () => - expect(instanceFormula.invertible).toBe(invertible)); - if (invertible) { - test("Instance function inverts correctly", () => - expectedEvaluation != null && - !Decimal.isNaN(expectedEvaluation) && - expect(instanceFormula.invert(expectedEvaluation)).toSatisfy( - compare_tolerance(value) - )); + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expectedEvaluation = Decimal[functionName](...args); + } catch { + // If this is an invalid Decimal operation, then ignore this test case + return; } }); + + test("Static formula is not marked as having a variable", async () => + expect(staticFormula.hasVariable).toBe(false)); + test("Static function evaluates correctly", async () => + expectedEvaluation != null && + expect(staticFormula.evaluate()).compare_tolerance(expectedEvaluation)); + test("Static function invertible", async () => + expect(staticFormula.invertible).toBe(invertible)); + if (invertible) { + test("Static function inverts correctly", async () => + expectedEvaluation != null && + !Decimal.isNaN(expectedEvaluation) && + expect(staticFormula.invert(expectedEvaluation)).compare_tolerance(value)); + } + + // Do those tests again but for non-static methods + test("Instance formula is not marked as having a variable", async () => + expect(instanceFormula.hasVariable).toBe(false)); + test("Instance function evaluates correctly", async () => + expectedEvaluation != null && + expect(instanceFormula.evaluate()).compare_tolerance(expectedEvaluation)); + test("Instance function invertible", async () => + expect(instanceFormula.invertible).toBe(invertible)); + if (invertible) { + test("Instance function inverts correctly", async () => + expectedEvaluation != null && + !Decimal.isNaN(expectedEvaluation) && + expect(instanceFormula.invert(expectedEvaluation)).compare_tolerance(value)); + } }); } +function testAliases<T extends FormulaFunctions[]>( + formula: Formula, + aliases: T, + args: FormulaSource[] +) { + const spy = vi.spyOn(formula, aliases[0]); + expect(spy).not.toHaveBeenCalled(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + aliases.slice(1).forEach(name => formula[name](...args)); + expect(spy).toHaveBeenCalledTimes(aliases.length - 1); +} + const testValues = [-2.5, -1, -0.1, 0, 0.1, 1, 2.5] as const; let testValueFormulas: InvertibleFormula[] = []; const invertibleZeroParamFunctionNames = [ - ["neg", "negate", "negated"], - ["recip", "reciprocal", "reciprocate"], - ["log10"], - ["log2"], - ["ln"], - ["pow10"], - ["exp"], - ["sqr"], - ["sqrt"], - ["cube"], - ["cbrt"], - ["lambertw"], - ["ssqrt"], - ["sin"], - ["cos"], - ["tan"], - ["asin"], - ["acos"], - ["atan"], - ["sinh"], - ["cosh"], - ["tanh"], - ["asinh"], - ["acosh"], - ["atanh"] + "neg", + "recip", + "log10", + "log2", + "ln", + "pow10", + "exp", + "sqr", + "sqrt", + "cube", + "cbrt", + "lambertw", + "ssqrt", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "asinh", + "acosh", + "atanh" ] as const; const nonInvertibleZeroParamFunctionNames = [ - ["abs"], - ["sign", "sgn"], - ["round"], - ["floor"], - ["ceil"], - ["trunc"], - ["pLog10"], - ["absLog10"], - ["factorial"], - ["gamma"], - ["lngamma"] + "abs", + "sign", + "round", + "floor", + "ceil", + "trunc", + "pLog10", + "absLog10", + "factorial", + "gamma", + "lngamma" ] as const; const invertibleOneParamFunctionNames = [ - ["add", "plus"], - ["sub", "subtract", "minus"], - ["mul", "multiply", "times"], - ["div", "divide"], - ["log", "logarithm"], - ["pow"], - ["root"], - ["slog"] + "add", + "sub", + "mul", + "div", + "log", + "pow", + "root", + "slog" ] as const; const nonInvertibleOneParamFunctionNames = [ - ["max"], - ["min"], - ["maxabs"], - ["minabs"], - ["clampMin"], - ["clampMax"], - ["layeradd10"] + "max", + "min", + "maxabs", + "minabs", + "clampMin", + "clampMax", + "layeradd10" ] as const; -const invertibleTwoParamFunctionNames = [["tetrate"], ["layeradd"]] as const; +const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; -const nonInvertibleTwoParamFunctionNames = [ - ["clamp"], - ["iteratedexp"], - ["iteratedlog"], - ["pentate"] -] as const; +const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; -describe("Creating Formulas", () => { +describe.concurrent("Creating Formulas", () => { beforeAll(() => { testValueFormulas = testValues.map(v => Formula.constant(v)); }); @@ -204,6 +233,23 @@ describe("Creating Formulas", () => { testConstant("ref", () => Formula.constant(ref(10))); }); + // Test that these are just pass-throughts so we don't need to test each one everywhere else + describe("Function aliases", () => { + let formula: Formula; + beforeAll(() => { + formula = Formula.constant(10); + }); + test("neg", async () => testAliases(formula, ["neg", "negate", "negated"], [0])); + test("recip", async () => + testAliases(formula, ["recip", "reciprocal", "reciprocate"], [0])); + test("sign", async () => testAliases(formula, ["sign", "sgn"], [0])); + test("add", async () => testAliases(formula, ["add", "plus"], [0])); + test("sub", async () => testAliases(formula, ["sub", "subtract", "minus"], [0])); + test("mul", async () => testAliases(formula, ["mul", "multiply", "times"], [0])); + test("div", async () => testAliases(formula, ["div", "divide"], [1])); + test("log", async () => testAliases(formula, ["log", "logarithm"], [0])); + }); + describe("Invertible 0-param", () => { invertibleZeroParamFunctionNames.forEach(names => { for (let i = 0; i < testValues.length; i++) { @@ -258,149 +304,147 @@ describe("Creating Formulas", () => { } }); }); +}); - describe("Variables", () => { +describe("Variables", () => { + let variable: Formula; + let constant: Formula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("Created variable is marked as a variable", () => expect(variable.hasVariable).toBe(true)); + test("Evaluate() returns variable's value", () => + expect(variable.evaluate()).compare_tolerance(10)); + test("Invert() is pass-through", () => expect(variable.invert(100)).compare_tolerance(100)); + + test("Nested variable is marked as having a variable", () => + expect(variable.add(10).div(3).pow(2).hasVariable).toBe(true)); + test("Nested non-variable is marked as not having a variable", () => + expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false)); + + describe("Invertible Formulas correctly calculate when they contain a variable", () => { + function checkFormula(formula: Formula, expectedBool = true) { + expect(formula.invertible).toBe(expectedBool); + expect(formula.hasVariable).toBe(expectedBool); + } + invertibleZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable))); + }); + }); + invertibleOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable), false)); + }); + }); + invertibleTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as invertible and having a variable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, constant), false)); + test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant, variable), false)); + test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable, variable), false)); + test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, variable), false)); + }); + }); + }); + + describe("Non-Invertible Formulas never marked as having a variable", () => { + function checkFormula(formula: Formula) { + expect(formula.invertible).toBe(false); + expect(formula.hasVariable).toBe(false); + } + nonInvertibleZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonInvertibleOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonInvertibleTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + + describe("Inverting calculates the value of the variable", () => { let variable: Formula; let constant: Formula; beforeAll(() => { - variable = Formula.variable(10); - constant = Formula.constant(10); + variable = Formula.variable(2); + constant = Formula.constant(3); }); - - test("Created variable is marked as a variable", () => - expect(variable.hasVariable).toBe(true)); - test("Evaluate() returns variable's value", () => - expect(variable.evaluate()).toSatisfy(compare_tolerance(10))); - test("Invert() is pass-through", () => - expect(variable.invert(100)).toSatisfy(compare_tolerance(100))); - - test("Nested variable is marked as having a variable", () => - expect(variable.add(10).div(3).pow(2).hasVariable).toBe(false)); - test("Nested non-variable is marked as not having a variable", () => - expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false)); - - describe("Invertible Formulas correctly calculate when they contain a variable", () => { - function checkFormula(formula: Formula, expectedBool = true) { - expect(formula.invertible).toBe(expectedBool); - expect(formula.hasVariable).toBe(expectedBool); - } - invertibleZeroParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](variable))); + invertibleOneParamFunctionNames.forEach(name => + describe(name, () => { + test(`${name}(var, const).invert()`, () => { + const formula = Formula[name](variable, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); }); - }); - invertibleOneParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable), false)); + test(`${name}(const, var).invert()`, () => { + const formula = Formula[name](constant, variable); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); }); - }); - invertibleTwoParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var, const, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, constant), false)); - test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, constant, variable), false)); - test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](constant, variable, variable), false)); - test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, variable), false)); + }) + ); + invertibleTwoParamFunctionNames.forEach(name => + describe(name, () => { + test(`${name}(var, const, const).invert()`, () => { + const formula = Formula[name](variable, constant, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); }); - }); - }); - - describe("Non-Invertible Formulas never marked as having a variable", () => { - function checkFormula(formula: Formula) { - expect(formula.invertible).toBe(false); - expect(formula.hasVariable).toBe(false); - } - nonInvertibleZeroParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable))); + test(`${name}(const, var, const).invert()`, () => { + const formula = Formula[name](constant, variable, constant); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); }); - }); - nonInvertibleOneParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var, const) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable))); + test(`${name}(const, const, var).invert()`, () => { + const formula = Formula[name](constant, constant, variable); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); }); - }); - nonInvertibleTwoParamFunctionNames.flat().forEach(name => { - describe(name, () => { - test(`${name}(var, const, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, constant))); - test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, constant, variable))); - test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](constant, variable, variable))); - test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, variable))); - }); - }); - }); - - describe("Inverting calculates the value of the variable", () => { - let variable: Formula; - let constant: Formula; - beforeAll(() => { - variable = Formula.variable(2); - constant = Formula.constant(3); - }); - invertibleOneParamFunctionNames.flat().forEach(name => - describe(name, () => { - test(`${name}(var, const).invert()`, () => { - const formula = Formula[name](variable, constant); - const result = formula.evaluate(); - expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); - }); - test(`${name}(const, var).invert()`, () => { - const formula = Formula[name](constant, variable); - const result = formula.evaluate(); - expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); - }); - }) - ); - invertibleTwoParamFunctionNames.flat().forEach(name => - describe(name, () => { - test(`${name}(var, const, const).invert()`, () => { - const formula = Formula[name](variable, constant, constant); - const result = formula.evaluate(); - expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); - }); - test(`${name}(const, var, const).invert()`, () => { - const formula = Formula[name](constant, variable, constant); - const result = formula.evaluate(); - expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); - }); - test(`${name}(const, const, var).invert()`, () => { - const formula = Formula[name](constant, constant, variable); - const result = formula.evaluate(); - expect(formula.invert(result)).toSatisfy(compare_tolerance(2)); - }); - }) - ); - }); + }) + ); }); }); From 773401069a2c7f605441ac3b62667d51dbbf0ec0 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 15 Jan 2023 00:06:54 -0600 Subject: [PATCH 13/56] Made formulas comparable and more efficient --- src/game/formulas.ts | 1317 ++++++++++++++++------------------- src/lib/break_eternity.ts | 4 + tests/game/formulas.test.ts | 351 +++++----- 3 files changed, 807 insertions(+), 865 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 1fcd392..e4d5af8 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,384 +1,689 @@ import Decimal, { DecimalSource } from "util/bignum"; -import { convertComputable, ProcessedComputable } from "util/computed"; -import { unref } from "vue"; +import { ProcessedComputable } from "util/computed"; +import { ref, Ref, unref } from "vue"; -export type FormulaSource = ProcessedComputable<DecimalSource> | Formula; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type GenericFormula = Formula<any>; +export type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula; export type InvertibleFormulaSource = ProcessedComputable<DecimalSource> | InvertibleFormula; +export type InvertibleFormula = GenericFormula & { + invert: (value: DecimalSource) => Decimal; +}; -export type InvertibleFormula = Formula & { invertible: true }; -export type VariableFormula = InvertibleFormula & { hasVariable: true }; - -function F(value: InvertibleFormulaSource): InvertibleFormula; -function F(value: FormulaSource): Formula; -function F(value: FormulaSource) { - return value instanceof Formula - ? value - : new Formula( - () => new Decimal(unref(value)), - value => new Decimal(value) - ); +function hasVariable(value: FormulaSource): value is InvertibleFormula { + return value instanceof Formula && value.hasVariable(); } -function processFormulaSource(value: FormulaSource) { - return value instanceof Formula ? value : convertComputable(value); -} +// It's really hard to type mapped tuples, but these classes seem to manage +type FormulasToDecimals<T extends FormulaSource[]> = { + [K in keyof T]: DecimalSource; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TupleGuard<T extends any[]> = T extends any[] ? FormulasToDecimals<T> : never; +type GuardedFormulasToDecimals<T extends FormulaSource[]> = TupleGuard<T>; -function unrefFormulaSource(value: Formula | ProcessedComputable<DecimalSource>) { +export function unrefFormulaSource(value: FormulaSource) { return value instanceof Formula ? value.evaluate() : unref(value); } -function isVariableFormula(value: FormulaSource): value is VariableFormula { - return value instanceof Formula && value.hasVariable; -} - -function calculateInvertibility(...inputs: FormulaSource[]) { - if (inputs.some(input => input instanceof Formula && !input.invertible)) { - return { - invertible: false, - hasVariable: false - }; +function invertNeg(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.neg(value)); } - const numVariables = inputs.filter( - input => input instanceof Formula && input.invertible && input.hasVariable - ).length; - return { - invertible: numVariables <= 1, - hasVariable: numVariables === 1 - }; + throw "Could not invert due to no input being a variable"; } -export default class Formula { - public readonly invertible: boolean; - public readonly hasVariable: boolean; - public readonly evaluate: () => Decimal; - public readonly invert: (value: DecimalSource) => Decimal; +function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs))); + } + throw "Could not invert due to no input being a variable"; +} + +function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs))); + } + throw "Could not invert due to no input being a variable"; +} + +function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertRecip(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.recip(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertLog10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow10(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertLog2(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(2, value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertLn(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs)))); + } + throw "Could not invert due to no input being a variable"; +} + +function invertPow10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, 10)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value))); + } + throw "Could not invert due to no input being a variable"; +} + +function invertExp(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.ln(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function tetrate( + value: DecimalSource, + height: DecimalSource = 2, + payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) +) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.tetrate(value, heightNumber, payload); +} + +function invertTetrate( + value: DecimalSource, + base: FormulaSource, + height: FormulaSource, + payload: FormulaSource +) { + if (hasVariable(base)) { + return base.invert( + Decimal.slog(value, Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber()) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +function iteratedexp( + value: DecimalSource, + height: DecimalSource = 2, + payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) +) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.iteratedexp(value, heightNumber, new Decimal(payload)); +} + +function invertIteratedExp( + value: DecimalSource, + lhs: FormulaSource, + height: FormulaSource, + payload: FormulaSource +) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.iteratedlog( + value, + Math.E, + Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber() + ) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +function iteratedLog(value: DecimalSource, lhs: DecimalSource = 10, times: DecimalSource = 2) { + const timesNumber = Decimal.minabs(times, 1e308).toNumber(); + return Decimal.iteratedlog(value, lhs, timesNumber); +} + +function slog(value: DecimalSource, lhs: DecimalSource = 10) { + const baseNumber = Decimal.minabs(lhs, 1e308).toNumber(); + return Decimal.slog(value, baseNumber); +} + +function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber()) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) { + const diffNumber = Decimal.minabs(diff, 1e308).toNumber(); + return Decimal.layeradd(value, diffNumber, base); +} + +function invertLayeradd( + value: DecimalSource, + lhs: FormulaSource, + diff: FormulaSource, + base: FormulaSource +) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.layeradd( + value, + Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber() + ) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +function invertLambertw(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(Math.E, value).times(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertSsqrt(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tetrate(value, 2)); + } + throw "Could not invert due to no input being a variable"; +} + +function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.pentate(value, heightNumber, payload); +} + +function invertSin(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.asin(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertCos(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.acos(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertTan(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.atan(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAsin(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sin(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAcos(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.cos(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAtan(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tan(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertSinh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.asinh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertCosh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.acosh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertTanh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.atanh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAsinh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sinh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAcosh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.cosh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +function invertAtanh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tanh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export default class Formula<T extends [FormulaSource] | FormulaSource[]> { + readonly inputs: T; + + private readonly internalEvaluate: + | ((...inputs: GuardedFormulasToDecimals<T>) => DecimalSource) + | undefined; + private readonly internalInvert: + | ((value: DecimalSource, ...inputs: T) => DecimalSource) + | undefined; + private readonly internalHasVariable: boolean; constructor( - evaluate: () => Decimal, - invert?: (value: DecimalSource) => Decimal, + inputs: T, + evaluate?: (this: Formula<T>, ...inputs: GuardedFormulasToDecimals<T>) => DecimalSource, + invert?: ( + this: Formula<T>, + value: DecimalSource, + ...inputs: [...T, ...unknown[]] + ) => DecimalSource, hasVariable = false ) { - this.invertible = invert != null; - this.hasVariable = hasVariable; - this.evaluate = evaluate; - this.invert = invert ?? (() => Decimal.dNaN); + if (inputs.length !== 1 && evaluate == null) { + throw "Evaluate function is required if inputs is not length 1"; + } + if (inputs.length !== 1 && invert == null && hasVariable) { + throw "A formula cannot be marked as having a variable if it is not invertible and inputs is not length 1"; + } + + this.inputs = inputs; + this.internalEvaluate = evaluate; + + if ( + inputs.some(input => input instanceof Formula && !input.isInvertible()) || + (hasVariable === false && evaluate != null && invert == null) + ) { + this.internalHasVariable = false; + return; + } + + const numVariables = inputs.filter( + input => input instanceof Formula && input.hasVariable() + ).length; + + // ??? + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.internalInvert = numVariables <= 1 ? invert : undefined; + this.internalHasVariable = numVariables === 1 || (numVariables === 0 && hasVariable); + } + + isInvertible(): this is InvertibleFormula { + return this.internalInvert != null || this.internalEvaluate == null; + } + + hasVariable(): boolean { + return this.internalHasVariable; + } + + evaluate(): DecimalSource { + return ( + this.internalEvaluate?.call( + this, + ...(this.inputs.map(unrefFormulaSource) as GuardedFormulasToDecimals<T>) + ) ?? unrefFormulaSource(this.inputs[0]) + ); + } + + invert(value: DecimalSource): DecimalSource { + return this.internalInvert?.call(this, value, ...this.inputs) ?? value; + } + + equals(other: GenericFormula): boolean { + return ( + this.inputs.length === other.inputs.length && + this.inputs.every((input, i) => + input instanceof Formula && other.inputs[i] instanceof Formula + ? input.equals(other.inputs[i]) + : !(input instanceof Formula) && + !(other.inputs[i] instanceof Formula) && + Decimal.eq(unref(input), unref(other.inputs[i])) + ) && + this.internalEvaluate === other.internalEvaluate && + this.internalInvert === other.internalInvert && + this.internalHasVariable === other.internalHasVariable + ); } public static constant(value: InvertibleFormulaSource): InvertibleFormula { - return F(value); + return new Formula([value]) as InvertibleFormula; } - public static variable(value: ProcessedComputable<DecimalSource>): VariableFormula { - return new Formula( - () => new Decimal(unref(value)), - value => new Decimal(value), - true - ) as VariableFormula; + public static variable(value: ProcessedComputable<DecimalSource>): InvertibleFormula { + return new Formula([value], undefined, undefined, true) as InvertibleFormula; } - public static abs(value: FormulaSource) { - return F(value).abs(); + public static abs(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.abs); } public static neg(value: InvertibleFormulaSource): InvertibleFormula; - public static neg(value: FormulaSource): Formula; + public static neg(value: FormulaSource): GenericFormula; public static neg(value: FormulaSource) { - return F(value).neg(); + return new Formula([value], Decimal.neg, invertNeg); } - - public static negate(value: InvertibleFormulaSource): InvertibleFormula; - public static negate(value: FormulaSource): Formula; public static negate(value: FormulaSource) { - return F(value).neg(); + return Formula.neg(value); } - - public static negated(value: InvertibleFormulaSource): InvertibleFormula; - public static negated(value: FormulaSource): Formula; public static negated(value: FormulaSource) { - return F(value).neg(); + return Formula.neg(value); } - public static sign(value: FormulaSource) { - return F(value).sign(); + public static sign(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.sign); } - public static sgn(value: FormulaSource) { - return F(value).sign(); + return Formula.sign(value); } - public static round(value: FormulaSource) { - return F(value).round(); + public static round(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.round); } - public static floor(value: FormulaSource) { - return F(value).floor(); + public static floor(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.floor); } - public static ceil(value: FormulaSource) { - return F(value).ceil(); + public static ceil(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.ceil); } - public static trunc(value: FormulaSource) { - return F(value).trunc(); + public static trunc(value: FormulaSource): GenericFormula { + return new Formula([value], Decimal.trunc); } public static add( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static add(value: FormulaSource, other: FormulaSource): Formula; + public static add(value: FormulaSource, other: FormulaSource): GenericFormula; public static add(value: FormulaSource, other: FormulaSource) { - return F(value).add(other); + return new Formula([value, other], Decimal.add, invertAdd); } - - public static plus( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static plus(value: FormulaSource, other: FormulaSource): Formula; public static plus(value: FormulaSource, other: FormulaSource) { - return F(value).add(other); + return Formula.add(value, other); } public static sub( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static sub(value: FormulaSource, other: FormulaSource): Formula; + public static sub(value: FormulaSource, other: FormulaSource): GenericFormula; public static sub(value: FormulaSource, other: FormulaSource) { - return F(value).sub(other); + return new Formula([value, other], Decimal.sub, invertSub); } - - public static subtract( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static subtract(value: FormulaSource, other: FormulaSource): Formula; public static subtract(value: FormulaSource, other: FormulaSource) { - return F(value).sub(other); + return Formula.sub(value, other); } - - public static minus( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static minus(value: FormulaSource, other: FormulaSource): Formula; public static minus(value: FormulaSource, other: FormulaSource) { - return F(value).sub(other); + return Formula.sub(value, other); } public static mul( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static mul(value: FormulaSource, other: FormulaSource): Formula; + public static mul(value: FormulaSource, other: FormulaSource): GenericFormula; public static mul(value: FormulaSource, other: FormulaSource) { - return F(value).mul(other); + return new Formula([value, other], Decimal.mul, invertMul); } - - public static multiply( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static multiply(value: FormulaSource, other: FormulaSource): Formula; public static multiply(value: FormulaSource, other: FormulaSource) { - return F(value).mul(other); + return Formula.mul(value, other); } - - public static times( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static times(value: FormulaSource, other: FormulaSource): Formula; public static times(value: FormulaSource, other: FormulaSource) { - return F(value).mul(other); + return Formula.mul(value, other); } public static div( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static div(value: FormulaSource, other: FormulaSource): Formula; + public static div(value: FormulaSource, other: FormulaSource): GenericFormula; public static div(value: FormulaSource, other: FormulaSource) { - return F(value).div(other); + return new Formula([value, other], Decimal.div, invertDiv); } - - public static divide( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula; - public static divide(value: FormulaSource, other: FormulaSource): Formula; public static divide(value: FormulaSource, other: FormulaSource) { - return F(value).div(other); + return Formula.div(value, other); } public static recip(value: InvertibleFormulaSource): InvertibleFormula; - public static recip(value: FormulaSource): Formula; + public static recip(value: FormulaSource): GenericFormula; public static recip(value: FormulaSource) { - return F(value).recip(); + return new Formula([value], Decimal.recip, invertRecip); } - - public static reciprocal(value: InvertibleFormulaSource): InvertibleFormula; - public static reciprocal(value: FormulaSource): Formula; - public static reciprocal(value: FormulaSource) { - return F(value).recip(); + public static reciprocal(value: FormulaSource): GenericFormula { + return Formula.recip(value); } - - public static reciprocate(value: InvertibleFormulaSource): InvertibleFormula; - public static reciprocate(value: FormulaSource): Formula; public static reciprocate(value: FormulaSource) { - return F(value).reciprocate(); + return Formula.recip(value); } - public static max(value: FormulaSource, other: FormulaSource) { - return F(value).max(other); + public static max(value: FormulaSource, other: FormulaSource): GenericFormula { + return new Formula([value, other], Decimal.max); } - public static min(value: FormulaSource, other: FormulaSource) { - return F(value).min(other); + public static min(value: FormulaSource, other: FormulaSource): GenericFormula { + return new Formula([value, other], Decimal.min); } - public static minabs(value: FormulaSource, other: FormulaSource) { - return F(value).minabs(other); + public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { + return new Formula([value, other], Decimal.minabs); } - public static maxabs(value: FormulaSource, other: FormulaSource) { - return F(value).maxabs(other); + public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { + return new Formula([value, other], Decimal.maxabs); } - public static clamp(value: FormulaSource, min: FormulaSource, max: FormulaSource) { - return F(value).clamp(min, max); + public static clamp( + value: FormulaSource, + min: FormulaSource, + max: FormulaSource + ): GenericFormula { + return new Formula([value, min, max], Decimal.clamp); } - public static clampMin(value: FormulaSource, min: FormulaSource) { - return F(value).clampMin(min); + public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { + return new Formula([value, min], Decimal.clampMin); } - public static clampMax(value: FormulaSource, max: FormulaSource) { - return F(value).clampMax(max); + public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { + return new Formula([value, max], Decimal.clampMax); } public static pLog10(value: InvertibleFormulaSource): InvertibleFormula; - public static pLog10(value: FormulaSource): Formula; + public static pLog10(value: FormulaSource): GenericFormula; public static pLog10(value: FormulaSource) { - return F(value).pLog10(); + return new Formula([value], Decimal.pLog10); } public static absLog10(value: InvertibleFormulaSource): InvertibleFormula; - public static absLog10(value: FormulaSource): Formula; + public static absLog10(value: FormulaSource): GenericFormula; public static absLog10(value: FormulaSource) { - return F(value).absLog10(); + return new Formula([value], Decimal.absLog10); } public static log10(value: InvertibleFormulaSource): InvertibleFormula; - public static log10(value: FormulaSource): Formula; + public static log10(value: FormulaSource): GenericFormula; public static log10(value: FormulaSource) { - return F(value).log10(); + return new Formula([value], Decimal.log10, invertLog10); } public static log( value: InvertibleFormulaSource, base: InvertibleFormulaSource ): InvertibleFormula; - public static log(value: FormulaSource, base: FormulaSource): Formula; + public static log(value: FormulaSource, base: FormulaSource): GenericFormula; public static log(value: FormulaSource, base: FormulaSource) { - return F(value).log(base); + return new Formula([value, base], Decimal.log, invertLog); } - - public static logarithm( - value: InvertibleFormulaSource, - base: InvertibleFormulaSource - ): InvertibleFormula; - public static logarithm(value: FormulaSource, base: FormulaSource): Formula; public static logarithm(value: FormulaSource, base: FormulaSource) { - return F(value).log(base); + return Formula.log(value, base); } public static log2(value: InvertibleFormulaSource): InvertibleFormula; - public static log2(value: FormulaSource): Formula; + public static log2(value: FormulaSource): GenericFormula; public static log2(value: FormulaSource) { - return F(value).log2(); + return new Formula([value], Decimal.log2, invertLog2); } public static ln(value: InvertibleFormulaSource): InvertibleFormula; - public static ln(value: FormulaSource): Formula; + public static ln(value: FormulaSource): GenericFormula; public static ln(value: FormulaSource) { - return F(value).ln(); + return new Formula([value], Decimal.ln, invertLn); } public static pow( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static pow(value: FormulaSource, other: FormulaSource): Formula; + public static pow(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow(value: FormulaSource, other: FormulaSource) { - return F(value).pow(other); + return new Formula([value, other], Decimal.pow, invertPow); } public static pow10(value: InvertibleFormulaSource): InvertibleFormula; - public static pow10(value: FormulaSource): Formula; + public static pow10(value: FormulaSource): GenericFormula; public static pow10(value: FormulaSource) { - return F(value).pow10(); + return new Formula([value], Decimal.pow10, invertPow10); } public static pow_base( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static pow_base(value: FormulaSource, other: FormulaSource): Formula; + public static pow_base(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow_base(value: FormulaSource, other: FormulaSource) { - return F(value).pow_base(other); + return new Formula([value, other], Decimal.pow_base, invertPowBase); } public static root( value: InvertibleFormulaSource, other: InvertibleFormulaSource ): InvertibleFormula; - public static root(value: FormulaSource, other: FormulaSource): Formula; + public static root(value: FormulaSource, other: FormulaSource): GenericFormula; public static root(value: FormulaSource, other: FormulaSource) { - return F(value).root(other); + return new Formula([value, other], Decimal.root, invertRoot); } public static factorial(value: FormulaSource) { - return F(value).factorial(); + return new Formula([value], Decimal.factorial); } public static gamma(value: FormulaSource) { - return F(value).gamma(); + return new Formula([value], Decimal.gamma); } public static lngamma(value: FormulaSource) { - return F(value).lngamma(); + return new Formula([value], Decimal.lngamma); } public static exp(value: InvertibleFormulaSource): InvertibleFormula; - public static exp(value: FormulaSource): Formula; + public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource) { - return F(value).exp(); + return new Formula([value], Decimal.exp, invertExp); } - public static sqr(value: InvertibleFormulaSource): InvertibleFormula; - public static sqr(value: FormulaSource): Formula; public static sqr(value: FormulaSource) { - return F(value).sqr(); + return Formula.pow(value, 2); } - public static sqrt(value: InvertibleFormulaSource): InvertibleFormula; - public static sqrt(value: FormulaSource): Formula; public static sqrt(value: FormulaSource) { - return F(value).sqrt(); + return Formula.root(value, 2); } - public static cube(value: InvertibleFormulaSource): InvertibleFormula; - public static cube(value: FormulaSource): Formula; public static cube(value: FormulaSource) { - return F(value).cube(); + return Formula.pow(value, 3); } - public static cbrt(value: InvertibleFormulaSource): InvertibleFormula; - public static cbrt(value: FormulaSource): Formula; public static cbrt(value: FormulaSource) { - return F(value).cbrt(); + return Formula.root(value, 3); } public static tetrate( @@ -390,13 +695,13 @@ export default class Formula { value: FormulaSource, height?: FormulaSource, payload?: FormulaSource - ): Formula; + ): GenericFormula; public static tetrate( value: FormulaSource, height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return F(value).tetrate(height, payload); + return new Formula([value, height, payload], tetrate, invertTetrate); } public static iteratedexp( @@ -408,34 +713,34 @@ export default class Formula { value: FormulaSource, height?: FormulaSource, payload?: FormulaSource - ): Formula; + ): GenericFormula; public static iteratedexp( value: FormulaSource, height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return F(value).iteratedexp(height, payload); + return new Formula([value, height, payload], iteratedexp, invertIteratedExp); } public static iteratedlog( value: FormulaSource, base: FormulaSource = 10, times: FormulaSource = 1 - ) { - return F(value).iteratedlog(base, times); + ): GenericFormula { + return new Formula([value, base, times], iteratedLog); } public static slog( value: InvertibleFormulaSource, base?: InvertibleFormulaSource ): InvertibleFormula; - public static slog(value: FormulaSource, base?: FormulaSource): Formula; + public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { - return F(value).slog(base); + return new Formula([value, base], slog, invertSlog); } - public static layeradd10(value: FormulaSource, diff: FormulaSource) { - return F(value).layeradd10(diff); + public static layeradd10(value: FormulaSource, diff: FormulaSource): GenericFormula { + return new Formula([value, diff], Decimal.layeradd10); } public static layeradd( @@ -447,21 +752,21 @@ export default class Formula { value: FormulaSource, diff: FormulaSource, base?: FormulaSource - ): Formula; + ): GenericFormula; public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { - return F(value).layeradd(diff, base); + return new Formula([value, diff, base], layeradd, invertLayeradd); } public static lambertw(value: InvertibleFormulaSource): InvertibleFormula; - public static lambertw(value: FormulaSource): Formula; + public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { - return F(value).lambertw(); + return new Formula([value], Decimal.lambertw, invertLambertw); } public static ssqrt(value: InvertibleFormulaSource): InvertibleFormula; - public static ssqrt(value: FormulaSource): Formula; + public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { - return F(value).ssqrt(); + return new Formula([value], Decimal.ssqrt, invertSsqrt); } public static pentate( @@ -469,753 +774,359 @@ export default class Formula { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return F(value).pentate(height, payload); + return new Formula([value, height, payload], pentate); } public static sin(value: InvertibleFormulaSource): InvertibleFormula; - public static sin(value: FormulaSource): Formula; + public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource) { - return F(value).sin(); + return new Formula([value], Decimal.sin, invertAsin); } public static cos(value: InvertibleFormulaSource): InvertibleFormula; - public static cos(value: FormulaSource): Formula; + public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource) { - return F(value).cos(); + return new Formula([value], Decimal.cos, invertAcos); } public static tan(value: InvertibleFormulaSource): InvertibleFormula; - public static tan(value: FormulaSource): Formula; + public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource) { - return F(value).tan(); + return new Formula([value], Decimal.tan, invertAtan); } public static asin(value: InvertibleFormulaSource): InvertibleFormula; - public static asin(value: FormulaSource): Formula; + public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource) { - return F(value).asin(); + return new Formula([value], Decimal.asin, invertSin); } public static acos(value: InvertibleFormulaSource): InvertibleFormula; - public static acos(value: FormulaSource): Formula; + public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource) { - return F(value).acos(); + return new Formula([value], Decimal.acos, invertCos); } public static atan(value: InvertibleFormulaSource): InvertibleFormula; - public static atan(value: FormulaSource): Formula; + public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource) { - return F(value).atan(); + return new Formula([value], Decimal.atan, invertTan); } public static sinh(value: InvertibleFormulaSource): InvertibleFormula; - public static sinh(value: FormulaSource): Formula; + public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource) { - return F(value).sinh(); + return new Formula([value], Decimal.sinh, invertAsinh); } public static cosh(value: InvertibleFormulaSource): InvertibleFormula; - public static cosh(value: FormulaSource): Formula; + public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource) { - return F(value).cosh(); + return new Formula([value], Decimal.cosh, invertAcosh); } public static tanh(value: InvertibleFormulaSource): InvertibleFormula; - public static tanh(value: FormulaSource): Formula; + public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource) { - return F(value).tanh(); + return new Formula([value], Decimal.tanh, invertAtanh); } public static asinh(value: InvertibleFormulaSource): InvertibleFormula; - public static asinh(value: FormulaSource): Formula; + public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource) { - return F(value).asinh(); + return new Formula([value], Decimal.asinh, invertSinh); } public static acosh(value: InvertibleFormulaSource): InvertibleFormula; - public static acosh(value: FormulaSource): Formula; + public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource) { - return F(value).acosh(); + return new Formula([value], Decimal.acosh, invertCosh); } public static atanh(value: InvertibleFormulaSource): InvertibleFormula; - public static atanh(value: FormulaSource): Formula; + public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource) { - return F(value).atanh(); + return new Formula([value], Decimal.atanh, invertTanh); } public abs() { - return new Formula(() => this.evaluate().abs()); + return Formula.abs(this); } - public neg(this: InvertibleFormula): InvertibleFormula; - public neg(this: Formula): Formula; public neg() { - return new Formula( - () => this.evaluate().neg(), - value => Decimal.neg(value), - this.hasVariable - ); + return Formula.neg(this); } - - public negate(this: InvertibleFormula): InvertibleFormula; - public negate(this: Formula): Formula; public negate() { - return this.neg(); + return Formula.neg(this); } - - public negated(this: InvertibleFormula): InvertibleFormula; - public negated(this: Formula): Formula; public negated() { - return this.neg(); + return Formula.neg(this); } public sign() { - return new Formula(() => new Decimal(this.evaluate().sign)); + return Formula.sign(this); } - public sgn() { return this.sign(); } public round() { - return new Formula(() => this.evaluate().round()); + return Formula.round(this); } public floor() { - return new Formula(() => this.evaluate().floor()); + return Formula.floor(this); } public ceil() { - return new Formula(() => this.evaluate().ceil()); + return Formula.ceil(this); } public trunc() { - return new Formula(() => this.evaluate().trunc()); + return Formula.trunc(this); } - public add(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public add(this: Formula, value: FormulaSource): Formula; public add(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().add(unrefFormulaSource(v)), - invertible - ? value => - Decimal.sub( - value, - isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() - ) - : undefined, - hasVariable - ); + return Formula.add(this, value); } - - public plus(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public plus(this: Formula, value: FormulaSource): Formula; public plus(value: FormulaSource) { - return this.add(value); + return Formula.add(this, value); } - public sub(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public sub(this: Formula, value: FormulaSource): Formula; public sub(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().sub(unrefFormulaSource(v)), - invertible - ? isVariableFormula(this) - ? value => Decimal.add(value, unrefFormulaSource(v)) - : value => Decimal.sub(this.evaluate(), value) - : undefined, - hasVariable - ); + return Formula.sub(this, value); } - - public subtract(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public subtract(this: Formula, value: FormulaSource): Formula; public subtract(value: FormulaSource) { - return this.sub(value); + return Formula.sub(this, value); } - - public minus(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public minus(this: Formula, value: FormulaSource): Formula; public minus(value: FormulaSource) { - return this.sub(value); + return Formula.sub(this, value); } - public mul(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public mul(this: Formula, value: FormulaSource): Formula; public mul(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().mul(unrefFormulaSource(v)), - invertible - ? value => - Decimal.div( - value, - isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() - ) - : undefined, - hasVariable - ); + return Formula.mul(this, value); } - - public multiply(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public multiply(this: Formula, value: FormulaSource): Formula; public multiply(value: FormulaSource) { - return this.mul(value); + return Formula.mul(this, value); } - - public times(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public times(this: Formula, value: FormulaSource): Formula; public times(value: FormulaSource) { - return this.mul(value); + return Formula.mul(this, value); } - public div(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public div(this: Formula, value: FormulaSource): Formula; public div(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().div(unrefFormulaSource(v)), - invertible - ? value => - Decimal.mul( - value, - isVariableFormula(this) ? unrefFormulaSource(v) : this.evaluate() - ) - : undefined, - hasVariable - ); + return Formula.div(this, value); } - - public divide(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public divide(this: Formula, value: FormulaSource): Formula; public divide(value: FormulaSource) { - return this.div(value); + return Formula.div(this, value); } - - public divideBy(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public divideBy(this: Formula, value: FormulaSource): Formula; public divideBy(value: FormulaSource) { - return this.div(value); + return Formula.div(this, value); } - - public dividedBy(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public dividedBy(this: Formula, value: FormulaSource): Formula; public dividedBy(value: FormulaSource) { - return this.div(value); + return Formula.div(this, value); } - public recip(this: InvertibleFormula): InvertibleFormula; - public recip(this: Formula): Formula; public recip() { - return new Formula( - () => this.evaluate().recip(), - this.invertible ? value => Decimal.recip(value) : undefined, - this.hasVariable - ); + return Formula.recip(this); } - - public reciprocal(this: InvertibleFormula): InvertibleFormula; - public reciprocal(this: Formula): Formula; public reciprocal() { - return this.recip(); + return Formula.recip(this); } - - public reciprocate(this: InvertibleFormula): InvertibleFormula; - public reciprocate(this: Formula): Formula; public reciprocate() { - return this.recip(); + return Formula.recip(this); } public max(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().max(unrefFormulaSource(v))); + return Formula.max(this, value); } public min(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().min(unrefFormulaSource(v))); + return Formula.min(this, value); } public maxabs(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().maxabs(unrefFormulaSource(v))); + return Formula.maxabs(this, value); } public minabs(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().minabs(unrefFormulaSource(v))); + return Formula.minabs(this, value); } public clamp(min: FormulaSource, max: FormulaSource) { - const minValue = processFormulaSource(min); - const maxValue = processFormulaSource(max); - return new Formula(() => - this.evaluate().clamp(unrefFormulaSource(minValue), unrefFormulaSource(maxValue)) - ); + return Formula.clamp(this, min, max); } public clampMin(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().clampMin(unrefFormulaSource(v))); + return Formula.clampMin(this, value); } public clampMax(value: FormulaSource) { - const v = processFormulaSource(value); - return new Formula(() => this.evaluate().clampMax(unrefFormulaSource(v))); + return Formula.clampMax(this, value); } - public pLog10(this: InvertibleFormula): InvertibleFormula; - public pLog10(this: Formula): Formula; public pLog10() { - return new Formula(() => this.evaluate().pLog10()); + return Formula.pLog10(this); } - public absLog10(this: InvertibleFormula): InvertibleFormula; - public absLog10(this: Formula): Formula; public absLog10() { - return new Formula(() => this.evaluate().absLog10()); + return Formula.absLog10(this); } - public log10(this: InvertibleFormula): InvertibleFormula; - public log10(this: Formula): Formula; public log10() { - return new Formula( - () => this.evaluate().log10(), - this.invertible ? value => Decimal.pow10(value) : undefined, - this.hasVariable - ); + return Formula.log10(this); } - public log(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public log(this: Formula, value: FormulaSource): Formula; public log(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().log(unrefFormulaSource(v)), - invertible - ? value => - isVariableFormula(this) - ? Decimal.pow(unrefFormulaSource(v), value) - : Decimal.root(this.evaluate(), value) - : undefined, - hasVariable - ); + return Formula.log(this, value); } - - public logarithm(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public logarithm(this: Formula, value: FormulaSource): Formula; public logarithm(value: FormulaSource) { - return this.log(value); + return Formula.log(this, value); } - public log2(this: InvertibleFormula): InvertibleFormula; - public log2(this: Formula): Formula; public log2() { - return new Formula( - () => this.evaluate().log2(), - this.invertible ? value => Decimal.pow(2, value) : undefined, - this.hasVariable - ); + return Formula.log2(this); } - public ln(this: InvertibleFormula): InvertibleFormula; - public ln(this: Formula): Formula; public ln() { - return new Formula( - () => this.evaluate().ln(), - this.invertible ? value => Decimal.exp(value) : undefined, - this.hasVariable - ); + return Formula.ln(this); } - public pow(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public pow(this: Formula, value: FormulaSource): Formula; public pow(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().pow(unrefFormulaSource(v)), - invertible - ? value => - isVariableFormula(this) - ? Decimal.root(value, unrefFormulaSource(v)) - : Decimal.ln(value).div(Decimal.ln(this.evaluate())) - : undefined, - hasVariable - ); + return Formula.pow(this, value); } - public pow10(this: InvertibleFormula): InvertibleFormula; - public pow10(this: Formula): Formula; public pow10() { - return new Formula( - () => this.evaluate().pow10(), - this.invertible ? value => Decimal.root(value, 10) : undefined, - this.hasVariable - ); + return Formula.pow10(this); } - public pow_base(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public pow_base(this: Formula, value: FormulaSource): Formula; public pow_base(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().pow_base(unrefFormulaSource(v)), - invertible - ? value => - isVariableFormula(this) - ? Decimal.ln(value).div(unrefFormulaSource(v)) - : Decimal.root(unrefFormulaSource(v), value) - : undefined, - hasVariable - ); + return Formula.pow_base(this, value); } - public root(this: InvertibleFormula, value: FormulaSource): InvertibleFormula; - public root(this: Formula, value: FormulaSource): Formula; public root(value: FormulaSource) { - const v = processFormulaSource(value); - const { invertible, hasVariable } = calculateInvertibility(this, value); - return new Formula( - () => this.evaluate().root(unrefFormulaSource(v)), - invertible - ? value => - isVariableFormula(this) - ? Decimal.root(value, Decimal.recip(unrefFormulaSource(v))) - : Decimal.ln(value).div(Decimal.ln(this.evaluate()).recip()) - : undefined, - hasVariable - ); + return Formula.root(this, value); } public factorial() { - return new Formula(() => this.evaluate().factorial()); + return Formula.factorial(this); } public gamma() { - return new Formula(() => this.evaluate().gamma()); + return Formula.gamma(this); } public lngamma() { - return new Formula(() => this.evaluate().lngamma()); + return Formula.lngamma(this); } - public exp(this: InvertibleFormula): InvertibleFormula; - public exp(this: Formula): Formula; public exp() { - return new Formula( - () => this.evaluate().exp(), - this.invertible ? value => Decimal.ln(value) : undefined, - this.hasVariable - ); + return Formula.exp(this); } - public sqr(this: InvertibleFormula): InvertibleFormula; - public sqr(this: Formula): Formula; public sqr() { - return this.pow(2); + return Formula.pow(this, 2); } - public sqrt(this: InvertibleFormula): InvertibleFormula; - public sqrt(this: Formula): Formula; public sqrt() { - return this.root(2); + return Formula.root(this, 2); } - - public cube(this: InvertibleFormula): InvertibleFormula; - public cube(this: Formula): Formula; public cube() { - return this.pow(3); + return Formula.pow(this, 3); } - public cbrt(this: InvertibleFormula): InvertibleFormula; - public cbrt(this: Formula): Formula; public cbrt() { - return this.pow(1 / 3); + return Formula.root(this, 3); } - public tetrate( - this: InvertibleFormula, - height?: FormulaSource, - payload?: FormulaSource - ): InvertibleFormula; - public tetrate(this: Formula, height?: FormulaSource, payload?: FormulaSource): Formula; public tetrate( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - const heightValue = processFormulaSource(height); - const payloadValue = processFormulaSource(payload); - const { invertible, hasVariable } = calculateInvertibility(this, height, payload); - return new Formula( - () => - this.evaluate().tetrate( - Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), - unrefFormulaSource(payloadValue) - ), - invertible - ? value => - Decimal.slog( - value, - Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() - ) - : undefined, - hasVariable - ); + return Formula.tetrate(this, height, payload); } - public iteratedexp( - this: InvertibleFormula, - height?: FormulaSource, - payload?: FormulaSource - ): InvertibleFormula; - public iteratedexp(this: Formula, height?: FormulaSource, payload?: FormulaSource): Formula; public iteratedexp( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - const heightValue = processFormulaSource(height); - const payloadValue = processFormulaSource(payload); - const { invertible, hasVariable } = calculateInvertibility(this, height, payload); - return new Formula( - () => - this.evaluate().iteratedexp( - Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), - new Decimal(unrefFormulaSource(payloadValue)) - ), - invertible - ? value => - Decimal.iteratedlog( - value, - Math.E, - Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber() - ) - : undefined, - hasVariable - ); + return Formula.iteratedexp(this, height, payload); } public iteratedlog(base: FormulaSource = 10, times: FormulaSource = 1) { - const baseValue = processFormulaSource(base); - const timesValue = processFormulaSource(times); - return new Formula(() => - this.evaluate().iteratedlog( - unrefFormulaSource(baseValue), - Decimal.min(1e308, unrefFormulaSource(timesValue)).toNumber() - ) - ); + return Formula.iteratedlog(this, base, times); } public slog(base: FormulaSource = 10) { - const baseValue = processFormulaSource(base); - const { invertible, hasVariable } = calculateInvertibility(this, base); - return new Formula( - () => - this.evaluate().slog(Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber()), - invertible - ? value => - Decimal.tetrate( - value, - Decimal.min(1e308, unrefFormulaSource(baseValue)).toNumber() - ) - : undefined, - hasVariable - ); + return Formula.slog(this, base); } public layeradd10(diff: FormulaSource) { - const diffValue = processFormulaSource(diff); - return new Formula(() => this.evaluate().layeradd10(unrefFormulaSource(diffValue))); + return Formula.layeradd10(this, diff); } - public layeradd( - this: InvertibleFormula, - diff: FormulaSource, - base: FormulaSource - ): InvertibleFormula; - public layeradd(this: Formula, diff: FormulaSource, base: FormulaSource): Formula; public layeradd(diff: FormulaSource, base: FormulaSource) { - const diffValue = processFormulaSource(diff); - const baseValue = processFormulaSource(base); - const { invertible, hasVariable } = calculateInvertibility(this, diff, base); - return new Formula( - () => - this.evaluate().layeradd( - Decimal.min(1e308, unrefFormulaSource(diffValue)).toNumber(), - unrefFormulaSource(baseValue) - ), - invertible - ? value => - Decimal.layeradd( - value, - Decimal.min(1e308, unrefFormulaSource(diffValue)).negate().toNumber(), - unrefFormulaSource(baseValue) - ) - : undefined, - hasVariable - ); + return Formula.layeradd(this, diff, base); } - public lambertw(this: InvertibleFormula): InvertibleFormula; - public lambertw(this: Formula): Formula; public lambertw() { - return new Formula( - () => this.evaluate().lambertw(), - this.invertible ? value => Decimal.pow(Math.E, value).times(value) : undefined, - this.hasVariable - ); + return Formula.lambertw(this); } - public ssqrt(this: InvertibleFormula): InvertibleFormula; - public ssqrt(this: Formula): Formula; public ssqrt() { - return new Formula( - () => this.evaluate().ssqrt(), - this.invertible ? value => Decimal.tetrate(value, 2) : undefined, - this.hasVariable - ); + return Formula.ssqrt(this); } public pentate( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - const heightValue = processFormulaSource(height); - const payloadValue = processFormulaSource(payload); - return new Formula(() => - this.evaluate().pentate( - Decimal.min(1e308, unrefFormulaSource(heightValue)).toNumber(), - unrefFormulaSource(payloadValue) - ) - ); + return Formula.pentate(this, height, payload); } - public sin(this: InvertibleFormula): InvertibleFormula; - public sin(this: Formula): Formula; public sin() { - return new Formula( - () => this.evaluate().sin(), - this.invertible ? value => Decimal.asin(value) : undefined, - this.hasVariable - ); + return Formula.sin(this); } - public cos(this: InvertibleFormula): InvertibleFormula; - public cos(this: Formula): Formula; public cos() { - return new Formula( - () => this.evaluate().cos(), - this.invertible ? value => Decimal.acos(value) : undefined, - this.hasVariable - ); + return Formula.cos(this); } - public tan(this: InvertibleFormula): InvertibleFormula; - public tan(this: Formula): Formula; public tan() { - return new Formula( - () => this.evaluate().tan(), - this.invertible ? value => Decimal.atan(value) : undefined, - this.hasVariable - ); + return Formula.tan(this); } - public asin(this: InvertibleFormula): InvertibleFormula; - public asin(this: Formula): Formula; public asin() { - return new Formula( - () => this.evaluate().asin(), - this.invertible ? value => Decimal.sin(value) : undefined, - this.hasVariable - ); + return Formula.asin(this); } - public acos(this: InvertibleFormula): InvertibleFormula; - public acos(this: Formula): Formula; public acos() { - return new Formula( - () => this.evaluate().acos(), - this.invertible ? value => Decimal.cos(value) : undefined, - this.hasVariable - ); + return Formula.acos(this); } - public atan(this: InvertibleFormula): InvertibleFormula; - public atan(this: Formula): Formula; public atan() { - return new Formula( - () => this.evaluate().atan(), - this.invertible ? value => Decimal.tan(value) : undefined, - this.hasVariable - ); + return Formula.atan(this); } - public sinh(this: InvertibleFormula): InvertibleFormula; - public sinh(this: Formula): Formula; public sinh() { - return new Formula( - () => this.evaluate().sinh(), - this.invertible ? value => Decimal.asinh(value) : undefined, - this.hasVariable - ); + return Formula.sinh(this); } - public cosh(this: InvertibleFormula): InvertibleFormula; - public cosh(this: Formula): Formula; public cosh() { - return new Formula( - () => this.evaluate().cosh(), - this.invertible ? value => Decimal.acosh(value) : undefined, - this.hasVariable - ); + return Formula.cosh(this); } - public tanh(this: InvertibleFormula): InvertibleFormula; - public tanh(this: Formula): Formula; public tanh() { - return new Formula( - () => this.evaluate().tanh(), - this.invertible ? value => Decimal.atanh(value) : undefined, - this.hasVariable - ); + return Formula.tanh(this); } - public asinh(this: InvertibleFormula): InvertibleFormula; - public asinh(this: Formula): Formula; public asinh() { - return new Formula( - () => this.evaluate().asinh(), - this.invertible ? value => Decimal.sinh(value) : undefined, - this.hasVariable - ); + return Formula.asinh(this); } - public acosh(this: InvertibleFormula): InvertibleFormula; - public acosh(this: Formula): Formula; public acosh() { - return new Formula( - () => this.evaluate().acosh(), - this.invertible ? value => Decimal.cosh(value) : undefined, - this.hasVariable - ); + return Formula.acosh(this); } - public atanh(this: InvertibleFormula): InvertibleFormula; - public atanh(this: Formula): Formula; public atanh() { - return new Formula( - () => this.evaluate().atanh(), - this.invertible ? value => Decimal.tanh(value) : undefined, - this.hasVariable - ); + return Formula.atanh(this); } } diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts index a1d957c..8d39f22 100644 --- a/src/lib/break_eternity.ts +++ b/src/lib/break_eternity.ts @@ -802,6 +802,10 @@ export default class Decimal { return D(value).pow10(); } + public static pow_base(value: DecimalSource, other: DecimalSource): Decimal { + return D(value).pow_base(other); + } + public static root(value: DecimalSource, other: DecimalSource): Decimal { return D(value).root(other); } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 530bfa3..484bd34 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,20 +1,24 @@ -import Formula, { FormulaSource, InvertibleFormula } from "game/formulas"; +import Formula, { GenericFormula, InvertibleFormula, unrefFormulaSource } from "game/formulas"; import Decimal, { DecimalSource, format } from "util/bignum"; -import { beforeAll, describe, expect, test, vi } from "vitest"; +import { beforeAll, describe, expect, test } from "vitest"; import { ref } from "vue"; -type FormulaFunctions = keyof Formula & keyof typeof Formula & keyof typeof Decimal; - -interface FixedLengthArray<T, L extends number> extends ArrayLike<T> { - length: L; -} +type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; expect.extend({ compare_tolerance(received, expected) { const { isNot } = this; + let pass = false; + if (!Decimal.isFinite(expected)) { + pass = !Decimal.isFinite(received); + } else if (Decimal.isNaN(expected)) { + pass = Decimal.isNaN(received); + } else { + pass = Decimal.eq_tolerance(received, expected); + } return { // do not alter your "pass" based on isNot. Vitest does it for you - pass: Decimal.eq_tolerance(received, expected), + pass, message: () => `Expected ${received} to${ (isNot as boolean) ? " not" : "" @@ -45,113 +49,93 @@ function testConstant( expectedValue: DecimalSource = 10 ) { describe(desc, () => { - let formula: Formula; + let formula: GenericFormula; beforeAll(() => { formula = formulaFunc(); }); - test("evaluates correctly", async () => + test("evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(expectedValue)); - test("invert is pass-through", async () => - expect(formula.invert(25)).compare_tolerance(25)); - test("is invertible", async () => expect(formula.invertible).toBe(true)); - test("is not marked as having a variable", async () => - expect(formula.hasVariable).toBe(false)); + test("invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); + test("is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); }); } +function testFormula<T extends FormulaFunctions>( + functionName: T, + args: Readonly<Parameters<typeof Formula[T]>>, + invertible = true +) { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[functionName](...args); + }); + test("Formula is not marked as having a variable", () => + expect(formula.hasVariable()).toBe(false)); + test(`Formula is${invertible ? "" : " not"} invertible`, () => + expect(formula.isInvertible()).toBe(invertible)); + if (invertible) { + test(`Formula throws if inverting without any variables`, () => + expect(() => formula.invert(10)).toThrow()); + } +} + // Utility function that will test all the different // It's a lot of tests, but I'd rather be exhaustive -function testFormula<T extends FormulaFunctions>( +function testFormulaCall<T extends FormulaFunctions>( functionName: T, - args: Readonly<FixedLengthArray<number, Parameters<typeof Formula[T]>["length"]>>, - invertible = true + args: Readonly<Parameters<typeof Formula[T]>> ) { - let value: Decimal; - - beforeAll(() => { - value = testValueFormulas[args[0]].evaluate(); - }); - let testName = functionName + "("; for (let i = 0; i < args.length; i++) { if (i !== 0) { testName += ", "; } - testName += testValues[args[i]]; + testName += args[i]; } - testName += ")"; - describe(testName, () => { - let expectedEvaluation: Decimal | undefined; - const formulaArgs: Formula[] = []; - let staticFormula: Formula; - let instanceFormula: Formula; - beforeAll(() => { - for (let i = 0; i < args.length; i++) { - formulaArgs.push(testValueFormulas[args[i]]); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - staticFormula = Formula[functionName](...formulaArgs); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - instanceFormula = formulaArgs[0][functionName](...formulaArgs.slice(1)); + testName += ") evaluates correctly"; + test(testName, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const formula = Formula[functionName](...args); - try { + try { + const expectedEvaluation = Decimal[functionName]( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - expectedEvaluation = Decimal[functionName](...args); - } catch { - // If this is an invalid Decimal operation, then ignore this test case - return; + ...args.map(i => unrefFormulaSource(i)) + ); + if (expectedEvaluation != null) { + expect(formula.evaluate()).compare_tolerance(expectedEvaluation); } - }); - - test("Static formula is not marked as having a variable", async () => - expect(staticFormula.hasVariable).toBe(false)); - test("Static function evaluates correctly", async () => - expectedEvaluation != null && - expect(staticFormula.evaluate()).compare_tolerance(expectedEvaluation)); - test("Static function invertible", async () => - expect(staticFormula.invertible).toBe(invertible)); - if (invertible) { - test("Static function inverts correctly", async () => - expectedEvaluation != null && - !Decimal.isNaN(expectedEvaluation) && - expect(staticFormula.invert(expectedEvaluation)).compare_tolerance(value)); - } - - // Do those tests again but for non-static methods - test("Instance formula is not marked as having a variable", async () => - expect(instanceFormula.hasVariable).toBe(false)); - test("Instance function evaluates correctly", async () => - expectedEvaluation != null && - expect(instanceFormula.evaluate()).compare_tolerance(expectedEvaluation)); - test("Instance function invertible", async () => - expect(instanceFormula.invertible).toBe(invertible)); - if (invertible) { - test("Instance function inverts correctly", async () => - expectedEvaluation != null && - !Decimal.isNaN(expectedEvaluation) && - expect(instanceFormula.invert(expectedEvaluation)).compare_tolerance(value)); + } catch { + // If this is an invalid Decimal operation, then ignore this test case } }); } -function testAliases<T extends FormulaFunctions[]>( - formula: Formula, - aliases: T, - args: FormulaSource[] +function testAliases<T extends FormulaFunctions>( + aliases: T[], + args: Parameters<typeof Formula[T]> ) { - const spy = vi.spyOn(formula, aliases[0]); - expect(spy).not.toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - aliases.slice(1).forEach(name => formula[name](...args)); - expect(spy).toHaveBeenCalledTimes(aliases.length - 1); + describe(aliases[0], () => { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[aliases[0]](...args); + }); + + aliases.slice(1).forEach(alias => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true)); + }); + }); } -const testValues = [-2.5, -1, -0.1, 0, 0.1, 1, 2.5] as const; -let testValueFormulas: InvertibleFormula[] = []; +const testValues = ["-1e400", 0, 0.25] as const; const invertibleZeroParamFunctionNames = [ "neg", @@ -220,11 +204,64 @@ const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] a const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; -describe.concurrent("Creating Formulas", () => { - beforeAll(() => { - testValueFormulas = testValues.map(v => Formula.constant(v)); +describe("Formula Equality Checking", () => { + describe("Equality Checks", () => { + test("Equals", () => Formula.add(1, 1).equals(Formula.add(1, 1))); + test("Not Equals due to inputs", () => Formula.add(1, 1).equals(Formula.add(1, 0))); + test("Not Equals due to functions", () => Formula.add(1, 1).equals(Formula.sub(1, 1))); + test("Not Equals due to hasVariable", () => + Formula.constant(1).equals(Formula.variable(1))); }); + describe("Formula aliases", () => { + testAliases(["neg", "negate", "negated"], [1]); + testAliases(["recip", "reciprocal", "reciprocate"], [1]); + testAliases(["sign", "sgn"], [1]); + testAliases(["add", "plus"], [1, 1]); + testAliases(["sub", "subtract", "minus"], [1, 1]); + testAliases(["mul", "multiply", "times"], [1, 1]); + testAliases(["div", "divide"], [1, 1]); + testAliases(["log", "logarithm"], [1, 1]); + }); + + describe("Instance vs Static methods", () => { + let formula: GenericFormula; + beforeAll(() => { + formula = Formula.constant(10); + }); + [...invertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames].forEach( + name => { + test(name, () => { + const instanceFormula = formula[name](); + const staticFormula = Formula[name](formula); + expect(instanceFormula.equals(staticFormula)).toBe(true); + }); + } + ); + + [...invertibleOneParamFunctionNames, ...nonInvertibleOneParamFunctionNames].forEach( + name => { + test(name, () => { + const instanceFormula = formula[name](10); + const staticFormula = Formula[name](formula, 10); + expect(instanceFormula.equals(staticFormula)).toBe(true); + }); + } + ); + + [...invertibleTwoParamFunctionNames, ...nonInvertibleTwoParamFunctionNames].forEach( + name => { + test(name, () => { + const instanceFormula = formula[name](1, 1); + const staticFormula = Formula[name](formula, 1, 1); + expect(instanceFormula.equals(staticFormula)).toBe(true); + }); + } + ); + }); +}); + +describe("Creating Formulas", () => { describe("Constants", () => { testConstant("number", () => Formula.constant(10)); testConstant("string", () => Formula.constant("10")); @@ -233,101 +270,91 @@ describe.concurrent("Creating Formulas", () => { testConstant("ref", () => Formula.constant(ref(10))); }); - // Test that these are just pass-throughts so we don't need to test each one everywhere else - describe("Function aliases", () => { - let formula: Formula; - beforeAll(() => { - formula = Formula.constant(10); - }); - test("neg", async () => testAliases(formula, ["neg", "negate", "negated"], [0])); - test("recip", async () => - testAliases(formula, ["recip", "reciprocal", "reciprocate"], [0])); - test("sign", async () => testAliases(formula, ["sign", "sgn"], [0])); - test("add", async () => testAliases(formula, ["add", "plus"], [0])); - test("sub", async () => testAliases(formula, ["sub", "subtract", "minus"], [0])); - test("mul", async () => testAliases(formula, ["mul", "multiply", "times"], [0])); - test("div", async () => testAliases(formula, ["div", "divide"], [1])); - test("log", async () => testAliases(formula, ["log", "logarithm"], [0])); - }); - describe("Invertible 0-param", () => { - invertibleZeroParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - testFormula(names, [i] as const); - } - }); + invertibleZeroParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0] as const); + testValues.forEach(i => testFormulaCall(names, [i] as const)); + }) + ); }); describe("Non-Invertible 0-param", () => { - nonInvertibleZeroParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - testFormula(names, [i] as const, false); - } - }); + nonInvertibleZeroParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0] as const, false); + testValues.forEach(i => testFormulaCall(names, [i] as const)); + }) + ); }); describe("Invertible 1-param", () => { - invertibleOneParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - for (let j = 0; j < testValues.length; j++) { - testFormula(names, [i, j] as const); - } - } - }); + invertibleOneParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0, 0] as const); + testValues.forEach(i => + testValues.forEach(j => testFormulaCall(names, [i, j] as const)) + ); + }) + ); }); describe("Non-Invertible 1-param", () => { - nonInvertibleOneParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - for (let j = 0; j < testValues.length; j++) { - testFormula(names, [i, j] as const, false); - } - } - }); + nonInvertibleOneParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0, 0] as const, false); + testValues.forEach(i => + testValues.forEach(j => testFormulaCall(names, [i, j] as const)) + ); + }) + ); }); describe("Invertible 2-param", () => { - invertibleTwoParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - for (let j = 0; j < testValues.length; j++) { - for (let k = 0; k < testValues.length; k++) { - testFormula(names, [i, j, k] as const); - } - } - } - }); + invertibleTwoParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0, 0, 0] as const); + testValues.forEach(i => + testValues.forEach(j => + testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) + ) + ); + }) + ); }); describe("Non-Invertible 2-param", () => { - nonInvertibleTwoParamFunctionNames.forEach(names => { - for (let i = 0; i < testValues.length; i++) { - for (let j = 0; j < testValues.length; j++) { - for (let k = 0; k < testValues.length; k++) { - testFormula(names, [i, j, k] as const, false); - } - } - } - }); + nonInvertibleTwoParamFunctionNames.forEach(names => + describe(names, () => { + testFormula(names, [0, 0, 0] as const, false); + testValues.forEach(i => + testValues.forEach(j => + testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) + ) + ); + }) + ); }); }); describe("Variables", () => { - let variable: Formula; - let constant: Formula; + let variable: GenericFormula; + let constant: GenericFormula; beforeAll(() => { variable = Formula.variable(10); constant = Formula.constant(10); }); - test("Created variable is marked as a variable", () => expect(variable.hasVariable).toBe(true)); + test("Created variable is marked as a variable", () => + expect(variable.hasVariable()).toBe(true)); test("Evaluate() returns variable's value", () => expect(variable.evaluate()).compare_tolerance(10)); test("Invert() is pass-through", () => expect(variable.invert(100)).compare_tolerance(100)); test("Nested variable is marked as having a variable", () => - expect(variable.add(10).div(3).pow(2).hasVariable).toBe(true)); + expect(variable.add(10).div(3).pow(2).hasVariable()).toBe(true)); test("Nested non-variable is marked as not having a variable", () => - expect(constant.add(10).div(3).pow(2).hasVariable).toBe(false)); + expect(constant.add(10).div(3).pow(2).hasVariable()).toBe(false)); describe("Invertible Formulas correctly calculate when they contain a variable", () => { - function checkFormula(formula: Formula, expectedBool = true) { - expect(formula.invertible).toBe(expectedBool); - expect(formula.hasVariable).toBe(expectedBool); + function checkFormula(formula: GenericFormula, expectedBool = true) { + expect(formula.isInvertible()).toBe(expectedBool); + expect(formula.hasVariable()).toBe(expectedBool); } invertibleZeroParamFunctionNames.forEach(name => { describe(name, () => { @@ -366,9 +393,9 @@ describe("Variables", () => { }); describe("Non-Invertible Formulas never marked as having a variable", () => { - function checkFormula(formula: Formula) { - expect(formula.invertible).toBe(false); - expect(formula.hasVariable).toBe(false); + function checkFormula(formula: GenericFormula) { + expect(formula.isInvertible()).toBe(false); + expect(formula.hasVariable()).toBe(false); } nonInvertibleZeroParamFunctionNames.forEach(name => { describe(name, () => { @@ -407,8 +434,8 @@ describe("Variables", () => { }); describe("Inverting calculates the value of the variable", () => { - let variable: Formula; - let constant: Formula; + let variable: GenericFormula; + let constant: GenericFormula; beforeAll(() => { variable = Formula.variable(2); constant = Formula.constant(3); From 675b30fdd0a011c18aee93d61a956e91d54dcb8c Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 19 Jan 2023 07:45:33 -0600 Subject: [PATCH 14/56] Implement step-wise functions --- src/game/formulas.ts | 40 +++++++++++++++++++++++ tests/game/formulas.test.ts | 63 ++++++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index e4d5af8..7453c05 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -432,6 +432,46 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + /** + * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier. + * This function assumes the incoming {@ref value} will be continuous and monotonically increasing. + * @param value The value before applying the step + * @param start The value at which to start applying the step + * @param formulaModifier How this step should modify the formula. The incoming value will be the unmodified formula value _minus the start value_. So for example if an incoming formula evaluates to 200 and has a step that starts at 150, the formulaModifier would be given 50 as the parameter + */ + public static step<T extends FormulaSource>( + value: T, + start: ProcessedComputable<DecimalSource>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + const lhsRef = ref<DecimalSource>(0); + const formula = formulaModifier(lhsRef); + function evalStep(lhs: DecimalSource) { + if (Decimal.lt(lhs, unref(start))) { + return lhs; + } + lhsRef.value = Decimal.sub(lhs, unref(start)); + return Decimal.add(formula.evaluate(), unref(start)); + } + function invertStep(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + if (Decimal.gt(value, unref(start))) { + value = Decimal.add( + formula.invert(Decimal.sub(value, unref(start))), + unref(start) + ); + } + return lhs.invert(value); + } + throw "Could not invert due to no input being a variable"; + } + return new Formula( + [value], + evalStep, + formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + ); + } + public static constant(value: InvertibleFormulaSource): InvertibleFormula { return new Formula([value]) as InvertibleFormula; } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 484bd34..cb5f4d4 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,7 +1,7 @@ import Formula, { GenericFormula, InvertibleFormula, unrefFormulaSource } from "game/formulas"; import Decimal, { DecimalSource, format } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { ref } from "vue"; +import { Ref, ref } from "vue"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -53,10 +53,10 @@ function testConstant( beforeAll(() => { formula = formulaFunc(); }); - test("evaluates correctly", () => + test("Evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(expectedValue)); - test("invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); - test("is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); + test("Invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); + test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); }); } @@ -475,3 +475,58 @@ describe("Variables", () => { ); }); }); + +describe("Step-wise", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("Formula without variable is marked as such", () => { + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).hasVariable()).toBe(false); + }); + + test("Formula with variable is marked as such", () => { + expect(Formula.step(variable, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(variable, 10, value => Formula.sqrt(value)).hasVariable()).toBe(true); + }); + + test("Non-invertible formula modifier marks formula as such", () => { + expect(Formula.step(constant, 10, value => Formula.abs(value)).isInvertible()).toBe(false); + expect(Formula.step(constant, 10, value => Formula.abs(value)).hasVariable()).toBe(false); + }); + + test("Formula modifiers with variables mark formula as non-invertible", () => { + expect( + Formula.step(constant, 10, value => Formula.add(value, variable)).isInvertible() + ).toBe(false); + expect( + Formula.step(constant, 10, value => Formula.add(value, variable)).hasVariable() + ).toBe(false); + }); + + describe("Pass-through underneath start", () => { + test("Evaluates correctly", () => + expect( + Formula.step(constant, 20, value => Formula.sqrt(value)).evaluate() + ).compare_tolerance(10)); + test("Inverts correctly with variable in input", () => + expect( + Formula.step(variable, 20, value => Formula.sqrt(value)).invert(10) + ).compare_tolerance(10)); + }); + + describe("Evaluates correctly beyond start", () => { + test("Evaluates correctly", () => + expect( + Formula.step(variable, 8, value => Formula.add(value, 2)).evaluate() + ).compare_tolerance(12)); + test("Inverts correctly", () => + expect( + Formula.step(variable, 8, value => Formula.add(value, 2)).invert(12) + ).compare_tolerance(10)); + }); +}); From 30aec8a93cb86b9d5fad23cff4a084f13fdebf1c Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 19 Jan 2023 08:36:16 -0600 Subject: [PATCH 15/56] Implement conditional formulas --- src/game/formulas.ts | 61 +++++++++++++++++++++++++++++++------ tests/game/formulas.test.ts | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 7453c05..6b183e0 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,5 +1,5 @@ import Decimal, { DecimalSource } from "util/bignum"; -import { ProcessedComputable } from "util/computed"; +import { Computable, convertComputable, ProcessedComputable } from "util/computed"; import { ref, Ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -439,26 +439,27 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { * @param start The value at which to start applying the step * @param formulaModifier How this step should modify the formula. The incoming value will be the unmodified formula value _minus the start value_. So for example if an incoming formula evaluates to 200 and has a step that starts at 150, the formulaModifier would be given 50 as the parameter */ - public static step<T extends FormulaSource>( - value: T, - start: ProcessedComputable<DecimalSource>, + public static step( + value: FormulaSource, + start: Computable<DecimalSource>, formulaModifier: (value: Ref<DecimalSource>) => GenericFormula ) { const lhsRef = ref<DecimalSource>(0); const formula = formulaModifier(lhsRef); + const processedStart = convertComputable(start); function evalStep(lhs: DecimalSource) { - if (Decimal.lt(lhs, unref(start))) { + if (Decimal.lt(lhs, unref(processedStart))) { return lhs; } - lhsRef.value = Decimal.sub(lhs, unref(start)); - return Decimal.add(formula.evaluate(), unref(start)); + lhsRef.value = Decimal.sub(lhs, unref(processedStart)); + return Decimal.add(formula.evaluate(), unref(processedStart)); } function invertStep(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - if (Decimal.gt(value, unref(start))) { + if (Decimal.gt(value, unref(processedStart))) { value = Decimal.add( - formula.invert(Decimal.sub(value, unref(start))), - unref(start) + formula.invert(Decimal.sub(value, unref(processedStart))), + unref(processedStart) ); } return lhs.invert(value); @@ -472,6 +473,46 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + public static if( + value: FormulaSource, + condition: Computable<boolean>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + const lhsRef = ref<DecimalSource>(0); + const formula = formulaModifier(lhsRef); + const processedCondition = convertComputable(condition); + function evalStep(lhs: DecimalSource) { + if (unref(processedCondition)) { + lhsRef.value = lhs; + return formula.evaluate(); + } else { + return lhs; + } + } + function invertStep(value: DecimalSource, lhs: FormulaSource) { + if (!hasVariable(lhs)) { + throw "Could not invert due to no input being a variable"; + } + if (unref(processedCondition)) { + return lhs.invert(formula.invert(value)); + } else { + return lhs.invert(value); + } + } + return new Formula( + [value], + evalStep, + formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + ); + } + public static conditional( + value: FormulaSource, + condition: Computable<boolean>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + return Formula.if(value, condition, formulaModifier); + } + public static constant(value: InvertibleFormulaSource): InvertibleFormula { return new Formula([value]) as InvertibleFormula; } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index cb5f4d4..a998e26 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -530,3 +530,58 @@ describe("Step-wise", () => { ).compare_tolerance(10)); }); }); + +describe("Conditionals", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("Formula without variable is marked as such", () => { + expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.if(constant, true, value => Formula.sqrt(value)).hasVariable()).toBe(false); + }); + + test("Formula with variable is marked as such", () => { + expect(Formula.if(variable, true, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.if(variable, true, value => Formula.sqrt(value)).hasVariable()).toBe(true); + }); + + test("Non-invertible formula modifier marks formula as such", () => { + expect(Formula.if(constant, true, value => Formula.abs(value)).isInvertible()).toBe(false); + expect(Formula.if(constant, true, value => Formula.abs(value)).hasVariable()).toBe(false); + }); + + test("Formula modifiers with variables mark formula as non-invertible", () => { + expect( + Formula.if(constant, true, value => Formula.add(value, variable)).isInvertible() + ).toBe(false); + expect( + Formula.if(constant, true, value => Formula.add(value, variable)).hasVariable() + ).toBe(false); + }); + + describe("Pass-through with condition false", () => { + test("Evaluates correctly", () => + expect( + Formula.if(constant, false, value => Formula.sqrt(value)).evaluate() + ).compare_tolerance(10)); + test("Inverts correctly with variable in input", () => + expect( + Formula.if(variable, false, value => Formula.sqrt(value)).invert(10) + ).compare_tolerance(10)); + }); + + describe("Evaluates correctly with condition true", () => { + test("Evaluates correctly", () => + expect( + Formula.if(variable, true, value => Formula.add(value, 2)).evaluate() + ).compare_tolerance(12)); + test("Inverts correctly", () => + expect( + Formula.if(variable, true, value => Formula.add(value, 2)).invert(12) + ).compare_tolerance(10)); + }); +}); From 5293a2ba9286401bf2e453e21d71b47902cd611f Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 19 Jan 2023 23:26:46 -0600 Subject: [PATCH 16/56] Added tests for custom formulas --- tests/game/formulas.test.ts | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index a998e26..94ba563 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -585,3 +585,55 @@ describe("Conditionals", () => { ).compare_tolerance(10)); }); }); + +describe("Custom Formulas", () => { + describe("Formula with just one input", () => { + let formula: GenericFormula; + beforeAll(() => { + formula = new Formula([10]); + }); + test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); + test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); + test("Evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(10)); + test("Invert is pass-through", () => expect(formula.invert(20)).compare_tolerance(20)); + }); + + describe("Formula with non-one inputs without required other params", () => { + test("Zero inputs throws", () => expect(() => new Formula([])).toThrow()); + test("Two inputs throws", () => expect(() => new Formula([1, 2])).toThrow()); + test("Zero inputs and invert throws", () => + expect(() => new Formula([], undefined, value => value)).toThrow()); + test("Two inputs and invert throws", () => + expect(() => new Formula([1, 2], undefined, value => value)).toThrow()); + test("Zero inputs and evaluate and hasVariable throws", () => + expect(() => new Formula([], () => 10, undefined, true)).toThrow()); + test("Two inputs and evaluate and hasVariable throws", () => + expect(() => new Formula([1, 2], () => 10, undefined, true)).toThrow()); + }); + + describe("Formula with evaluate", () => { + test("Zero input evaluates correctly", () => + expect(new Formula([], () => 10).evaluate()).compare_tolerance(10)); + test("One input evaluates correctly", () => + expect(new Formula([1], value => value).evaluate()).compare_tolerance(1)); + test("Two inputs evaluates correctly", () => + expect(new Formula([1, 2], (v1, v2) => v1).evaluate()).compare_tolerance(1)); + }); + + describe("Formula with invert", () => { + test("Zero input inverts correctly", () => + expect(new Formula([], undefined, value => value).invert(10)).compare_tolerance(10)); + test("One input inverts correctly", () => + expect(new Formula([1], undefined, (value, v1) => v1).invert(10)).compare_tolerance(1)); + test("Two inputs inverts correctly", () => + expect( + new Formula([1, 2], undefined, (value, v1, v2) => v2).invert(10) + ).compare_tolerance(2)); + }); + + test("Formula with hasVariable", () => { + const formula = new Formula([], undefined, value => value, true); + expect(formula.isInvertible()).toBe(true); + expect(formula.hasVariable()).toBe(true); + }); +}); From aebf318f830295c564dc197be2a5035b3cff72f1 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 4 Feb 2023 17:56:05 -0600 Subject: [PATCH 17/56] Lots of formula changes Many tests fail and inverting integrals is basically non-functional because it just chains stuff Will need to implement integration by parts and/or integration by substitution for that --- package.json | 2 +- src/game/formulas.ts | 1083 ++++++++++++++++++++++++++++++----- tests/game/formulas.test.ts | 681 +++++++++++++++++----- 3 files changed, 1475 insertions(+), 291 deletions(-) diff --git a/package.json b/package.json index 4b87061..18f336b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.26.3", + "vitest": "^0.27.3", "vue-tsc": "^0.38.1" }, "engines": { diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 6b183e0..7e893be 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,14 +1,45 @@ +import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { ref, Ref, unref } from "vue"; +import { computed, ref, Ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula<any>; export type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula; export type InvertibleFormulaSource = ProcessedComputable<DecimalSource> | InvertibleFormula; export type InvertibleFormula = GenericFormula & { - invert: (value: DecimalSource) => Decimal; + invert: (value: DecimalSource) => DecimalSource; }; +export type IntegrableFormula = GenericFormula & { + evaluateIntegral: (variable?: DecimalSource) => DecimalSource; +}; +export type InvertibleIntegralFormula = GenericFormula & { + invertIntegral: (value: DecimalSource) => DecimalSource; +}; + +export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> = + | { + variable: ProcessedComputable<DecimalSource>; + } + | { + inputs: [FormulaSource]; + } + | { + inputs: T; + evaluate: (this: Formula<T>, ...inputs: GuardedFormulasToDecimals<T>) => DecimalSource; + invert?: ( + this: Formula<T>, + value: DecimalSource, + ...inputs: [...T, ...unknown[]] + ) => DecimalSource; + integrate?: ( + this: Formula<T>, + variable: DecimalSource | undefined, + ...inputs: T + ) => DecimalSource; + invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource; + hasVariable?: boolean; + }; function hasVariable(value: FormulaSource): value is InvertibleFormula { return value instanceof Formula && value.hasVariable(); @@ -22,8 +53,8 @@ type FormulasToDecimals<T extends FormulaSource[]> = { type TupleGuard<T extends any[]> = T extends any[] ? FormulasToDecimals<T> : never; type GuardedFormulasToDecimals<T extends FormulaSource[]> = TupleGuard<T>; -export function unrefFormulaSource(value: FormulaSource) { - return value instanceof Formula ? value.evaluate() : unref(value); +export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) { + return value instanceof Formula ? value.evaluate(variable) : unref(value); } function invertNeg(value: DecimalSource, lhs: FormulaSource) { @@ -33,6 +64,10 @@ function invertNeg(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) { + return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg(); +} + function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs))); @@ -42,6 +77,32 @@ function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integrateAdd(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(rhs), x)); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(lhs), x)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); @@ -51,6 +112,30 @@ function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integrateSub(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(rhs), x).neg()); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); @@ -60,6 +145,28 @@ function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integrateMul(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); @@ -69,6 +176,28 @@ function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integrateDiv(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + function invertRecip(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.recip(value)); @@ -76,6 +205,21 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.ln(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(value)); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow10(value)); @@ -83,6 +227,23 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); @@ -92,6 +253,25 @@ function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integrateLog(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times( + x, + Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs))) + ); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(2, value)); @@ -99,6 +279,21 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(value)); @@ -106,6 +301,21 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.ln(x).sub(1)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); @@ -115,6 +325,30 @@ function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } +function integratePow(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + const pow = Decimal.add(unrefFormulaSource(rhs), 1); + return Decimal.pow(x, pow).div(pow); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + const b = unrefFormulaSource(lhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } else if (hasVariable(rhs)) { + const denominator = Decimal.ln(unrefFormulaSource(lhs)); + return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); + } + throw "Could not invert due to no input being a variable"; +} + function invertPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, 10)); @@ -122,6 +356,23 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); @@ -131,6 +382,32 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou throw "Could not invert due to no input being a variable"; } +function integratePowBase( + variable: DecimalSource | undefined, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs, variable); + return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b)); + } else if (hasVariable(rhs)) { + const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1); + return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); @@ -140,6 +417,33 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource throw "Could not invert due to no input being a variable"; } +function integrateRoot( + variable: DecimalSource | undefined, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1)) + .times(b) + .div(Decimal.add(b, 1)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert( + Decimal.add(b, 1) + .times(value) + .div(b) + .pow(Decimal.div(b, Decimal.add(b, 1))) + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertExp(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value)); @@ -147,6 +451,13 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.exp(unrefFormulaSource(lhs, variable)); + } + throw "Could not integrate due to no input being a variable"; +} + function tetrate( value: DecimalSource, height: DecimalSource = 2, @@ -268,6 +579,13 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.cos(unrefFormulaSource(lhs, variable)).neg(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertCos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acos(value)); @@ -275,6 +593,13 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.sin(unrefFormulaSource(lhs, variable)); + } + throw "Could not integrate due to no input being a variable"; +} + function invertTan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atan(value)); @@ -282,6 +607,13 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAsin(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sin(value)); @@ -289,6 +621,16 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.asin(x) + .times(x) + .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAcos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cos(value)); @@ -296,6 +638,16 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.acos(x) + .times(x) + .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAtan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tan(value)); @@ -303,6 +655,16 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.atan(x) + .times(x) + .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + function invertSinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.asinh(value)); @@ -310,6 +672,14 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.cosh(x); + } + throw "Could not integrate due to no input being a variable"; +} + function invertCosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acosh(value)); @@ -317,6 +687,14 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.sinh(x); + } + throw "Could not integrate due to no input being a variable"; +} + function invertTanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atanh(value)); @@ -324,6 +702,14 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.cosh(x).ln(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAsinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sinh(value)); @@ -331,6 +717,14 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAcosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cosh(value)); @@ -338,6 +732,16 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.acosh(x) + .times(x) + .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAtanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tanh(value)); @@ -345,6 +749,16 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.atanh(x) + .times(x) + .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + export default class Formula<T extends [FormulaSource] | FormulaSource[]> { readonly inputs: T; @@ -354,66 +768,114 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { private readonly internalInvert: | ((value: DecimalSource, ...inputs: T) => DecimalSource) | undefined; + private readonly internalIntegrate: + | ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource) + | undefined; + private readonly internalInvertIntegral: + | ((value: DecimalSource, ...inputs: T) => DecimalSource) + | undefined; private readonly internalHasVariable: boolean; - constructor( - inputs: T, - evaluate?: (this: Formula<T>, ...inputs: GuardedFormulasToDecimals<T>) => DecimalSource, - invert?: ( - this: Formula<T>, - value: DecimalSource, - ...inputs: [...T, ...unknown[]] - ) => DecimalSource, - hasVariable = false - ) { - if (inputs.length !== 1 && evaluate == null) { - throw "Evaluate function is required if inputs is not length 1"; + constructor(options: FormulaOptions<T>) { + // Variable case + if ("variable" in options) { + this.inputs = [options.variable] as T; + this.internalHasVariable = true; + return; } - if (inputs.length !== 1 && invert == null && hasVariable) { - throw "A formula cannot be marked as having a variable if it is not invertible and inputs is not length 1"; - } - - this.inputs = inputs; - this.internalEvaluate = evaluate; - - if ( - inputs.some(input => input instanceof Formula && !input.isInvertible()) || - (hasVariable === false && evaluate != null && invert == null) - ) { + // Constant case + if (!("evaluate" in options)) { + if (options.inputs.length !== 1) { + throw "Evaluate function is required if inputs is not length 1"; + } + this.inputs = options.inputs as T; this.internalHasVariable = false; return; } + const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options; + if (invert == null && invertIntegral == null && hasVariable) { + throw "A formula cannot be marked as having a variable if it is not invertible"; + } + + this.inputs = inputs; + this.internalEvaluate = evaluate; + this.internalIntegrate = integrate; + const numVariables = inputs.filter( input => input instanceof Formula && input.hasVariable() ).length; + const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as + | GenericFormula + | undefined; - // ??? + this.internalHasVariable = + numVariables === 1 || (numVariables === 0 && hasVariable === true); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - this.internalInvert = numVariables <= 1 ? invert : undefined; - this.internalHasVariable = numVariables === 1 || (numVariables === 0 && hasVariable); + this.internalInvert = + this.internalHasVariable && variable?.isInvertible() ? invert : undefined; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.internalInvertIntegral = + this.internalHasVariable && variable?.isIntegralInvertible() ? invert : undefined; } isInvertible(): this is InvertibleFormula { - return this.internalInvert != null || this.internalEvaluate == null; + return ( + this.internalHasVariable && + (this.internalInvert != null || this.internalEvaluate == null) + ); + } + + isIntegrable(): this is IntegrableFormula { + return this.internalHasVariable && this.internalIntegrate != null; + } + + isIntegralInvertible(): this is InvertibleIntegralFormula { + return this.internalHasVariable && this.internalInvertIntegral != null; } hasVariable(): boolean { return this.internalHasVariable; } - evaluate(): DecimalSource { + /** + * Evaluate the current result of the formula + * @param variable Optionally override the value of the variable while evaluating. Ignored if there is not variable + */ + evaluate(variable?: DecimalSource): DecimalSource { return ( this.internalEvaluate?.call( this, - ...(this.inputs.map(unrefFormulaSource) as GuardedFormulasToDecimals<T>) - ) ?? unrefFormulaSource(this.inputs[0]) + ...(this.inputs.map(input => + unrefFormulaSource(input, variable) + ) as GuardedFormulasToDecimals<T>) + ) ?? + variable ?? + unrefFormulaSource(this.inputs[0]) ); } invert(value: DecimalSource): DecimalSource { - return this.internalInvert?.call(this, value, ...this.inputs) ?? value; + if (this.internalInvert) { + return this.internalInvert.call(this, value, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return value; + } + throw "Cannot invert non-invertible formula"; + } + + evaluateIntegral(variable?: DecimalSource): DecimalSource { + return ( + this.internalIntegrate?.call(this, variable, ...this.inputs) ?? + variable ?? + unrefFormulaSource(this.inputs[0]) + ); + } + + invertIntegral(value: DecimalSource): DecimalSource { + return this.internalInvertIntegral?.call(this, value, ...this.inputs) ?? value; } equals(other: GenericFormula): boolean { @@ -428,10 +890,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ) && this.internalEvaluate === other.internalEvaluate && this.internalInvert === other.internalInvert && + this.internalIntegrate === other.internalIntegrate && + this.internalInvertIntegral === other.internalInvertIntegral && this.internalHasVariable === other.internalHasVariable ); } + public static constant( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { + return new Formula({ inputs: [value] }) as InvertibleFormula; + } + + public static variable( + value: ProcessedComputable<DecimalSource> + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { + return new Formula({ variable: value }) as InvertibleFormula; + } + /** * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier. * This function assumes the incoming {@ref value} will be continuous and monotonically increasing. @@ -443,7 +919,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { value: FormulaSource, start: Computable<DecimalSource>, formulaModifier: (value: Ref<DecimalSource>) => GenericFormula - ) { + ): GenericFormula { const lhsRef = ref<DecimalSource>(0); const formula = formulaModifier(lhsRef); const processedStart = convertComputable(start); @@ -466,18 +942,18 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } throw "Could not invert due to no input being a variable"; } - return new Formula( - [value], - evalStep, - formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined - ); + return new Formula({ + inputs: [value], + evaluate: evalStep, + invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + }); } public static if( value: FormulaSource, condition: Computable<boolean>, formulaModifier: (value: Ref<DecimalSource>) => GenericFormula - ) { + ): GenericFormula { const lhsRef = ref<DecimalSource>(0); const formula = formulaModifier(lhsRef); const processedCondition = convertComputable(condition); @@ -499,11 +975,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return lhs.invert(value); } } - return new Formula( - [value], - evalStep, - formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined - ); + return new Formula({ + inputs: [value], + evaluate: evalStep, + invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + }); } public static conditional( value: FormulaSource, @@ -513,22 +989,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.if(value, condition, formulaModifier); } - public static constant(value: InvertibleFormulaSource): InvertibleFormula { - return new Formula([value]) as InvertibleFormula; - } - - public static variable(value: ProcessedComputable<DecimalSource>): InvertibleFormula { - return new Formula([value], undefined, undefined, true) as InvertibleFormula; - } - public static abs(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.abs); + return new Formula({ inputs: [value], evaluate: Decimal.abs }); } - public static neg(value: InvertibleFormulaSource): InvertibleFormula; + public static neg(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static neg(value: FormulaSource): GenericFormula; public static neg(value: FormulaSource) { - return new Formula([value], Decimal.neg, invertNeg); + return new Formula({ + inputs: [value], + evaluate: Decimal.neg, + invert: invertNeg, + integrate: integrateNeg + }); } public static negate(value: FormulaSource) { return Formula.neg(value); @@ -538,35 +1011,41 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } public static sign(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.sign); + return new Formula({ inputs: [value], evaluate: Decimal.sign }); } public static sgn(value: FormulaSource) { return Formula.sign(value); } public static round(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.round); + return new Formula({ inputs: [value], evaluate: Decimal.round }); } public static floor(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.floor); + return new Formula({ inputs: [value], evaluate: Decimal.floor }); } public static ceil(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.ceil); + return new Formula({ inputs: [value], evaluate: Decimal.ceil }); } public static trunc(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.trunc); + return new Formula({ inputs: [value], evaluate: Decimal.trunc }); } public static add( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static add(value: FormulaSource, other: FormulaSource): GenericFormula; public static add(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.add, invertAdd); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.add, + invert: invertAdd, + integrate: integrateAdd, + invertIntegral: invertIntegrateAdd + }); } public static plus(value: FormulaSource, other: FormulaSource) { return Formula.add(value, other); @@ -575,10 +1054,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static sub( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static sub(value: FormulaSource, other: FormulaSource): GenericFormula; public static sub(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.sub, invertSub); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.sub, + invert: invertSub, + integrate: integrateSub, + invertIntegral: invertIntegrateSub + }); } public static subtract(value: FormulaSource, other: FormulaSource) { return Formula.sub(value, other); @@ -590,10 +1075,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static mul( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static mul(value: FormulaSource, other: FormulaSource): GenericFormula; public static mul(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.mul, invertMul); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.mul, + invert: invertMul, + integrate: integrateMul, + invertIntegral: invertIntegrateMul + }); } public static multiply(value: FormulaSource, other: FormulaSource) { return Formula.mul(value, other); @@ -605,19 +1096,33 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static div( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static div(value: FormulaSource, other: FormulaSource): GenericFormula; public static div(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.div, invertDiv); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.div, + invert: invertDiv, + integrate: integrateDiv, + invertIntegral: invertIntegrateDiv + }); } public static divide(value: FormulaSource, other: FormulaSource) { return Formula.div(value, other); } - public static recip(value: InvertibleFormulaSource): InvertibleFormula; + public static recip( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static recip(value: FormulaSource): GenericFormula; public static recip(value: FormulaSource) { - return new Formula([value], Decimal.recip, invertRecip); + return new Formula({ + inputs: [value], + evaluate: Decimal.recip, + invert: invertRecip, + integrate: integrateRecip, + invertIntegral: invertIntegrateRecip + }); } public static reciprocal(value: FormulaSource): GenericFormula { return Formula.recip(value); @@ -627,19 +1132,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } public static max(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.max); + return new Formula({ inputs: [value, other], evaluate: Decimal.max }); } public static min(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.min); + return new Formula({ inputs: [value, other], evaluate: Decimal.min }); } public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.minabs); + return new Formula({ inputs: [value, other], evaluate: Decimal.minabs }); } public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.maxabs); + return new Formula({ inputs: [value, other], evaluate: Decimal.maxabs }); } public static clamp( @@ -647,108 +1152,169 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { min: FormulaSource, max: FormulaSource ): GenericFormula { - return new Formula([value, min, max], Decimal.clamp); + return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp }); } public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { - return new Formula([value, min], Decimal.clampMin); + return new Formula({ inputs: [value, min], evaluate: Decimal.clampMin }); } public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { - return new Formula([value, max], Decimal.clampMax); + return new Formula({ inputs: [value, max], evaluate: Decimal.clampMax }); } public static pLog10(value: InvertibleFormulaSource): InvertibleFormula; public static pLog10(value: FormulaSource): GenericFormula; public static pLog10(value: FormulaSource) { - return new Formula([value], Decimal.pLog10); + return new Formula({ inputs: [value], evaluate: Decimal.pLog10 }); } public static absLog10(value: InvertibleFormulaSource): InvertibleFormula; public static absLog10(value: FormulaSource): GenericFormula; public static absLog10(value: FormulaSource) { - return new Formula([value], Decimal.absLog10); + return new Formula({ inputs: [value], evaluate: Decimal.absLog10 }); } - public static log10(value: InvertibleFormulaSource): InvertibleFormula; + public static log10( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log10(value: FormulaSource): GenericFormula; public static log10(value: FormulaSource) { - return new Formula([value], Decimal.log10, invertLog10); + return new Formula({ + inputs: [value], + evaluate: Decimal.log10, + invert: invertLog10, + integrate: integrateLog10, + invertIntegral: invertIntegrateLog10 + }); } public static log( value: InvertibleFormulaSource, base: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log(value: FormulaSource, base: FormulaSource): GenericFormula; public static log(value: FormulaSource, base: FormulaSource) { - return new Formula([value, base], Decimal.log, invertLog); + return new Formula({ + inputs: [value, base], + evaluate: Decimal.log, + invert: invertLog, + integrate: integrateLog, + invertIntegral: invertIntegrateLog + }); } public static logarithm(value: FormulaSource, base: FormulaSource) { return Formula.log(value, base); } - public static log2(value: InvertibleFormulaSource): InvertibleFormula; + public static log2( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log2(value: FormulaSource): GenericFormula; public static log2(value: FormulaSource) { - return new Formula([value], Decimal.log2, invertLog2); + return new Formula({ + inputs: [value], + evaluate: Decimal.log2, + invert: invertLog2, + integrate: integrateLog2, + invertIntegral: invertIntegrateLog2 + }); } - public static ln(value: InvertibleFormulaSource): InvertibleFormula; + public static ln( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static ln(value: FormulaSource): GenericFormula; public static ln(value: FormulaSource) { - return new Formula([value], Decimal.ln, invertLn); + return new Formula({ + inputs: [value], + evaluate: Decimal.ln, + invert: invertLn, + integrate: integrateLn, + invertIntegral: invertIntegrateLn + }); } public static pow( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.pow, invertPow); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.pow, + invert: invertPow, + integrate: integratePow, + invertIntegral: invertIntegratePow + }); } - public static pow10(value: InvertibleFormulaSource): InvertibleFormula; + public static pow10( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow10(value: FormulaSource): GenericFormula; public static pow10(value: FormulaSource) { - return new Formula([value], Decimal.pow10, invertPow10); + return new Formula({ + inputs: [value], + evaluate: Decimal.pow10, + invert: invertPow10, + integrate: integratePow10, + invertIntegral: invertIntegratePow10 + }); } public static pow_base( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow_base(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow_base(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.pow_base, invertPowBase); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.pow_base, + invert: invertPowBase, + integrate: integratePowBase, + invertIntegral: invertIntegratePowBase + }); } public static root( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static root(value: FormulaSource, other: FormulaSource): GenericFormula; public static root(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.root, invertRoot); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.root, + invert: invertRoot, + integrate: integrateRoot, + invertIntegral: invertIntegrateRoot + }); } public static factorial(value: FormulaSource) { - return new Formula([value], Decimal.factorial); + return new Formula({ inputs: [value], evaluate: Decimal.factorial }); } public static gamma(value: FormulaSource) { - return new Formula([value], Decimal.gamma); + return new Formula({ inputs: [value], evaluate: Decimal.gamma }); } public static lngamma(value: FormulaSource) { - return new Formula([value], Decimal.lngamma); + return new Formula({ inputs: [value], evaluate: Decimal.lngamma }); } - public static exp(value: InvertibleFormulaSource): InvertibleFormula; + public static exp(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource) { - return new Formula([value], Decimal.exp, invertExp); + return new Formula({ + inputs: [value], + evaluate: Decimal.exp, + invert: invertExp, + integrate: integrateExp + }); } public static sqr(value: FormulaSource) { @@ -782,7 +1348,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], tetrate, invertTetrate); + return new Formula({ + inputs: [value, height, payload], + evaluate: tetrate, + invert: invertTetrate + }); } public static iteratedexp( @@ -800,7 +1370,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], iteratedexp, invertIteratedExp); + return new Formula({ + inputs: [value, height, payload], + evaluate: iteratedexp, + invert: invertIteratedExp + }); } public static iteratedlog( @@ -808,7 +1382,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { base: FormulaSource = 10, times: FormulaSource = 1 ): GenericFormula { - return new Formula([value, base, times], iteratedLog); + return new Formula({ inputs: [value, base, times], evaluate: iteratedLog }); } public static slog( @@ -817,11 +1391,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ): InvertibleFormula; public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { - return new Formula([value, base], slog, invertSlog); + return new Formula({ inputs: [value, base], evaluate: slog, invert: invertSlog }); } public static layeradd10(value: FormulaSource, diff: FormulaSource): GenericFormula { - return new Formula([value, diff], Decimal.layeradd10); + return new Formula({ inputs: [value, diff], evaluate: Decimal.layeradd10 }); } public static layeradd( @@ -835,19 +1409,23 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { base?: FormulaSource ): GenericFormula; public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { - return new Formula([value, diff, base], layeradd, invertLayeradd); + return new Formula({ + inputs: [value, diff, base], + evaluate: layeradd, + invert: invertLayeradd + }); } public static lambertw(value: InvertibleFormulaSource): InvertibleFormula; public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { - return new Formula([value], Decimal.lambertw, invertLambertw); + return new Formula({ inputs: [value], evaluate: Decimal.lambertw, invert: invertLambertw }); } public static ssqrt(value: InvertibleFormulaSource): InvertibleFormula; public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { - return new Formula([value], Decimal.ssqrt, invertSsqrt); + return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: invertSsqrt }); } public static pentate( @@ -855,79 +1433,159 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], pentate); + return new Formula({ inputs: [value, height, payload], evaluate: pentate }); } - public static sin(value: InvertibleFormulaSource): InvertibleFormula; + public static sin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource) { - return new Formula([value], Decimal.sin, invertAsin); + return new Formula({ + inputs: [value], + evaluate: Decimal.sin, + invert: invertAsin, + integrate: integrateSin + }); } - public static cos(value: InvertibleFormulaSource): InvertibleFormula; + public static cos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource) { - return new Formula([value], Decimal.cos, invertAcos); + return new Formula({ + inputs: [value], + evaluate: Decimal.cos, + invert: invertAcos, + integrate: integrateCos + }); } - public static tan(value: InvertibleFormulaSource): InvertibleFormula; + public static tan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource) { - return new Formula([value], Decimal.tan, invertAtan); + return new Formula({ + inputs: [value], + evaluate: Decimal.tan, + invert: invertAtan, + integrate: integrateTan + }); } - public static asin(value: InvertibleFormulaSource): InvertibleFormula; + public static asin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource) { - return new Formula([value], Decimal.asin, invertSin); + return new Formula({ + inputs: [value], + evaluate: Decimal.asin, + invert: invertSin, + integrate: integrateAsin + }); } - public static acos(value: InvertibleFormulaSource): InvertibleFormula; + public static acos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource) { - return new Formula([value], Decimal.acos, invertCos); + return new Formula({ + inputs: [value], + evaluate: Decimal.acos, + invert: invertCos, + integrate: integrateAcos + }); } - public static atan(value: InvertibleFormulaSource): InvertibleFormula; + public static atan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource) { - return new Formula([value], Decimal.atan, invertTan); + return new Formula({ + inputs: [value], + evaluate: Decimal.atan, + invert: invertTan, + integrate: integrateAtan + }); } - public static sinh(value: InvertibleFormulaSource): InvertibleFormula; + public static sinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource) { - return new Formula([value], Decimal.sinh, invertAsinh); + return new Formula({ + inputs: [value], + evaluate: Decimal.sinh, + invert: invertAsinh, + integrate: integrateSinh + }); } - public static cosh(value: InvertibleFormulaSource): InvertibleFormula; + public static cosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource) { - return new Formula([value], Decimal.cosh, invertAcosh); + return new Formula({ + inputs: [value], + evaluate: Decimal.cosh, + invert: invertAcosh, + integrate: integrateCosh + }); } - public static tanh(value: InvertibleFormulaSource): InvertibleFormula; + public static tanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource) { - return new Formula([value], Decimal.tanh, invertAtanh); + return new Formula({ + inputs: [value], + evaluate: Decimal.tanh, + invert: invertAtanh, + integrate: integrateTanh + }); } - public static asinh(value: InvertibleFormulaSource): InvertibleFormula; + public static asinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource) { - return new Formula([value], Decimal.asinh, invertSinh); + return new Formula({ + inputs: [value], + evaluate: Decimal.asinh, + invert: invertSinh, + integrate: integrateAsinh + }); } - public static acosh(value: InvertibleFormulaSource): InvertibleFormula; + public static acosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource) { - return new Formula([value], Decimal.acosh, invertCosh); + return new Formula({ + inputs: [value], + evaluate: Decimal.acosh, + invert: invertCosh, + integrate: integrateAcosh + }); } - public static atanh(value: InvertibleFormulaSource): InvertibleFormula; + public static atanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource) { - return new Formula([value], Decimal.atanh, invertTanh); + return new Formula({ + inputs: [value], + evaluate: Decimal.atanh, + invert: invertTanh, + integrate: integrateAtanh + }); + } + + public step( + start: Computable<DecimalSource>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + return Formula.step(this, start, formulaModifier); + } + + public if( + condition: Computable<boolean>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + return Formula.if(this, condition, formulaModifier); + } + public conditional( + condition: Computable<boolean>, + formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + ) { + return Formula.if(this, condition, formulaModifier); } public abs() { @@ -967,42 +1625,102 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.trunc(this); } + public add( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public add(this: FormulaSource, value: FormulaSource): GenericFormula; public add(value: FormulaSource) { return Formula.add(this, value); } + public plus( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public plus(this: FormulaSource, value: FormulaSource): GenericFormula; public plus(value: FormulaSource) { return Formula.add(this, value); } + public sub( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public sub(this: FormulaSource, value: FormulaSource): GenericFormula; public sub(value: FormulaSource) { return Formula.sub(this, value); } + public subtract( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public subtract(this: FormulaSource, value: FormulaSource): GenericFormula; public subtract(value: FormulaSource) { return Formula.sub(this, value); } + public minus( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public minus(this: FormulaSource, value: FormulaSource): GenericFormula; public minus(value: FormulaSource) { return Formula.sub(this, value); } + public mul( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public mul(this: FormulaSource, value: FormulaSource): GenericFormula; public mul(value: FormulaSource) { return Formula.mul(this, value); } + public multiply( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public multiply(this: FormulaSource, value: FormulaSource): GenericFormula; public multiply(value: FormulaSource) { return Formula.mul(this, value); } + public times( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public times(this: FormulaSource, value: FormulaSource): GenericFormula; public times(value: FormulaSource) { return Formula.mul(this, value); } + public div( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public div(this: FormulaSource, value: FormulaSource): GenericFormula; public div(value: FormulaSource) { return Formula.div(this, value); } + public divide( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public divide(this: FormulaSource, value: FormulaSource): GenericFormula; public divide(value: FormulaSource) { return Formula.div(this, value); } + public divideBy( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public divideBy(this: FormulaSource, value: FormulaSource): GenericFormula; public divideBy(value: FormulaSource) { return Formula.div(this, value); } + public dividedBy( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public dividedBy(this: FormulaSource, value: FormulaSource): GenericFormula; public dividedBy(value: FormulaSource) { return Formula.div(this, value); } @@ -1057,9 +1775,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.log10(this); } + public log( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public log(this: FormulaSource, value: FormulaSource): GenericFormula; public log(value: FormulaSource) { return Formula.log(this, value); } + public logarithm( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public logarithm(this: FormulaSource, value: FormulaSource): GenericFormula; public logarithm(value: FormulaSource) { return Formula.log(this, value); } @@ -1072,6 +1800,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.ln(this); } + public pow( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow(this: FormulaSource, value: FormulaSource): GenericFormula; public pow(value: FormulaSource) { return Formula.pow(this, value); } @@ -1080,10 +1813,20 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.pow10(this); } + public pow_base( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow_base(this: FormulaSource, value: FormulaSource): GenericFormula; public pow_base(value: FormulaSource) { return Formula.pow_base(this, value); } + public root( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public root(this: FormulaSource, value: FormulaSource): GenericFormula; public root(value: FormulaSource) { return Formula.root(this, value); } @@ -1118,6 +1861,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.root(this, 3); } + public tetrate( + this: InvertibleFormulaSource, + height: InvertibleFormulaSource, + payload: InvertibleFormulaSource + ): InvertibleFormula; + public tetrate( + this: FormulaSource, + height: FormulaSource, + payload: FormulaSource + ): GenericFormula; public tetrate( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) @@ -1125,6 +1878,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.tetrate(this, height, payload); } + public iteratedexp( + this: InvertibleFormulaSource, + height: InvertibleFormulaSource, + payload: InvertibleFormulaSource + ): InvertibleFormula; + public iteratedexp( + this: FormulaSource, + height: FormulaSource, + payload: FormulaSource + ): GenericFormula; public iteratedexp( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) @@ -1136,6 +1899,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.iteratedlog(this, base, times); } + public slog(this: InvertibleFormulaSource, base?: InvertibleFormulaSource): InvertibleFormula; + public slog(this: FormulaSource, base?: FormulaSource): GenericFormula; public slog(base: FormulaSource = 10) { return Formula.slog(this, base); } @@ -1144,6 +1909,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.layeradd10(this, diff); } + public layeradd( + this: InvertibleFormulaSource, + diff: InvertibleFormulaSource, + base?: InvertibleFormulaSource + ): InvertibleFormula; + public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula; public layeradd(diff: FormulaSource, base: FormulaSource) { return Formula.layeradd(this, diff, base); } @@ -1211,3 +1982,43 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.atanh(this); } } + +/** + * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. Returns a ref of how many can be bought, as well as how much that will cost. + * @param formula The formula to use for calculating buy max from + * @param resource The resource used when purchasing (is only read from) + * @param spendResources Whether or not to count spent resources on each purchase or not + */ +export function calculateMaxAffordable( + formula: GenericFormula, + resource: Resource, + spendResources: Computable<boolean> = true +) { + const computedSpendResources = convertComputable(spendResources); + const maxAffordable = computed(() => { + if (unref(computedSpendResources)) { + if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { + throw "Cannot calculate max affordable of formula with non-invertible integral"; + } + return Decimal.floor( + formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) + ); + } else { + if (!formula.isInvertible()) { + throw "Cannot calculate max affordable of non-invertible formula"; + } + return Decimal.floor((formula as InvertibleFormula).invert(resource.value)); + } + }); + const cost = computed(() => { + if (unref(computedSpendResources)) { + return Decimal.sub( + formula.evaluateIntegral(maxAffordable.value), + formula.evaluateIntegral() + ); + } else { + return formula.evaluate(maxAffordable.value); + } + }); + return { maxAffordable, cost }; +} diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 94ba563..f8b4490 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,7 +1,13 @@ -import Formula, { GenericFormula, InvertibleFormula, unrefFormulaSource } from "game/formulas"; +import { createResource, Resource } from "features/resources/resource"; +import Formula, { + calculateMaxAffordable, + GenericFormula, + InvertibleFormula, + unrefFormulaSource +} from "game/formulas"; import Decimal, { DecimalSource, format } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { Ref, ref } from "vue"; +import { ref } from "vue"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -43,98 +49,6 @@ declare global { } } -function testConstant( - desc: string, - formulaFunc: () => InvertibleFormula, - expectedValue: DecimalSource = 10 -) { - describe(desc, () => { - let formula: GenericFormula; - beforeAll(() => { - formula = formulaFunc(); - }); - test("Evaluates correctly", () => - expect(formula.evaluate()).compare_tolerance(expectedValue)); - test("Invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); - test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); - }); -} - -function testFormula<T extends FormulaFunctions>( - functionName: T, - args: Readonly<Parameters<typeof Formula[T]>>, - invertible = true -) { - let formula: GenericFormula; - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - formula = Formula[functionName](...args); - }); - test("Formula is not marked as having a variable", () => - expect(formula.hasVariable()).toBe(false)); - test(`Formula is${invertible ? "" : " not"} invertible`, () => - expect(formula.isInvertible()).toBe(invertible)); - if (invertible) { - test(`Formula throws if inverting without any variables`, () => - expect(() => formula.invert(10)).toThrow()); - } -} - -// Utility function that will test all the different -// It's a lot of tests, but I'd rather be exhaustive -function testFormulaCall<T extends FormulaFunctions>( - functionName: T, - args: Readonly<Parameters<typeof Formula[T]>> -) { - let testName = functionName + "("; - for (let i = 0; i < args.length; i++) { - if (i !== 0) { - testName += ", "; - } - testName += args[i]; - } - testName += ") evaluates correctly"; - test(testName, () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const formula = Formula[functionName](...args); - - try { - const expectedEvaluation = Decimal[functionName]( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ...args.map(i => unrefFormulaSource(i)) - ); - if (expectedEvaluation != null) { - expect(formula.evaluate()).compare_tolerance(expectedEvaluation); - } - } catch { - // If this is an invalid Decimal operation, then ignore this test case - } - }); -} - -function testAliases<T extends FormulaFunctions>( - aliases: T[], - args: Parameters<typeof Formula[T]> -) { - describe(aliases[0], () => { - let formula: GenericFormula; - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - formula = Formula[aliases[0]](...args); - }); - - aliases.slice(1).forEach(alias => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true)); - }); - }); -} - const testValues = ["-1e400", 0, 0.25] as const; const invertibleZeroParamFunctionNames = [ @@ -164,7 +78,6 @@ const invertibleZeroParamFunctionNames = [ "acosh", "atanh" ] as const; - const nonInvertibleZeroParamFunctionNames = [ "abs", "sign", @@ -178,6 +91,64 @@ const nonInvertibleZeroParamFunctionNames = [ "gamma", "lngamma" ] as const; +const integrableZeroParamFunctionNames = [ + "neg", + "recip", + "log10", + "log2", + "ln", + "pow10", + "exp", + "sqr", + "sqrt", + "cube", + "cbrt", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "asinh", + "acosh", + "atanh" +] as const; +const nonIntegrableZeroParamFunctionNames = [ + ...nonInvertibleZeroParamFunctionNames, + "lambertw", + "ssqrt" +] as const; +const invertibleIntegralZeroPramFunctionNames = [ + "recip", + "log10", + "log2", + "ln", + "pow10", + "sqr", + "sqrt", + "cube", + "cbrt" +] as const; +const nonInvertibleIntegralZeroPramFunctionNames = [ + ...nonIntegrableZeroParamFunctionNames, + "neg", + "exp", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "asinh", + "acosh", + "atanh" +] as const; const invertibleOneParamFunctionNames = [ "add", @@ -189,7 +160,6 @@ const invertibleOneParamFunctionNames = [ "root", "slog" ] as const; - const nonInvertibleOneParamFunctionNames = [ "max", "min", @@ -199,10 +169,18 @@ const nonInvertibleOneParamFunctionNames = [ "clampMax", "layeradd10" ] as const; +const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const; +const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const; +const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames; +const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames; const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; - const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; +const nonIntegrableTwoParamFunctionNames = [ + ...invertibleTwoParamFunctionNames, + ...nonInvertibleZeroParamFunctionNames +]; +const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunctionNames; describe("Formula Equality Checking", () => { describe("Equality Checks", () => { @@ -214,6 +192,25 @@ describe("Formula Equality Checking", () => { }); describe("Formula aliases", () => { + function testAliases<T extends FormulaFunctions>( + aliases: T[], + args: Parameters<typeof Formula[T]> + ) { + describe(aliases[0], () => { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[aliases[0]](...args); + }); + + aliases.slice(1).forEach(alias => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true)); + }); + }); + } testAliases(["neg", "negate", "negated"], [1]); testAliases(["recip", "reciprocal", "reciprocate"], [1]); testAliases(["sign", "sgn"], [1]); @@ -263,6 +260,30 @@ describe("Formula Equality Checking", () => { describe("Creating Formulas", () => { describe("Constants", () => { + function testConstant( + desc: string, + formulaFunc: () => InvertibleFormula, + expectedValue: DecimalSource = 10 + ) { + describe(desc, () => { + let formula: GenericFormula; + beforeAll(() => { + formula = formulaFunc(); + }); + test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); + test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false)); + test("Integral is not invertible", () => + expect(formula.isIntegralInvertible()).toBe(false)); + test("Is not marked as having a variable", () => + expect(formula.hasVariable()).toBe(false)); + test("Evaluates correctly", () => + expect(formula.evaluate()).compare_tolerance(expectedValue)); + test("Invert throws", () => expect(() => formula.invert(25)).toThrow()); + test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow()); + test("Invert integral throws", () => + expect(() => formula.invertIntegral(25)).toThrow()); + }); + } testConstant("number", () => Formula.constant(10)); testConstant("string", () => Formula.constant("10")); testConstant("formula", () => Formula.constant(Formula.constant(10))); @@ -270,10 +291,64 @@ describe("Creating Formulas", () => { testConstant("ref", () => Formula.constant(ref(10))); }); + function checkFormula<T extends FormulaFunctions>( + functionName: T, + args: Readonly<Parameters<typeof Formula[T]>> + ) { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[functionName](...args); + }); + // None of these formulas have variables, so they should all behave the same + test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); + test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); + test(`Formula throws if trying to invert`, () => + expect(() => formula.invert(10)).toThrow()); + test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false)); + test("Has a non-invertible integral", () => + expect(formula.isIntegralInvertible()).toBe(false)); + } + + // Utility function that will test all the different + // It's a lot of tests, but I'd rather be exhaustive + function testFormulaCall<T extends FormulaFunctions>( + functionName: T, + args: Readonly<Parameters<typeof Formula[T]>> + ) { + let testName = functionName + "("; + for (let i = 0; i < args.length; i++) { + if (i !== 0) { + testName += ", "; + } + testName += args[i]; + } + testName += ") evaluates correctly"; + test(testName, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const formula = Formula[functionName](...args); + + try { + const expectedEvaluation = Decimal[functionName]( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...args.map(i => unrefFormulaSource(i)) + ); + if (expectedEvaluation != null) { + expect(formula.evaluate()).compare_tolerance(expectedEvaluation); + } + } catch { + // If this is an invalid Decimal operation, then ignore this test case + } + }); + } + describe("Invertible 0-param", () => { invertibleZeroParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0] as const); + checkFormula(names, [0] as const); testValues.forEach(i => testFormulaCall(names, [i] as const)); }) ); @@ -281,7 +356,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 0-param", () => { nonInvertibleZeroParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0] as const, false); + checkFormula(names, [0] as const); testValues.forEach(i => testFormulaCall(names, [i] as const)); }) ); @@ -289,7 +364,7 @@ describe("Creating Formulas", () => { describe("Invertible 1-param", () => { invertibleOneParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0] as const); + checkFormula(names, [0, 0] as const); testValues.forEach(i => testValues.forEach(j => testFormulaCall(names, [i, j] as const)) ); @@ -299,7 +374,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 1-param", () => { nonInvertibleOneParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0] as const, false); + checkFormula(names, [0, 0] as const); testValues.forEach(i => testValues.forEach(j => testFormulaCall(names, [i, j] as const)) ); @@ -309,7 +384,7 @@ describe("Creating Formulas", () => { describe("Invertible 2-param", () => { invertibleTwoParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0, 0] as const); + checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => testValues.forEach(j => testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) @@ -321,7 +396,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 2-param", () => { nonInvertibleTwoParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0, 0] as const, false); + checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => testValues.forEach(j => testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) @@ -342,14 +417,27 @@ describe("Variables", () => { test("Created variable is marked as a variable", () => expect(variable.hasVariable()).toBe(true)); - test("Evaluate() returns variable's value", () => + test("evaluate() returns variable's value", () => expect(variable.evaluate()).compare_tolerance(10)); - test("Invert() is pass-through", () => expect(variable.invert(100)).compare_tolerance(100)); + test("evaluate(variable) overrides variable value", () => + expect(variable.add(10).evaluate(20)).compare_tolerance(30)); test("Nested variable is marked as having a variable", () => expect(variable.add(10).div(3).pow(2).hasVariable()).toBe(true)); test("Nested non-variable is marked as not having a variable", () => expect(constant.add(10).div(3).pow(2).hasVariable()).toBe(false)); +}); + +describe("Inverting", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("variable.invert() is pass-through", () => + expect(variable.invert(100)).compare_tolerance(100)); describe("Invertible Formulas correctly calculate when they contain a variable", () => { function checkFormula(formula: GenericFormula, expectedBool = true) { @@ -392,42 +480,43 @@ describe("Variables", () => { }); }); - describe("Non-Invertible Formulas never marked as having a variable", () => { + describe("Non-invertible formulas marked as such", () => { function checkFormula(formula: GenericFormula) { expect(formula.isInvertible()).toBe(false); - expect(formula.hasVariable()).toBe(false); + expect(formula.isIntegrable()).toBe(false); + expect(formula.isIntegralInvertible()).toBe(false); } nonInvertibleZeroParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var) is marked as not invertible and not having a variable`, () => + test(`${name}(var) is marked as not invertible`, () => checkFormula(Formula[name](variable))); }); }); nonInvertibleOneParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable))); }); }); nonInvertibleTwoParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var, const) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, const, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable, constant))); - test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant, variable))); - test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable, variable))); - test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable, variable))); }); }); @@ -476,6 +565,164 @@ describe("Variables", () => { }); }); +describe("Integrating", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("evaluateIntegral() returns variable's value", () => + expect(variable.evaluate()).compare_tolerance(10)); + test("evaluateIntegral(variable) overrides variable value", () => + expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400)); + + describe("Integrable functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegrable()).toBe(true); + expect(() => formula.evaluateIntegral()).to.not.throw(); + } + integrableZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + integrableOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + }); + + describe("Non-Integrable functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegrable()).toBe(false); + } + nonIntegrableZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonIntegrableOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonIntegrableTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + + // TODO I think these tests will require writing at least one known example for every function + describe.todo("Integrable formulas integrate correctly"); +}); + +describe("Inverting integrals", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("variable.invertIntegral() is pass-through", () => + expect(variable.invertIntegral(20)).compare_tolerance(20)); + + describe("Invertible Integral functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegralInvertible()).toBe(true); + expect(() => formula.invertIntegral(10)).to.not.throw(); + } + invertibleIntegralZeroPramFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable))); + }); + }); + invertibleIntegralOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + }); + + describe("Non-Invertible integral functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegralInvertible()).toBe(false); + } + nonInvertibleIntegralZeroPramFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonInvertibleIntegralOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonInvertibleIntegralTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + + // TODO I think these tests will require writing at least one known example for every function + describe.todo("Invertible Integral formulas invert correctly"); +}); + describe("Step-wise", () => { let variable: GenericFormula; let constant: GenericFormula; @@ -485,7 +732,7 @@ describe("Step-wise", () => { }); test("Formula without variable is marked as such", () => { - expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(false); expect(Formula.step(constant, 10, value => Formula.sqrt(value)).hasVariable()).toBe(false); }); @@ -499,6 +746,24 @@ describe("Step-wise", () => { expect(Formula.step(constant, 10, value => Formula.abs(value)).hasVariable()).toBe(false); }); + test("Formula never marked integrable", () => { + expect(Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegrable()).toBe( + false + ); + expect(() => + Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral() + ).toThrow(); + }); + + test("Formula never marked as having an invertible integral", () => { + expect( + Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegralInvertible() + ).toBe(false); + expect(() => + Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10) + ).toThrow(); + }); + test("Formula modifiers with variables mark formula as non-invertible", () => { expect( Formula.step(constant, 10, value => Formula.add(value, variable)).isInvertible() @@ -540,7 +805,7 @@ describe("Conditionals", () => { }); test("Formula without variable is marked as such", () => { - expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(false); expect(Formula.if(constant, true, value => Formula.sqrt(value)).hasVariable()).toBe(false); }); @@ -554,6 +819,24 @@ describe("Conditionals", () => { expect(Formula.if(constant, true, value => Formula.abs(value)).hasVariable()).toBe(false); }); + test("Formula never marked integrable", () => { + expect(Formula.if(constant, true, value => Formula.add(value, 10)).isIntegrable()).toBe( + false + ); + expect(() => + Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral() + ).toThrow(); + }); + + test("Formula never marked as having an invertible integral", () => { + expect( + Formula.if(constant, true, value => Formula.add(value, 10)).isIntegralInvertible() + ).toBe(false); + expect(() => + Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10) + ).toThrow(); + }); + test("Formula modifiers with variables mark formula as non-invertible", () => { expect( Formula.if(constant, true, value => Formula.add(value, variable)).isInvertible() @@ -587,53 +870,143 @@ describe("Conditionals", () => { }); describe("Custom Formulas", () => { - describe("Formula with just one input", () => { - let formula: GenericFormula; - beforeAll(() => { - formula = new Formula([10]); - }); - test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); - test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); - test("Evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(10)); - test("Invert is pass-through", () => expect(formula.invert(20)).compare_tolerance(20)); - }); - - describe("Formula with non-one inputs without required other params", () => { - test("Zero inputs throws", () => expect(() => new Formula([])).toThrow()); - test("Two inputs throws", () => expect(() => new Formula([1, 2])).toThrow()); - test("Zero inputs and invert throws", () => - expect(() => new Formula([], undefined, value => value)).toThrow()); - test("Two inputs and invert throws", () => - expect(() => new Formula([1, 2], undefined, value => value)).toThrow()); - test("Zero inputs and evaluate and hasVariable throws", () => - expect(() => new Formula([], () => 10, undefined, true)).toThrow()); - test("Two inputs and evaluate and hasVariable throws", () => - expect(() => new Formula([1, 2], () => 10, undefined, true)).toThrow()); - }); - describe("Formula with evaluate", () => { test("Zero input evaluates correctly", () => - expect(new Formula([], () => 10).evaluate()).compare_tolerance(10)); + expect(new Formula({ inputs: [], evaluate: () => 10 }).evaluate()).compare_tolerance( + 10 + )); test("One input evaluates correctly", () => - expect(new Formula([1], value => value).evaluate()).compare_tolerance(1)); + expect( + new Formula({ inputs: [1], evaluate: value => value }).evaluate() + ).compare_tolerance(1)); test("Two inputs evaluates correctly", () => - expect(new Formula([1, 2], (v1, v2) => v1).evaluate()).compare_tolerance(1)); + expect( + new Formula({ inputs: [1, 2], evaluate: (v1, v2) => v1 }).evaluate() + ).compare_tolerance(1)); }); describe("Formula with invert", () => { test("Zero input inverts correctly", () => - expect(new Formula([], undefined, value => value).invert(10)).compare_tolerance(10)); + expect( + new Formula({ inputs: [], evaluate: () => 6, invert: value => value }).invert(10) + ).compare_tolerance(10)); test("One input inverts correctly", () => - expect(new Formula([1], undefined, (value, v1) => v1).invert(10)).compare_tolerance(1)); + expect( + new Formula({ inputs: [1], evaluate: () => 10, invert: (value, v1) => v1 }).invert( + 10 + ) + ).compare_tolerance(1)); test("Two inputs inverts correctly", () => expect( - new Formula([1, 2], undefined, (value, v1, v2) => v2).invert(10) + new Formula({ + inputs: [1, 2], + evaluate: () => 10, + invert: (value, v1, v2) => v2 + }).invert(10) ).compare_tolerance(2)); }); - test("Formula with hasVariable", () => { - const formula = new Formula([], undefined, value => value, true); - expect(formula.isInvertible()).toBe(true); - expect(formula.hasVariable()).toBe(true); + describe("Formula with integrate", () => { + test("Zero input integrates correctly", () => + expect( + new Formula({ + inputs: [], + evaluate: () => 10, + integrate: () => 20 + }).evaluateIntegral() + ).compare_tolerance(20)); + test("One input integrates correctly", () => + expect( + new Formula({ + inputs: [1], + evaluate: () => 10, + integrate: val => val ?? 20 + }).evaluateIntegral() + ).compare_tolerance(20)); + test("Two inputs integrates correctly", () => + expect( + new Formula({ + inputs: [1, 2], + evaluate: (v1, v2) => 10, + integrate: (v1, v2) => 3 + }).evaluateIntegral() + ).compare_tolerance(3)); + }); + + describe("Formula with invertIntegral", () => { + test("Zero input inverts integral correctly", () => + expect( + new Formula({ + inputs: [], + evaluate: () => 10, + invertIntegral: () => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + test("One input inverts integral correctly", () => + expect( + new Formula({ + inputs: [1], + evaluate: () => 10, + invertIntegral: val => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + test("Two inputs inverts integral correctly", () => + expect( + new Formula({ + inputs: [1, 2], + evaluate: (v1, v2) => 10, + invertIntegral: (v1, v2) => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + }); +}); + +describe("Buy Max", () => { + let resource: Resource; + beforeAll(() => { + resource = createResource(10); + }); + describe("With spending", () => { + test("Throws on formula with non-invertible integral", () => { + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.neg(10), + resource, + false + ); + expect(() => maxAffordable.value).toThrow(); + expect(() => cost.value).toThrow(); + }); + // https://www.desmos.com/calculator/5vgletdc1p + test("Calculates max affordable and cost correctly", () => { + const variable = Formula.variable(10); + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.pow(1.05, variable), + resource, + false + ); + expect(maxAffordable.value).compare_tolerance(47); + expect(cost.value).compare_tolerance(Decimal.pow(1.05, 47)); + }); + }); + describe("Without spending", () => { + test("Throws on non-invertible formula", () => { + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.abs(10), + resource, + false + ); + expect(() => maxAffordable.value).toThrow(); + expect(() => cost.value).toThrow(); + }); + // https://www.desmos.com/calculator/5vgletdc1p + test("Calculates max affordable and cost correctly", () => { + const variable = Formula.variable(10); + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.pow(1.05, variable), + resource + ); + expect(maxAffordable.value).compare_tolerance(7); + expect(cost.value).compare_tolerance(7.35); + }); }); }); From 4e9a0f6cb56f388e4fb89ff4edc2a028fb044845 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 4 Feb 2023 18:51:11 -0600 Subject: [PATCH 18/56] Add some more tests for nested formulas --- src/game/formulas.ts | 2 ++ tests/game/formulas.test.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 7e893be..2ccfbde 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -875,6 +875,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } invertIntegral(value: DecimalSource): DecimalSource { + // This is nearly completely non-functional + // Proper nesting will require somehow using integration by substitution or integration by parts return this.internalInvertIntegral?.call(this, value, ...this.inputs) ?? value; } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index f8b4490..d9be004 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -563,6 +563,11 @@ describe("Inverting", () => { }) ); }); + + test("Inverting nested formulas", () => { + const formula = Formula.add(variable, constant).times(constant); + expect(formula.invert(100)).compare_tolerance(0); + }); }); describe("Integrating", () => { @@ -643,6 +648,11 @@ describe("Integrating", () => { // TODO I think these tests will require writing at least one known example for every function describe.todo("Integrable formulas integrate correctly"); + + test("Integrating nested formulas", () => { + const formula = Formula.add(variable, constant).times(constant); + expect(formula.evaluateIntegral()).compare_tolerance(1500); + }); }); describe("Inverting integrals", () => { @@ -721,6 +731,11 @@ describe("Inverting integrals", () => { // TODO I think these tests will require writing at least one known example for every function describe.todo("Invertible Integral formulas invert correctly"); + + test("Inverting integral of nested formulas", () => { + const formula = Formula.add(variable, constant).times(constant); + expect(formula.invertIntegral(1500)).compare_tolerance(10); + }); }); describe("Step-wise", () => { From 60625ec9a0277d81fc6ed6645d85e8ffe882e7be Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 4 Feb 2023 19:08:46 -0600 Subject: [PATCH 19/56] Update vitest --- package.json | 2 +- tests/game/formulas.test.ts | 30 +++++++++++++++--------------- vite.config.ts | 5 +---- vitest.config.ts | 9 +++++++++ 4 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 vitest.config.ts diff --git a/package.json b/package.json index 18f336b..b548099 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.27.3", + "vitest": "^0.28.6", "vue-tsc": "^0.38.1" }, "engines": { diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index d9be004..eed9d42 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -11,8 +11,22 @@ import { ref } from "vue"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; +interface CustomMatchers<R = unknown> { + compare_tolerance(expected: DecimalSource): R; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Vi { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Assertion extends CustomMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface AsymmetricMatchersContaining extends CustomMatchers {} + } +} + expect.extend({ - compare_tolerance(received, expected) { + compare_tolerance(received: DecimalSource, expected: DecimalSource) { const { isNot } = this; let pass = false; if (!Decimal.isFinite(expected)) { @@ -35,20 +49,6 @@ expect.extend({ } }); -interface CustomMatchers<R = unknown> { - compare_tolerance(expected: DecimalSource): R; -} - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Vi { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface Assertion extends CustomMatchers {} - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface AsymmetricMatchersContaining extends CustomMatchers {} - } -} - const testValues = ["-1e400", 0, 0.25] as const; const invertibleZeroParamFunctionNames = [ diff --git a/vite.config.ts b/vite.config.ts index 080d8e6..f3ce360 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -57,8 +57,5 @@ export default defineConfig({ ] } }) - ], - test: { - environment: "jsdom" - } + ] }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3ab791b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { mergeConfig } from 'vite' +import { defineConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig(viteConfig, defineConfig({ + test: { + environment: "jsdom" + } +})) From 536b3e0f1798444abc6db265da5a72bd38e22a10 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 00:24:50 -0600 Subject: [PATCH 20/56] Made maxAffordable be relative rather than absolute --- src/game/formulas.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 2ccfbde..5b95c7f 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,7 +1,7 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ref, Ref, unref } from "vue"; +import { computed, ComputedRef, ref, Ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula<any>; @@ -776,11 +776,14 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { | undefined; private readonly internalHasVariable: boolean; + public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined; + constructor(options: FormulaOptions<T>) { // Variable case if ("variable" in options) { this.inputs = [options.variable] as T; this.internalHasVariable = true; + this.innermostVariable = options.variable; return; } // Constant case @@ -811,6 +814,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { this.internalHasVariable = numVariables === 1 || (numVariables === 0 && hasVariable === true); + if (this.internalHasVariable) { + this.innermostVariable = variable?.innermostVariable; + } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.internalInvert = @@ -2004,7 +2010,7 @@ export function calculateMaxAffordable( } return Decimal.floor( formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) - ); + ).sub(unref(formula.innermostVariable) ?? 0); } else { if (!formula.isInvertible()) { throw "Cannot calculate max affordable of non-invertible formula"; @@ -2013,13 +2019,11 @@ export function calculateMaxAffordable( } }); const cost = computed(() => { + const newValue = maxAffordable.value.add(unref(formula.innermostVariable) ?? 0); if (unref(computedSpendResources)) { - return Decimal.sub( - formula.evaluateIntegral(maxAffordable.value), - formula.evaluateIntegral() - ); + return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); } else { - return formula.evaluate(maxAffordable.value); + return formula.evaluate(newValue); } }); return { maxAffordable, cost }; From b89c4cde09fae44a9ca15915668a08a0aa01ac16 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 00:25:48 -0600 Subject: [PATCH 21/56] Fix error about persistent value that isn't part of a layer --- tests/game/formulas.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index eed9d42..217b980 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -979,7 +979,7 @@ describe("Custom Formulas", () => { describe("Buy Max", () => { let resource: Resource; beforeAll(() => { - resource = createResource(10); + resource = createResource(ref(10)); }); describe("With spending", () => { test("Throws on formula with non-invertible integral", () => { From 757cfaa1ab23a853cfed2d396cad258b2690982b Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 00:26:24 -0600 Subject: [PATCH 22/56] Make calculateMaxAffordable only accept spendResources values on supported formulas --- src/game/formulas.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 5b95c7f..0c95ba2 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1998,7 +1998,17 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { * @param spendResources Whether or not to count spent resources on each purchase or not */ export function calculateMaxAffordable( - formula: GenericFormula, + formula: InvertibleFormula, + resource: Resource, + spendResources?: true +): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> }; +export function calculateMaxAffordable( + formula: InvertibleIntegralFormula, + resource: Resource, + spendResources: Computable<boolean> +): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> }; +export function calculateMaxAffordable( + formula: InvertibleFormula, resource: Resource, spendResources: Computable<boolean> = true ) { From 7eeb0318e2ca9fd0a343d217f12fb041b6c23b05 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 02:44:03 -0600 Subject: [PATCH 23/56] Make requirements support buying max --- src/game/formulas.ts | 46 +++++++++---- src/game/requirements.tsx | 127 ++++++++++++++++++++++++++---------- tests/game/formulas.test.ts | 34 ++++------ 3 files changed, 137 insertions(+), 70 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 0c95ba2..3536123 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1992,7 +1992,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } /** - * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. Returns a ref of how many can be bought, as well as how much that will cost. + * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. * @param formula The formula to use for calculating buy max from * @param resource The resource used when purchasing (is only read from) * @param spendResources Whether or not to count spent resources on each purchase or not @@ -2001,19 +2001,19 @@ export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, spendResources?: true -): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> }; +): ComputedRef<DecimalSource>; export function calculateMaxAffordable( formula: InvertibleIntegralFormula, resource: Resource, spendResources: Computable<boolean> -): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> }; +): ComputedRef<DecimalSource>; export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, spendResources: Computable<boolean> = true ) { const computedSpendResources = convertComputable(spendResources); - const maxAffordable = computed(() => { + return computed(() => { if (unref(computedSpendResources)) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { throw "Cannot calculate max affordable of formula with non-invertible integral"; @@ -2028,13 +2028,33 @@ export function calculateMaxAffordable( return Decimal.floor((formula as InvertibleFormula).invert(resource.value)); } }); - const cost = computed(() => { - const newValue = maxAffordable.value.add(unref(formula.innermostVariable) ?? 0); - if (unref(computedSpendResources)) { - return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); - } else { - return formula.evaluate(newValue); - } - }); - return { maxAffordable, cost }; +} + +/** + * Utility for calculating the cost of a formula for a given amount of purchases. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. + * @param formula The formula to use for calculating buy max from + * @param amountToBuy The amount of purchases to calculate the cost for + * @param spendResources Whether or not to count spent resources on each purchase or not + */ +export function calculateCost( + formula: InvertibleFormula, + amountToBuy: DecimalSource, + spendResources?: true +): DecimalSource; +export function calculateCost( + formula: InvertibleIntegralFormula, + amountToBuy: DecimalSource, + spendResources: boolean +): DecimalSource; +export function calculateCost( + formula: InvertibleFormula, + amountToBuy: DecimalSource, + spendResources = true +) { + const newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0); + if (spendResources) { + return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); + } else { + return formula.evaluate(newValue); + } } diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 607800b..b4126fc 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -11,6 +11,7 @@ import { import { createLazyProxy } from "util/proxies"; import { joinJSX, renderJSX } from "util/vue"; import { computed, unref } from "vue"; +import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from "./formulas"; /** * An object that can be used to describe a requirement to perform some purchase or other action. @@ -18,36 +19,33 @@ import { computed, unref } from "vue"; */ 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; + partialDisplay?: (amount?: DecimalSource) => JSX.Element; /** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */ - display?: JSXFunction; + display?: (amount?: DecimalSource) => JSX.Element; visibility: ProcessedComputable<Visibility.Visible | Visibility.None>; - requirementMet: ProcessedComputable<boolean>; + requirementMet: ProcessedComputable<DecimalSource | boolean>; requiresPay: ProcessedComputable<boolean>; - pay?: VoidFunction; + buyMax?: ProcessedComputable<boolean>; + pay?: (amount?: DecimalSource) => void; } export type Requirements = Requirement | Requirement[]; export interface CostRequirementOptions { resource: Resource; - cost: Computable<DecimalSource>; + cost: Computable<DecimalSource> | GenericFormula; visibility?: Computable<Visibility.Visible | Visibility.None>; - requiresPay?: ProcessedComputable<boolean>; - pay?: VoidFunction; + requiresPay?: Computable<boolean>; + buyMax?: Computable<boolean>; + spendResources?: Computable<boolean>; + pay?: (amount?: DecimalSource) => void; } -export function createCostRequirement<T extends CostRequirementOptions>( - optionsFunc: () => T -): Requirement { +export function createCostRequirement<T extends CostRequirementOptions>(optionsFunc: () => T) { 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(() => ( + req.partialDisplay = amount => ( <span style={ unref(req.requirementMet as ProcessedComputable<boolean>) @@ -57,33 +55,81 @@ export function createCostRequirement<T extends CostRequirementOptions>( > {displayResource( req.resource, - unref(req.cost as ProcessedComputable<DecimalSource>) + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref( + req.spendResources as ProcessedComputable<boolean> | undefined + ) ?? true + ) + : unref(req.cost as ProcessedComputable<DecimalSource>) )}{" "} {req.resource.displayName} </span> - )); - req.display = jsx(() => ( + ); + req.display = amount => ( <div> {unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "} {displayResource( req.resource, - unref(req.cost as ProcessedComputable<DecimalSource>) + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref( + req.spendResources as ProcessedComputable<boolean> | undefined + ) ?? true + ) + : 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"); + processComputable(req as T, "spendResources"); setDefault(req, "requiresPay", true); - setDefault(req, "pay", function () { - req.resource.value = Decimal.sub( - req.resource.value, - unref(req.cost as ProcessedComputable<DecimalSource>) - ).max(0); + setDefault(req, "pay", function (amount?: DecimalSource) { + const cost = + req.cost instanceof Formula + ? calculateCost( + req.cost, + amount ?? 1, + unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? + true + ) + : unref(req.cost as ProcessedComputable<DecimalSource>); + req.resource.value = Decimal.sub(req.resource.value, cost).max(0); }); + processComputable(req as T, "buyMax"); + + if ( + "buyMax" in req && + req.buyMax !== false && + req.cost instanceof Formula && + req.cost.isInvertible() + ) { + req.requirementMet = calculateMaxAffordable( + req.cost, + req.resource, + unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true + ); + } else { + req.requirementMet = computed(() => { + if (req.cost instanceof Formula) { + return Decimal.gte(req.resource.value, req.cost.evaluate()); + } else { + return Decimal.gte( + req.resource.value, + unref(req.cost as ProcessedComputable<DecimalSource>) + ); + } + }); + } return req as Requirement; }); @@ -112,14 +158,26 @@ export function createBooleanRequirement( })); } -export function requirementsMet(requirements: Requirements) { +export function requirementsMet(requirements: Requirements): boolean { if (isArray(requirements)) { - return requirements.every(r => unref(r.requirementMet)); + return requirements.every(requirementsMet); } - return unref(requirements.requirementMet); + const reqsMet = unref(requirements.requirementMet); + return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0); } -export function displayRequirements(requirements: Requirements) { +export function maxRequirementsMet(requirements: Requirements): DecimalSource { + if (isArray(requirements)) { + return requirements.map(maxRequirementsMet).reduce(Decimal.min); + } + const reqsMet = unref(requirements.requirementMet); + if (typeof reqsMet === "boolean") { + return reqsMet ? Infinity : 0; + } + return reqsMet; +} + +export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (isArray(requirements)) { requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible); if (requirements.length === 1) { @@ -136,7 +194,7 @@ export function displayRequirements(requirements: Requirements) { <div> Costs:{" "} {joinJSX( - withCosts.map(r => r.partialDisplay!()), + withCosts.map(r => r.partialDisplay!(amount)), <>, </> )} </div> @@ -145,7 +203,7 @@ export function displayRequirements(requirements: Requirements) { <div> Requires:{" "} {joinJSX( - withoutCosts.map(r => r.partialDisplay!()), + withoutCosts.map(r => r.partialDisplay!(amount)), <>, </> )} </div> @@ -156,10 +214,11 @@ export function displayRequirements(requirements: Requirements) { return requirements.display?.() ?? <></>; } -export function payRequirements(requirements: Requirements) { +export function payRequirements(requirements: Requirements, buyMax = false) { + const amount = buyMax ? maxRequirementsMet(requirements) : 1; if (isArray(requirements)) { - requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.()); + requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount)); } else if (unref(requirements.requiresPay)) { - requirements.pay?.(); + requirements.pay?.(amount); } } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 217b980..2be632e 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,5 +1,6 @@ import { createResource, Resource } from "features/resources/resource"; import Formula, { + calculateCost, calculateMaxAffordable, GenericFormula, InvertibleFormula, @@ -983,45 +984,32 @@ describe("Buy Max", () => { }); describe("With spending", () => { test("Throws on formula with non-invertible integral", () => { - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.neg(10), - resource, - false - ); + const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); expect(() => maxAffordable.value).toThrow(); - expect(() => cost.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(10); - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.pow(1.05, variable), - resource, - false - ); + const formula = Formula.pow(1.05, variable); + const maxAffordable = calculateMaxAffordable(formula, resource, false); expect(maxAffordable.value).compare_tolerance(47); - expect(cost.value).compare_tolerance(Decimal.pow(1.05, 47)); + expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance( + Decimal.pow(1.05, 47) + ); }); }); describe("Without spending", () => { test("Throws on non-invertible formula", () => { - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.abs(10), - resource, - false - ); + const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false); expect(() => maxAffordable.value).toThrow(); - expect(() => cost.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(10); - const { maxAffordable, cost } = calculateMaxAffordable( - Formula.pow(1.05, variable), - resource - ); + const formula = Formula.pow(1.05, variable); + const maxAffordable = calculateMaxAffordable(formula, resource); expect(maxAffordable.value).compare_tolerance(7); - expect(cost.value).compare_tolerance(7.35); + expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35); }); }); }); From 68702c5620e29c0dbe53d241cbd28cec9cbc5cf4 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 02:51:07 -0600 Subject: [PATCH 24/56] Make buyables support buying max --- src/features/buyable.tsx | 24 +++++++++++++++++++----- src/game/requirements.tsx | 3 +-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx index cdff9a0..89dcada 100644 --- a/src/features/buyable.tsx +++ b/src/features/buyable.tsx @@ -6,6 +6,7 @@ import { DefaultValue, Persistent, persistent } from "game/persistence"; import { createVisibilityRequirement, displayRequirements, + maxRequirementsMet, payRequirements, Requirements, requirementsMet @@ -44,6 +45,7 @@ export interface BuyableOptions { style?: Computable<StyleValue>; mark?: Computable<boolean | string>; small?: Computable<boolean>; + buyMax?: Computable<boolean>; display?: Computable<BuyableDisplay>; onPurchase?: VoidFunction; } @@ -70,6 +72,7 @@ export type Buyable<T extends BuyableOptions> = Replace< style: GetComputableType<T["style"]>; mark: GetComputableType<T["mark"]>; small: GetComputableType<T["small"]>; + buyMax: GetComputableType<T["buyMax"]>; display: Ref<CoercableComponent>; } >; @@ -98,9 +101,9 @@ export function createBuyable<T extends BuyableOptions>( const limitRequirement = { requirementMet: computed(() => - Decimal.lt( - (buyable as GenericBuyable).amount.value, - unref((buyable as GenericBuyable).purchaseLimit) + Decimal.sub( + unref((buyable as GenericBuyable).purchaseLimit), + (buyable as GenericBuyable).amount.value ) ), requiresPay: false, @@ -138,7 +141,12 @@ export function createBuyable<T extends BuyableOptions>( if (!unref(genericBuyable.canClick)) { return; } - payRequirements(buyable.requirements); + payRequirements( + buyable.requirements, + unref(genericBuyable.buyMax) + ? maxRequirementsMet(genericBuyable.requirements) + : 1 + ); genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); genericBuyable.onPurchase?.(); }; @@ -187,7 +195,12 @@ export function createBuyable<T extends BuyableOptions>( {genericBuyable.maxed.value ? null : ( <div> <br /> - {displayRequirements(genericBuyable.requirements)} + {displayRequirements( + genericBuyable.requirements, + unref(genericBuyable.buyMax) + ? maxRequirementsMet(genericBuyable.requirements) + : 1 + )} </div> )} </span> @@ -203,6 +216,7 @@ export function createBuyable<T extends BuyableOptions>( processComputable(buyable as T, "style"); processComputable(buyable as T, "mark"); processComputable(buyable as T, "small"); + processComputable(buyable as T, "buyMax"); buyable[GatherProps] = function (this: GenericBuyable) { const { display, visibility, style, classes, onClick, canClick, small, mark, id } = diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index b4126fc..0a73372 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -214,8 +214,7 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS return requirements.display?.() ?? <></>; } -export function payRequirements(requirements: Requirements, buyMax = false) { - const amount = buyMax ? maxRequirementsMet(requirements) : 1; +export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (isArray(requirements)) { requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount)); } else if (unref(requirements.requiresPay)) { From 728263376776cab6bbcdc1ee0e045db31e389908 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sun, 5 Feb 2023 04:23:53 -0600 Subject: [PATCH 25/56] Documented requirements.tsx --- src/game/requirements.tsx | 89 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 0a73372..ca68843 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -1,5 +1,5 @@ import { isArray } from "@vue/shared"; -import { CoercableComponent, jsx, JSXFunction, setDefault, Visibility } from "features/feature"; +import { CoercableComponent, jsx, setDefault, Visibility } from "features/feature"; import { displayResource, Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "lib/break_eternity"; import { @@ -18,30 +18,84 @@ import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from " * @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}. */ + /** + * The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. + */ partialDisplay?: (amount?: DecimalSource) => JSX.Element; - /** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */ + /** + * The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. + */ display?: (amount?: DecimalSource) => JSX.Element; + /** + * Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property. + */ visibility: ProcessedComputable<Visibility.Visible | Visibility.None>; + /** + * Whether or not this requirement has been met. + */ requirementMet: ProcessedComputable<DecimalSource | boolean>; + /** + * Whether or not this requirement will need to affect the game state when whatever is using this requirement gets triggered. + */ requiresPay: ProcessedComputable<boolean>; + /** + * Whether or not this requirement can have multiple levels of requirements that can be completed at once. + */ buyMax?: ProcessedComputable<boolean>; + /** + * Perform any effects to the game state that should happen when the requirement gets triggered. + * @param amount The amount of levels of requirements to pay for. + */ pay?: (amount?: DecimalSource) => void; } +/** + * Utility type for accepting 1 or more {@link Requirement}s. + */ export type Requirements = Requirement | Requirement[]; +/** An object that configures a {@link Requirement} based on a resource cost. */ export interface CostRequirementOptions { + /** + * The resource that will be checked for meeting the {@link cost}. + */ resource: Resource; + /** + * The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case {@link buyMax} will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support buyMax by passing a custom {@link pay} function. + */ cost: Computable<DecimalSource> | GenericFormula; + /** + * Pass-through to {@link Requirement.visibility}. + */ visibility?: Computable<Visibility.Visible | Visibility.None>; + /** + * Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}. + */ requiresPay?: Computable<boolean>; + /** + * Pass-through to {@link Requirement.buyMax}. + * @see {@link cost} for restrictions on buying max support. + */ buyMax?: Computable<boolean>; + /** + * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions. + * @see {Formula} + */ spendResources?: Computable<boolean>; + /** + * Pass-through to {@link Requirement.pay}. May be required for buying max support. + * @see {@link cost} for restrictions on buying max support. + */ pay?: (amount?: DecimalSource) => void; } -export function createCostRequirement<T extends CostRequirementOptions>(optionsFunc: () => T) { +/** + * Lazily creates a requirement with the given options, that is based on meeting an amount of a resource. + * @param optionsFunc Cost requirement options. + */ +export function createCostRequirement<T extends CostRequirementOptions>( + optionsFunc: () => T +): Requirement { return createLazyProxy(() => { const req = optionsFunc() as T & Partial<Requirement>; @@ -135,6 +189,10 @@ export function createCostRequirement<T extends CostRequirementOptions>(optionsF }); } +/** + * Utility function for creating a requirement that a specified vue feature is visible + * @param feature The feature to check the visibility of + */ export function createVisibilityRequirement(feature: { visibility: ProcessedComputable<Visibility>; }): Requirement { @@ -145,6 +203,11 @@ export function createVisibilityRequirement(feature: { })); } +/** + * Creates a requirement based on a true/false value + * @param requirement The boolean requirement to use + * @param display How to display this requirement to the user + */ export function createBooleanRequirement( requirement: Computable<boolean>, display?: CoercableComponent @@ -158,6 +221,10 @@ export function createBooleanRequirement( })); } +/** + * Utility for checking if 1+ requirements are all met + * @param requirements The 1+ requirements to check + */ export function requirementsMet(requirements: Requirements): boolean { if (isArray(requirements)) { return requirements.every(requirementsMet); @@ -166,6 +233,10 @@ export function requirementsMet(requirements: Requirements): boolean { return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0); } +/** + * Calculates the maximum number of levels that could be acquired with the current requirement states. True/false requirements will be counted as Infinity or 0. + * @param requirements The 1+ requirements to check + */ export function maxRequirementsMet(requirements: Requirements): DecimalSource { if (isArray(requirements)) { return requirements.map(maxRequirementsMet).reduce(Decimal.min); @@ -177,6 +248,11 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource { return reqsMet; } +/** + * Utility function for display 1+ requirements compactly. + * @param requirements The 1+ requirements to display + * @param amount The amount of levels earned to be displayed + */ export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (isArray(requirements)) { requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible); @@ -214,6 +290,11 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS return requirements.display?.() ?? <></>; } +/** + * Utility function for paying the costs for 1+ requirements + * @param requirements The 1+ requirements to pay + * @param amount How many levels to pay for + */ export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (isArray(requirements)) { requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount)); From a88b1520e11f75860068dd777ab1cda34eb9fc97 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Mon, 6 Feb 2023 00:55:55 -0600 Subject: [PATCH 26/56] Further documented formulas --- src/game/formulas.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 3536123..a3a692c 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -759,6 +759,12 @@ function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) throw "Could not integrate due to no input being a variable"; } +/** + * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted. + * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently. + * @see {@link calculateMaxAffordable} + * @see {@link game/requirements.createCostRequirement} + */ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { readonly inputs: T; @@ -827,6 +833,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { this.internalHasVariable && variable?.isIntegralInvertible() ? invert : undefined; } + /** Type predicate that this formula can be inverted. */ isInvertible(): this is InvertibleFormula { return ( this.internalHasVariable && @@ -834,14 +841,17 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + /** Type predicate that this formula can be integrated. */ isIntegrable(): this is IntegrableFormula { return this.internalHasVariable && this.internalIntegrate != null; } + /** Type predicate that this formula has an integral function that can be inverted. */ isIntegralInvertible(): this is InvertibleIntegralFormula { return this.internalHasVariable && this.internalInvertIntegral != null; } + /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */ hasVariable(): boolean { return this.internalHasVariable; } @@ -863,6 +873,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + /** + * Takes a potential result of the formula, and calculates what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula is invertible. + * @param value The result of the formula + * @see {@link isInvertible} + */ invert(value: DecimalSource): DecimalSource { if (this.internalInvert) { return this.internalInvert.call(this, value, ...this.inputs); @@ -872,6 +887,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { throw "Cannot invert non-invertible formula"; } + /** + * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable + * @param variable Optionally override the value of the variable while evaluating + * @see {@link isIntegrable} + */ evaluateIntegral(variable?: DecimalSource): DecimalSource { return ( this.internalIntegrate?.call(this, variable, ...this.inputs) ?? @@ -880,12 +900,21 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + /** + * Given the potential result of the formula's integral (sand the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible. + * @param value The result of the integral. + * @see {@link isIntegralInvertible} + */ invertIntegral(value: DecimalSource): DecimalSource { // This is nearly completely non-functional // Proper nesting will require somehow using integration by substitution or integration by parts return this.internalInvertIntegral?.call(this, value, ...this.inputs) ?? value; } + /** + * Compares if two formulas are equivalent to each other. Note that function contexts can lead to false negatives. + * @param other The formula to compare to this one. + */ equals(other: GenericFormula): boolean { return ( this.inputs.length === other.inputs.length && @@ -904,12 +933,20 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { ); } + /** + * Creates a formula that evaluates to a constant value. + * @param value The constant value for this formula. + */ public static constant( value: InvertibleFormulaSource ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { return new Formula({ inputs: [value] }) as InvertibleFormula; } + /** + * Creates a formula that is marked as the variable for an outer formula. Typically used for inverting and integrating. + * @param value The variable for this formula. + */ public static variable( value: ProcessedComputable<DecimalSource> ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { @@ -957,6 +994,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } + /** + * Applies a modifier to a formula under a given condition. + * @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 + */ public static if( value: FormulaSource, condition: Computable<boolean>, @@ -989,6 +1032,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined }); } + /** @see {@link if} */ public static conditional( value: FormulaSource, condition: Computable<boolean>, From a4d6c2e844e1d82ed7451a48ec909324908ae220 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 7 Feb 2023 08:31:09 -0600 Subject: [PATCH 27/56] Added tests for requirements --- tests/game/formulas.test.ts | 41 +------ tests/game/requirements.test.ts | 182 ++++++++++++++++++++++++++++++++ tests/utils.ts | 40 +++++++ 3 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 tests/game/requirements.test.ts create mode 100644 tests/utils.ts diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 2be632e..fdfa1fd 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -6,50 +6,13 @@ import Formula, { InvertibleFormula, unrefFormulaSource } from "game/formulas"; -import Decimal, { DecimalSource, format } from "util/bignum"; +import Decimal, { DecimalSource } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; import { ref } from "vue"; +import "../utils"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; -interface CustomMatchers<R = unknown> { - compare_tolerance(expected: DecimalSource): R; -} - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Vi { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface Assertion extends CustomMatchers {} - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface AsymmetricMatchersContaining extends CustomMatchers {} - } -} - -expect.extend({ - compare_tolerance(received: DecimalSource, expected: DecimalSource) { - const { isNot } = this; - let pass = false; - if (!Decimal.isFinite(expected)) { - pass = !Decimal.isFinite(received); - } else if (Decimal.isNaN(expected)) { - pass = Decimal.isNaN(received); - } else { - pass = Decimal.eq_tolerance(received, expected); - } - return { - // do not alter your "pass" based on isNot. Vitest does it for you - pass, - message: () => - `Expected ${received} to${ - (isNot as boolean) ? " not" : "" - } be close to ${expected}`, - expected: format(expected), - actual: format(received) - }; - } -}); - const testValues = ["-1e400", 0, 0.25] as const; const invertibleZeroParamFunctionNames = [ diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts new file mode 100644 index 0000000..3a26dbc --- /dev/null +++ b/tests/game/requirements.test.ts @@ -0,0 +1,182 @@ +import { Visibility } from "features/feature"; +import { createResource, Resource } from "features/resources/resource"; +import Formula from "game/formulas"; +import { + createBooleanRequirement, + createCostRequirement, + createVisibilityRequirement, + maxRequirementsMet, + payRequirements, + Requirement, + requirementsMet +} from "game/requirements"; +import { beforeAll, describe, expect, test } from "vitest"; +import { isRef, ref, unref } from "vue"; +import "../utils"; + +describe("Creating cost requirement", () => { + describe("Minimal requirement", () => { + let resource: Resource; + let requirement: Requirement; + beforeAll(() => { + resource = createResource(ref(10)); + requirement = createCostRequirement(() => ({ + resource, + cost: 10 + })); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("resource pass-through", () => (requirement as any).resource === resource); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("cost pass-through", () => (requirement as any).cost === 10); + + test("partialDisplay exists", () => + requirement.partialDisplay != null && typeof requirement.partialDisplay === "function"); + test("display exists", () => + requirement.display != null && typeof requirement.display === "function"); + test("pay exists", () => requirement.pay != null && typeof requirement.pay === "function"); + test("requirementMet exists", () => + requirement.requirementMet != null && isRef(requirement.requirementMet)); + test("is visible", () => requirement.visibility === Visibility.Visible); + test("requires pay", () => requirement.requiresPay === true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("spends resources", () => (requirement as any).spendResources !== false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("does not buy max", () => (requirement as any).buyMax !== true); + }); + + describe("Fully customized", () => { + let resource: Resource; + let requirement: Requirement; + beforeAll(() => { + resource = createResource(ref(10)); + requirement = createCostRequirement(() => ({ + resource, + cost: 10, + visibility: Visibility.None, + requiresPay: false, + buyMax: true, + spendResources: false, + // eslint-disable-next-line @typescript-eslint/no-empty-function + pay() {} + })); + }); + + test("pay is empty function", () => + requirement.pay != null && + typeof requirement.pay === "function" && + requirement.pay.length === 1); + test("is not visible", () => requirement.visibility === Visibility.None); + test("does not require pay", () => requirement.requiresPay === false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("does not spend resources", () => (requirement as any).spendResources); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + test("buys max", () => (requirement as any).buyMax); + }); + + test("Requirement met when meeting the cost", () => { + const resource = createResource(ref(10)); + const requirement = createCostRequirement(() => ({ + resource, + cost: 10 + })); + expect(unref(requirement.requirementMet)).toBe(true); + }); + + test("Requirement not met when not meeting the cost", () => { + const resource = createResource(ref(10)); + const requirement = createCostRequirement(() => ({ + resource, + cost: 100 + })); + expect(unref(requirement.requirementMet)).toBe(false); + }); +}); + +describe("Creating visibility requirement", () => { + test("Requirement met when visible", () => { + const requirement = createVisibilityRequirement({ visibility: Visibility.Visible }); + expect(unref(requirement.requirementMet)).toBe(true); + }); + + test("Requirement not met when not visible", () => { + let requirement = createVisibilityRequirement({ visibility: Visibility.None }); + expect(unref(requirement.requirementMet)).toBe(false); + requirement = createVisibilityRequirement({ visibility: Visibility.Hidden }); + expect(unref(requirement.requirementMet)).toBe(false); + }); +}); + +describe("Creating boolean requirement", () => { + test("Requirement met when true", () => { + const requirement = createBooleanRequirement(ref(true)); + expect(unref(requirement.requirementMet)).toBe(true); + }); + + test("Requirement not met when false", () => { + const requirement = createBooleanRequirement(ref(false)); + expect(unref(requirement.requirementMet)).toBe(false); + }); +}); + +describe("Checking all requirements met", () => { + let metRequirement: Requirement; + let unmetRequirement: Requirement; + beforeAll(() => { + metRequirement = createBooleanRequirement(true); + unmetRequirement = createBooleanRequirement(false); + }); + + test("Returns true if no requirements", () => { + expect(requirementsMet([])).toBe(true); + }); + + test("Returns true if all requirements met", () => { + expect(requirementsMet([metRequirement, metRequirement])).toBe(true); + }); + + test("Returns false if any requirements unmet", () => { + expect(requirementsMet([metRequirement, unmetRequirement])).toBe(false); + }); +}); + +describe("Checking maximum levels of requirements met", () => { + test("Returns 0 if any requirement is not met", () => { + const requirements = [ + createBooleanRequirement(false), + createBooleanRequirement(true), + createCostRequirement(() => ({ + resource: createResource(ref(10)), + cost: Formula.variable(0) + })) + ]; + expect(maxRequirementsMet(requirements)).toBe(0); + }); + + test("Returns correct number of requirements met", () => { + const requirements = [ + createBooleanRequirement(true), + createCostRequirement(() => ({ + resource: createResource(ref(10)), + cost: Formula.variable(0) + })) + ]; + expect(maxRequirementsMet(requirements)).toBe(10); + }); +}); + +test("Paying requirements", () => { + const resource = createResource(ref(100)); + const noPayment = createCostRequirement(() => ({ + resource, + cost: 10, + requiresPay: false + })); + const payment = createCostRequirement(() => ({ + resource, + cost: 10 + })); + payRequirements([noPayment, payment]); + expect(resource.value).compare_tolerance(90); +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..0e58e19 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,40 @@ +import Decimal, { DecimalSource, format } from "util/bignum"; +import { expect } from "vitest"; + +interface CustomMatchers<R = unknown> { + compare_tolerance(expected: DecimalSource): R; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Vi { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface Assertion extends CustomMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface AsymmetricMatchersContaining extends CustomMatchers {} + } +} + +expect.extend({ + compare_tolerance(received: DecimalSource, expected: DecimalSource) { + const { isNot } = this; + let pass = false; + if (!Decimal.isFinite(expected)) { + pass = !Decimal.isFinite(received); + } else if (Decimal.isNaN(expected)) { + pass = Decimal.isNaN(received); + } else { + pass = Decimal.eq_tolerance(received, expected); + } + return { + // do not alter your "pass" based on isNot. Vitest does it for you + pass, + message: () => + `Expected ${received} to${ + (isNot as boolean) ? " not" : "" + } be close to ${expected}`, + expected: format(expected), + actual: format(received) + }; + } +}); From de0e3f90b8b13bf532b1ba63b9e44f126b9a3d16 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 9 Feb 2023 06:57:36 -0600 Subject: [PATCH 28/56] Updated formula typing for non-invertible params in invertible formulas --- src/game/formulas.ts | 465 +++++++++++++++++++----------------- tests/game/formulas.test.ts | 7 +- 2 files changed, 258 insertions(+), 214 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index a3a692c..10b3fcd 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -6,7 +6,6 @@ import { computed, ComputedRef, ref, Ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula<any>; export type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula; -export type InvertibleFormulaSource = ProcessedComputable<DecimalSource> | InvertibleFormula; export type InvertibleFormula = GenericFormula & { invert: (value: DecimalSource) => DecimalSource; }; @@ -938,7 +937,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { * @param value The constant value for this formula. */ public static constant( - value: InvertibleFormulaSource + value: ProcessedComputable<DecimalSource> ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { return new Formula({ inputs: [value] }) as InvertibleFormula; } @@ -1045,7 +1044,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value], evaluate: Decimal.abs }); } - public static neg(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static neg<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static neg(value: FormulaSource): GenericFormula; public static neg(value: FormulaSource) { return new Formula({ @@ -1055,9 +1054,13 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { integrate: integrateNeg }); } + public static negate<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; + public static negate(value: FormulaSource): GenericFormula; public static negate(value: FormulaSource) { return Formula.neg(value); } + public static negated<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; + public static negated(value: FormulaSource): GenericFormula; public static negated(value: FormulaSource) { return Formula.neg(value); } @@ -1085,10 +1088,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value], evaluate: Decimal.trunc }); } - public static add( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static add<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static add<T extends GenericFormula>(value: FormulaSource, other: T): T; public static add(value: FormulaSource, other: FormulaSource): GenericFormula; public static add(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1099,14 +1100,15 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateAdd }); } + public static plus<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static plus<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static plus(value: FormulaSource, other: FormulaSource): GenericFormula; public static plus(value: FormulaSource, other: FormulaSource) { return Formula.add(value, other); } - public static sub( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static sub<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static sub<T extends GenericFormula>(value: FormulaSource, other: T): T; public static sub(value: FormulaSource, other: FormulaSource): GenericFormula; public static sub(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1117,17 +1119,21 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateSub }); } + public static subtract<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static subtract<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static subtract(value: FormulaSource, other: FormulaSource): GenericFormula; public static subtract(value: FormulaSource, other: FormulaSource) { return Formula.sub(value, other); } + public static minus<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static minus<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static minus(value: FormulaSource, other: FormulaSource): GenericFormula; public static minus(value: FormulaSource, other: FormulaSource) { return Formula.sub(value, other); } - public static mul( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static mul<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static mul<T extends GenericFormula>(value: FormulaSource, other: T): T; public static mul(value: FormulaSource, other: FormulaSource): GenericFormula; public static mul(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1138,17 +1144,21 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateMul }); } + public static multiply<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static multiply<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static multiply(value: FormulaSource, other: FormulaSource): GenericFormula; public static multiply(value: FormulaSource, other: FormulaSource) { return Formula.mul(value, other); } + public static times<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static times<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static times(value: FormulaSource, other: FormulaSource): GenericFormula; public static times(value: FormulaSource, other: FormulaSource) { return Formula.mul(value, other); } - public static div( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static div<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static div<T extends GenericFormula>(value: FormulaSource, other: T): T; public static div(value: FormulaSource, other: FormulaSource): GenericFormula; public static div(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1159,13 +1169,14 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateDiv }); } + public static divide<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static divide<T extends GenericFormula>(value: FormulaSource, other: T): T; + public static divide(value: FormulaSource, other: FormulaSource): GenericFormula; public static divide(value: FormulaSource, other: FormulaSource) { return Formula.div(value, other); } - public static recip( - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static recip<T extends GenericFormula>(value: T): T; public static recip(value: FormulaSource): GenericFormula; public static recip(value: FormulaSource) { return new Formula({ @@ -1176,9 +1187,13 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateRecip }); } + public static reciprocal<T extends GenericFormula>(value: T): T; + public static reciprocal(value: FormulaSource): GenericFormula; public static reciprocal(value: FormulaSource): GenericFormula { return Formula.recip(value); } + public static reciprocate<T extends GenericFormula>(value: T): T; + public static reciprocate(value: FormulaSource): GenericFormula; public static reciprocate(value: FormulaSource) { return Formula.recip(value); } @@ -1215,21 +1230,15 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value, max], evaluate: Decimal.clampMax }); } - public static pLog10(value: InvertibleFormulaSource): InvertibleFormula; - public static pLog10(value: FormulaSource): GenericFormula; - public static pLog10(value: FormulaSource) { + public static pLog10(value: FormulaSource): GenericFormula { return new Formula({ inputs: [value], evaluate: Decimal.pLog10 }); } - public static absLog10(value: InvertibleFormulaSource): InvertibleFormula; - public static absLog10(value: FormulaSource): GenericFormula; - public static absLog10(value: FormulaSource) { + public static absLog10(value: FormulaSource): GenericFormula { return new Formula({ inputs: [value], evaluate: Decimal.absLog10 }); } - public static log10( - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static log10<T extends GenericFormula>(value: T): T; public static log10(value: FormulaSource): GenericFormula; public static log10(value: FormulaSource) { return new Formula({ @@ -1241,10 +1250,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static log( - value: InvertibleFormulaSource, - base: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static log<T extends GenericFormula>(value: T, base: FormulaSource): T; + public static log<T extends GenericFormula>(value: FormulaSource, base: T): T; public static log(value: FormulaSource, base: FormulaSource): GenericFormula; public static log(value: FormulaSource, base: FormulaSource) { return new Formula({ @@ -1255,13 +1262,14 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral: invertIntegrateLog }); } + public static logarithm<T extends GenericFormula>(value: T, base: FormulaSource): T; + public static logarithm<T extends GenericFormula>(value: FormulaSource, base: T): T; + public static logarithm(value: FormulaSource, base: FormulaSource): GenericFormula; public static logarithm(value: FormulaSource, base: FormulaSource) { return Formula.log(value, base); } - public static log2( - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static log2<T extends GenericFormula>(value: T): T; public static log2(value: FormulaSource): GenericFormula; public static log2(value: FormulaSource) { return new Formula({ @@ -1273,9 +1281,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static ln( - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static ln<T extends GenericFormula>(value: T): T; public static ln(value: FormulaSource): GenericFormula; public static ln(value: FormulaSource) { return new Formula({ @@ -1287,10 +1293,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static pow( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static pow<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static pow<T extends GenericFormula>(value: FormulaSource, other: T): T; public static pow(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1302,9 +1306,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static pow10( - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static pow10<T extends GenericFormula>(value: T): T; public static pow10(value: FormulaSource): GenericFormula; public static pow10(value: FormulaSource) { return new Formula({ @@ -1316,10 +1318,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static pow_base( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static pow_base<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static pow_base<T extends GenericFormula>(value: FormulaSource, other: T): T; public static pow_base(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow_base(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1331,10 +1331,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static root( - value: InvertibleFormulaSource, - other: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public static root<T extends GenericFormula>(value: T, other: FormulaSource): T; + public static root<T extends GenericFormula>(value: FormulaSource, other: T): T; public static root(value: FormulaSource, other: FormulaSource): GenericFormula; public static root(value: FormulaSource, other: FormulaSource) { return new Formula({ @@ -1358,7 +1356,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value], evaluate: Decimal.lngamma }); } - public static exp(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static exp<T extends GenericFormula>(value: T): Omit<T, "invertsIntegral">; public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource) { return new Formula({ @@ -1369,27 +1367,35 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } + public static sqr<T extends GenericFormula>(value: T): T; + public static sqr(value: FormulaSource): GenericFormula; public static sqr(value: FormulaSource) { return Formula.pow(value, 2); } + public static sqrt<T extends GenericFormula>(value: T): T; + public static sqrt(value: FormulaSource): GenericFormula; public static sqrt(value: FormulaSource) { return Formula.root(value, 2); } + public static cube<T extends GenericFormula>(value: T): T; + public static cube(value: FormulaSource): GenericFormula; public static cube(value: FormulaSource) { return Formula.pow(value, 3); } + public static cbrt<T extends GenericFormula>(value: T): T; + public static cbrt(value: FormulaSource): GenericFormula; public static cbrt(value: FormulaSource) { return Formula.root(value, 3); } - public static tetrate( - value: InvertibleFormulaSource, - height?: InvertibleFormulaSource, - payload?: InvertibleFormulaSource - ): InvertibleFormula; + public static tetrate<T extends GenericFormula>( + value: T, + height?: FormulaSource, + payload?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public static tetrate( value: FormulaSource, height?: FormulaSource, @@ -1407,11 +1413,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static iteratedexp( - value: InvertibleFormulaSource, - height?: InvertibleFormulaSource, - payload?: InvertibleFormulaSource - ): InvertibleFormula; + public static iteratedexp<T extends GenericFormula>( + value: T, + height?: FormulaSource, + payload?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public static iteratedexp( value: FormulaSource, height?: FormulaSource, @@ -1437,24 +1443,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value, base, times], evaluate: iteratedLog }); } - public static slog( - value: InvertibleFormulaSource, - base?: InvertibleFormulaSource - ): InvertibleFormula; + public static slog<T extends GenericFormula>( + value: T, + base?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { return new Formula({ inputs: [value, base], evaluate: slog, invert: invertSlog }); } - public static layeradd10(value: FormulaSource, diff: FormulaSource): GenericFormula { + public static layeradd10(value: FormulaSource, diff: FormulaSource) { return new Formula({ inputs: [value, diff], evaluate: Decimal.layeradd10 }); } - public static layeradd( - value: InvertibleFormulaSource, - diff: InvertibleFormulaSource, - base?: InvertibleFormulaSource - ): InvertibleFormula; + public static layeradd<T extends GenericFormula>( + value: T, + diff: FormulaSource, + base?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public static layeradd( value: FormulaSource, diff: FormulaSource, @@ -1468,13 +1474,17 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static lambertw(value: InvertibleFormulaSource): InvertibleFormula; + public static lambertw<T extends GenericFormula>( + value: T + ): Omit<T, "integrate" | "invertIntegral">; public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { return new Formula({ inputs: [value], evaluate: Decimal.lambertw, invert: invertLambertw }); } - public static ssqrt(value: InvertibleFormulaSource): InvertibleFormula; + public static ssqrt<T extends GenericFormula>( + value: T + ): Omit<T, "integrate" | "invertIntegral">; public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: invertSsqrt }); @@ -1484,11 +1494,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { value: FormulaSource, height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) - ) { + ): GenericFormula { return new Formula({ inputs: [value, height, payload], evaluate: pentate }); } - public static sin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static sin<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource) { return new Formula({ @@ -1499,7 +1509,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static cos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static cos<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource) { return new Formula({ @@ -1510,7 +1520,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static tan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static tan<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource) { return new Formula({ @@ -1521,7 +1531,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static asin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static asin<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource) { return new Formula({ @@ -1532,7 +1542,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static acos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static acos<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource) { return new Formula({ @@ -1543,7 +1553,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static atan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static atan<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource) { return new Formula({ @@ -1554,7 +1564,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static sinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static sinh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource) { return new Formula({ @@ -1565,7 +1575,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static cosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static cosh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource) { return new Formula({ @@ -1576,7 +1586,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static tanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static tanh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource) { return new Formula({ @@ -1587,7 +1597,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static asinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static asinh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource) { return new Formula({ @@ -1598,7 +1608,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static acosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static acosh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource) { return new Formula({ @@ -1609,7 +1619,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { }); } - public static atanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; + public static atanh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">; public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource) { return new Formula({ @@ -1644,13 +1654,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.abs(this); } - public neg() { + public neg<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public neg(this: GenericFormula): GenericFormula; + public neg(this: GenericFormula) { return Formula.neg(this); } - public negate() { + public negate<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public negate(this: GenericFormula): GenericFormula; + public negate(this: GenericFormula) { return Formula.neg(this); } - public negated() { + public negated<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public negated(this: GenericFormula): GenericFormula; + public negated(this: GenericFormula) { return Formula.neg(this); } @@ -1677,112 +1693,94 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.trunc(this); } - public add( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public add(this: FormulaSource, value: FormulaSource): GenericFormula; - public add(value: FormulaSource) { + public add<T extends GenericFormula>(this: T, value: FormulaSource): T; + public add<T extends GenericFormula>(this: GenericFormula, value: T): T; + public add(this: GenericFormula, value: FormulaSource): GenericFormula; + public add(this: GenericFormula, value: FormulaSource) { return Formula.add(this, value); } - public plus( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public plus(this: FormulaSource, value: FormulaSource): GenericFormula; + public plus<T extends GenericFormula>(this: T, value: FormulaSource): T; + public plus<T extends GenericFormula>(this: GenericFormula, value: T): T; + public plus(this: GenericFormula, value: FormulaSource): GenericFormula; public plus(value: FormulaSource) { return Formula.add(this, value); } - public sub( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public sub(this: FormulaSource, value: FormulaSource): GenericFormula; + public sub<T extends GenericFormula>(this: T, value: FormulaSource): T; + public sub<T extends GenericFormula>(this: GenericFormula, value: T): T; + public sub(this: GenericFormula, value: FormulaSource): GenericFormula; public sub(value: FormulaSource) { return Formula.sub(this, value); } - public subtract( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public subtract(this: FormulaSource, value: FormulaSource): GenericFormula; + public subtract<T extends GenericFormula>(this: T, value: FormulaSource): T; + public subtract<T extends GenericFormula>(this: GenericFormula, value: T): T; + public subtract(this: GenericFormula, value: FormulaSource): GenericFormula; public subtract(value: FormulaSource) { return Formula.sub(this, value); } - public minus( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public minus(this: FormulaSource, value: FormulaSource): GenericFormula; + public minus<T extends GenericFormula>(this: T, value: FormulaSource): T; + public minus<T extends GenericFormula>(this: GenericFormula, value: T): T; + public minus(this: GenericFormula, value: FormulaSource): GenericFormula; public minus(value: FormulaSource) { return Formula.sub(this, value); } - public mul( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public mul(this: FormulaSource, value: FormulaSource): GenericFormula; + public mul<T extends GenericFormula>(this: T, value: FormulaSource): T; + public mul<T extends GenericFormula>(this: GenericFormula, value: T): T; + public mul(this: GenericFormula, value: FormulaSource): GenericFormula; public mul(value: FormulaSource) { return Formula.mul(this, value); } - public multiply( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public multiply(this: FormulaSource, value: FormulaSource): GenericFormula; + public multiply<T extends GenericFormula>(this: T, value: FormulaSource): T; + public multiply<T extends GenericFormula>(this: GenericFormula, value: T): T; + public multiply(this: GenericFormula, value: FormulaSource): GenericFormula; public multiply(value: FormulaSource) { return Formula.mul(this, value); } - public times( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public times(this: FormulaSource, value: FormulaSource): GenericFormula; + public times<T extends GenericFormula>(this: T, value: FormulaSource): T; + public times<T extends GenericFormula>(this: GenericFormula, value: T): T; + public times(this: GenericFormula, value: FormulaSource): GenericFormula; public times(value: FormulaSource) { return Formula.mul(this, value); } - public div( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public div(this: FormulaSource, value: FormulaSource): GenericFormula; + public div<T extends GenericFormula>(this: T, value: FormulaSource): T; + public div<T extends GenericFormula>(this: GenericFormula, value: T): T; + public div(this: GenericFormula, value: FormulaSource): GenericFormula; public div(value: FormulaSource) { return Formula.div(this, value); } - public divide( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public divide(this: FormulaSource, value: FormulaSource): GenericFormula; + public divide<T extends GenericFormula>(this: T, value: FormulaSource): T; + public divide<T extends GenericFormula>(this: GenericFormula, value: T): T; + public divide(this: GenericFormula, value: FormulaSource): GenericFormula; public divide(value: FormulaSource) { return Formula.div(this, value); } - public divideBy( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public divideBy(this: FormulaSource, value: FormulaSource): GenericFormula; + public divideBy<T extends GenericFormula>(this: T, value: FormulaSource): T; + public divideBy<T extends GenericFormula>(this: GenericFormula, value: T): T; + public divideBy(this: GenericFormula, value: FormulaSource): GenericFormula; public divideBy(value: FormulaSource) { return Formula.div(this, value); } - public dividedBy( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; - public dividedBy(this: FormulaSource, value: FormulaSource): GenericFormula; + public dividedBy<T extends GenericFormula>(this: T, value: FormulaSource): T; + public dividedBy<T extends GenericFormula>(this: GenericFormula, value: T): T; + public dividedBy(this: GenericFormula, value: FormulaSource): GenericFormula; public dividedBy(value: FormulaSource) { return Formula.div(this, value); } + public recip<T extends GenericFormula>(this: T): T; + public recip(this: FormulaSource): GenericFormula; public recip() { return Formula.recip(this); } + public reciprocal<T extends GenericFormula>(this: T): T; + public reciprocal(this: FormulaSource): GenericFormula; public reciprocal() { return Formula.recip(this); } + public reciprocate<T extends GenericFormula>(this: T): T; + public reciprocate(this: FormulaSource): GenericFormula; public reciprocate() { return Formula.recip(this); } @@ -1823,61 +1821,59 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.absLog10(this); } + public log10<T extends GenericFormula>(this: T): T; + public log10(this: FormulaSource): GenericFormula; public log10() { return Formula.log10(this); } - public log( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public log<T extends GenericFormula>(this: T, value: FormulaSource): T; + public log<T extends GenericFormula>(this: FormulaSource, value: T): T; public log(this: FormulaSource, value: FormulaSource): GenericFormula; public log(value: FormulaSource) { return Formula.log(this, value); } - public logarithm( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public logarithm<T extends GenericFormula>(this: T, value: FormulaSource): T; + public logarithm<T extends GenericFormula>(this: FormulaSource, value: T): T; public logarithm(this: FormulaSource, value: FormulaSource): GenericFormula; public logarithm(value: FormulaSource) { return Formula.log(this, value); } + public log2<T extends GenericFormula>(this: T): T; + public log2(this: FormulaSource): GenericFormula; public log2() { return Formula.log2(this); } + public ln<T extends GenericFormula>(this: T): T; + public ln(this: FormulaSource): GenericFormula; public ln() { return Formula.ln(this); } - public pow( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow<T extends GenericFormula>(this: T, value: FormulaSource): T; + public pow<T extends GenericFormula>(this: FormulaSource, value: T): T; public pow(this: FormulaSource, value: FormulaSource): GenericFormula; public pow(value: FormulaSource) { return Formula.pow(this, value); } + public pow10<T extends GenericFormula>(this: T): T; + public pow10(this: FormulaSource): GenericFormula; public pow10() { return Formula.pow10(this); } - public pow_base( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow_base<T extends GenericFormula>(this: T, value: FormulaSource): T; + public pow_base<T extends GenericFormula>(this: FormulaSource, value: T): T; public pow_base(this: FormulaSource, value: FormulaSource): GenericFormula; public pow_base(value: FormulaSource) { return Formula.pow_base(this, value); } - public root( - this: InvertibleFormulaSource, - value: InvertibleFormulaSource - ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public root<T extends GenericFormula>(this: T, value: FormulaSource): T; + public root<T extends GenericFormula>(this: FormulaSource, value: T): T; public root(this: FormulaSource, value: FormulaSource): GenericFormula; public root(value: FormulaSource) { return Formula.root(this, value); @@ -1894,53 +1890,65 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.lngamma(this); } - public exp() { + public exp<T extends GenericFormula>(this: T): Omit<T, "invertsIntegral">; + public exp(this: FormulaSource): GenericFormula; + public exp(this: FormulaSource) { return Formula.exp(this); } + public sqr<T extends GenericFormula>(this: T): T; + public sqr(this: FormulaSource): GenericFormula; public sqr() { return Formula.pow(this, 2); } + public sqrt<T extends GenericFormula>(this: T): T; + public sqrt(this: FormulaSource): GenericFormula; public sqrt() { return Formula.root(this, 2); } + public cube<T extends GenericFormula>(this: T): T; + public cube(this: FormulaSource): GenericFormula; public cube() { return Formula.pow(this, 3); } + public cbrt<T extends GenericFormula>(this: T): T; + public cbrt(this: FormulaSource): GenericFormula; public cbrt() { return Formula.root(this, 3); } - public tetrate( - this: InvertibleFormulaSource, - height: InvertibleFormulaSource, - payload: InvertibleFormulaSource - ): InvertibleFormula; + public tetrate<T extends GenericFormula>( + this: T, + height?: FormulaSource, + payload?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public tetrate( this: FormulaSource, - height: FormulaSource, - payload: FormulaSource + height?: FormulaSource, + payload?: FormulaSource ): GenericFormula; public tetrate( + this: FormulaSource, height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { return Formula.tetrate(this, height, payload); } - public iteratedexp( - this: InvertibleFormulaSource, - height: InvertibleFormulaSource, - payload: InvertibleFormulaSource - ): InvertibleFormula; + public iteratedexp<T extends GenericFormula>( + this: T, + height?: FormulaSource, + payload?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public iteratedexp( this: FormulaSource, - height: FormulaSource, - payload: FormulaSource + height?: FormulaSource, + payload?: FormulaSource ): GenericFormula; public iteratedexp( + this: FormulaSource, height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { @@ -1951,9 +1959,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.iteratedlog(this, base, times); } - public slog(this: InvertibleFormulaSource, base?: InvertibleFormulaSource): InvertibleFormula; + public slog<T extends GenericFormula>( + this: T, + base?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public slog(this: FormulaSource, base?: FormulaSource): GenericFormula; - public slog(base: FormulaSource = 10) { + public slog(this: FormulaSource, base: FormulaSource = 10) { return Formula.slog(this, base); } @@ -1961,21 +1972,25 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.layeradd10(this, diff); } - public layeradd( - this: InvertibleFormulaSource, - diff: InvertibleFormulaSource, - base?: InvertibleFormulaSource - ): InvertibleFormula; + public layeradd<T extends GenericFormula>( + this: T, + diff: FormulaSource, + base?: FormulaSource + ): Omit<T, "integrate" | "invertIntegral">; public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula; - public layeradd(diff: FormulaSource, base: FormulaSource) { + public layeradd(this: FormulaSource, diff: FormulaSource, base: FormulaSource) { return Formula.layeradd(this, diff, base); } - public lambertw() { + public lambertw<T extends GenericFormula>(this: T): Omit<T, "integrate" | "invertIntegral">; + public lambertw(this: FormulaSource): GenericFormula; + public lambertw(this: FormulaSource) { return Formula.lambertw(this); } - public ssqrt() { + public ssqrt<T extends GenericFormula>(this: T): Omit<T, "integrate" | "invertIntegral">; + public ssqrt(this: FormulaSource): GenericFormula; + public ssqrt(this: FormulaSource) { return Formula.ssqrt(this); } @@ -1986,51 +2001,75 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return Formula.pentate(this, height, payload); } - public sin() { + public sin<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public sin(this: FormulaSource): GenericFormula; + public sin(this: FormulaSource) { return Formula.sin(this); } - public cos() { + public cos<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public cos(this: FormulaSource): GenericFormula; + public cos(this: FormulaSource) { return Formula.cos(this); } - public tan() { + public tan<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public tan(this: FormulaSource): GenericFormula; + public tan(this: FormulaSource) { return Formula.tan(this); } - public asin() { + public asin<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public asin(this: FormulaSource): GenericFormula; + public asin(this: FormulaSource) { return Formula.asin(this); } - public acos() { + public acos<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public acos(this: FormulaSource): GenericFormula; + public acos(this: FormulaSource) { return Formula.acos(this); } - public atan() { + public atan<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public atan(this: FormulaSource): GenericFormula; + public atan(this: FormulaSource) { return Formula.atan(this); } - public sinh() { + public sinh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public sinh(this: FormulaSource): GenericFormula; + public sinh(this: FormulaSource) { return Formula.sinh(this); } - public cosh() { + public cosh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public cosh(this: FormulaSource): GenericFormula; + public cosh(this: FormulaSource) { return Formula.cosh(this); } - public tanh() { + public tanh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public tanh(this: FormulaSource): GenericFormula; + public tanh(this: FormulaSource) { return Formula.tanh(this); } - public asinh() { + public asinh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public asinh(this: FormulaSource): GenericFormula; + public asinh(this: FormulaSource) { return Formula.asinh(this); } - public acosh() { + public acosh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public acosh(this: FormulaSource): GenericFormula; + public acosh(this: FormulaSource) { return Formula.acosh(this); } - public atanh() { + public atanh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">; + public atanh(this: FormulaSource): GenericFormula; + public atanh(this: FormulaSource) { return Formula.atanh(this); } } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index fdfa1fd..47f8d26 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -250,7 +250,6 @@ describe("Creating Formulas", () => { } testConstant("number", () => Formula.constant(10)); testConstant("string", () => Formula.constant("10")); - testConstant("formula", () => Formula.constant(Formula.constant(10))); testConstant("decimal", () => Formula.constant(new Decimal("1e400")), "1e400"); testConstant("ref", () => Formula.constant(ref(10))); }); @@ -532,6 +531,12 @@ describe("Inverting", () => { const formula = Formula.add(variable, constant).times(constant); expect(formula.invert(100)).compare_tolerance(0); }); + + test("Inverting with non-invertible sections", () => { + const formula = Formula.add(variable, constant.ceil()); + expect(formula.isInvertible()).toBe(true); + expect(formula.invert(10)).compare_tolerance(7); + }); }); describe("Integrating", () => { From b3d61149c4f8bcd5edc9694e3c8fefd4f8dbaea4 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 9 Feb 2023 07:41:58 -0600 Subject: [PATCH 29/56] Fix some requirements tests --- tests/game/requirements.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts index 3a26dbc..73d539f 100644 --- a/tests/game/requirements.test.ts +++ b/tests/game/requirements.test.ts @@ -151,7 +151,7 @@ describe("Checking maximum levels of requirements met", () => { cost: Formula.variable(0) })) ]; - expect(maxRequirementsMet(requirements)).toBe(0); + expect(maxRequirementsMet(requirements)).compare_tolerance(0); }); test("Returns correct number of requirements met", () => { @@ -162,7 +162,7 @@ describe("Checking maximum levels of requirements met", () => { cost: Formula.variable(0) })) ]; - expect(maxRequirementsMet(requirements)).toBe(10); + expect(maxRequirementsMet(requirements)).compare_tolerance(10); }); }); From fd925071e59bc0fc25f66c7f6b9df1b462091290 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 01:12:11 -0600 Subject: [PATCH 30/56] Fix some tests --- src/game/formulas.ts | 55 ++++++++++++++++++++++++++----------- tests/game/formulas.test.ts | 32 ++++++++++++++------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 10b3fcd..253e0db 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -829,7 +829,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.internalInvertIntegral = - this.internalHasVariable && variable?.isIntegralInvertible() ? invert : undefined; + this.internalHasVariable && variable?.isIntegralInvertible() + ? invertIntegral + : undefined; } /** Type predicate that this formula can be inverted. */ @@ -847,7 +849,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { /** Type predicate that this formula has an integral function that can be inverted. */ isIntegralInvertible(): this is InvertibleIntegralFormula { - return this.internalHasVariable && this.internalInvertIntegral != null; + return ( + this.internalHasVariable && + (this.internalInvertIntegral != null || this.internalEvaluate == null) + ); } /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */ @@ -892,11 +897,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { * @see {@link isIntegrable} */ evaluateIntegral(variable?: DecimalSource): DecimalSource { - return ( - this.internalIntegrate?.call(this, variable, ...this.inputs) ?? - variable ?? - unrefFormulaSource(this.inputs[0]) - ); + if (this.internalIntegrate) { + return this.internalIntegrate.call(this, variable, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return variable ?? unrefFormulaSource(this.inputs[0]); + } + throw "Cannot integrate formula without variable"; } /** @@ -907,7 +913,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral(value: DecimalSource): DecimalSource { // This is nearly completely non-functional // Proper nesting will require somehow using integration by substitution or integration by parts - return this.internalInvertIntegral?.call(this, value, ...this.inputs) ?? value; + if (this.internalInvertIntegral) { + return this.internalInvertIntegral.call(this, value, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return value; + } + throw "Cannot invert integral of formula without invertible integral"; } /** @@ -962,10 +973,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static step( value: FormulaSource, start: Computable<DecimalSource>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ): GenericFormula { const lhsRef = ref<DecimalSource>(0); - const formula = formulaModifier(lhsRef); + const formula = formulaModifier(Formula.variable(lhsRef)); const processedStart = convertComputable(start); function evalStep(lhs: DecimalSource) { if (Decimal.lt(lhs, unref(processedStart))) { @@ -1002,10 +1015,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static if( value: FormulaSource, condition: Computable<boolean>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ): GenericFormula { const lhsRef = ref<DecimalSource>(0); - const formula = formulaModifier(lhsRef); + const formula = formulaModifier(Formula.variable(lhsRef)); const processedCondition = convertComputable(condition); function evalStep(lhs: DecimalSource) { if (unref(processedCondition)) { @@ -1035,7 +1050,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public static conditional( value: FormulaSource, condition: Computable<boolean>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ) { return Formula.if(value, condition, formulaModifier); } @@ -1632,20 +1649,26 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { public step( start: Computable<DecimalSource>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ) { return Formula.step(this, start, formulaModifier); } public if( condition: Computable<boolean>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ) { return Formula.if(this, condition, formulaModifier); } public conditional( condition: Computable<boolean>, - formulaModifier: (value: Ref<DecimalSource>) => GenericFormula + formulaModifier: ( + value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => GenericFormula ) { return Formula.if(this, condition, formulaModifier); } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 47f8d26..c00d43b 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -569,8 +569,8 @@ describe("Integrating", () => { checkFormula(Formula[name](variable, constant))); test(`${name}(const, var) is marked as integrable`, () => checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as integrable`, () => - checkFormula(Formula[name](variable, variable))); + test(`${name}(var, var) is marked as not integrable`, () => + expect(Formula[name](variable, variable).isIntegrable()).toBe(false)); }); }); }); @@ -872,20 +872,29 @@ describe("Custom Formulas", () => { describe("Formula with invert", () => { test("Zero input inverts correctly", () => expect( - new Formula({ inputs: [], evaluate: () => 6, invert: value => value }).invert(10) + new Formula({ + inputs: [], + evaluate: () => 6, + invert: value => value, + variable: ref(10) + }).invert(10) ).compare_tolerance(10)); test("One input inverts correctly", () => expect( - new Formula({ inputs: [1], evaluate: () => 10, invert: (value, v1) => v1 }).invert( - 10 - ) + new Formula({ + inputs: [1], + evaluate: () => 10, + invert: (value, v1) => v1, + variable: ref(10) + }).invert(10) ).compare_tolerance(1)); test("Two inputs inverts correctly", () => expect( new Formula({ inputs: [1, 2], evaluate: () => 10, - invert: (value, v1, v2) => v2 + invert: (value, v1, v2) => v2, + variable: ref(10) }).invert(10) ).compare_tolerance(2)); }); @@ -923,7 +932,8 @@ describe("Custom Formulas", () => { new Formula({ inputs: [], evaluate: () => 10, - invertIntegral: () => 1 + invertIntegral: () => 1, + variable: ref(10) }).invertIntegral(8) ).compare_tolerance(1)); test("One input inverts integral correctly", () => @@ -931,7 +941,8 @@ describe("Custom Formulas", () => { new Formula({ inputs: [1], evaluate: () => 10, - invertIntegral: val => 1 + invertIntegral: val => 1, + variable: ref(10) }).invertIntegral(8) ).compare_tolerance(1)); test("Two inputs inverts integral correctly", () => @@ -939,7 +950,8 @@ describe("Custom Formulas", () => { new Formula({ inputs: [1, 2], evaluate: (v1, v2) => 10, - invertIntegral: (v1, v2) => 1 + invertIntegral: (v1, v2) => 1, + variable: ref(10) }).invertIntegral(8) ).compare_tolerance(1)); }); From 553c6a4554c918e3e7cb7a29118024198ca7badb Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 02:41:33 -0600 Subject: [PATCH 31/56] Make clamping functions pass-throughs for inverting --- src/game/formulas.ts | 95 ++++++++++++++++++++++++++--- tests/game/formulas.test.ts | 116 ++++++++++++++++++++---------------- 2 files changed, 153 insertions(+), 58 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 253e0db..080dba8 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -56,6 +56,10 @@ export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSourc return value instanceof Formula ? value.evaluate(variable) : unref(value); } +function passthrough(value: DecimalSource) { + return value; +} + function invertNeg(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.neg(value)); @@ -1216,19 +1220,63 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { } public static max(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, other], evaluate: Decimal.max }); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.max, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static min(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, other], evaluate: Decimal.min }); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.min, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, other], evaluate: Decimal.minabs }); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.minabs, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, other], evaluate: Decimal.maxabs }); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.maxabs, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static clamp( @@ -1236,15 +1284,48 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { min: FormulaSource, max: FormulaSource ): GenericFormula { - return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp }); + return new Formula({ + inputs: [value, min, max], + evaluate: Decimal.clamp, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, min], evaluate: Decimal.clampMin }); + return new Formula({ + inputs: [value, min], + evaluate: Decimal.clampMin, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { - return new Formula({ inputs: [value, max], evaluate: Decimal.clampMax }); + return new Formula({ + inputs: [value, max], + evaluate: Decimal.clampMax, + invert: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource, + invertIntegral: passthrough as ( + value: DecimalSource, + ...inputs: [FormulaSource, FormulaSource] + ) => DecimalSource + }); } public static pLog10(value: FormulaSource): GenericFormula { diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index c00d43b..f5f12a3 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -124,22 +124,14 @@ const invertibleOneParamFunctionNames = [ "root", "slog" ] as const; -const nonInvertibleOneParamFunctionNames = [ - "max", - "min", - "maxabs", - "minabs", - "clampMin", - "clampMax", - "layeradd10" -] as const; +const nonInvertibleOneParamFunctionNames = ["layeradd10"] as const; const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const; const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const; const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames; const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames; const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; -const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; +const nonInvertibleTwoParamFunctionNames = ["iteratedlog", "pentate"] as const; const nonIntegrableTwoParamFunctionNames = [ ...invertibleTwoParamFunctionNames, ...nonInvertibleZeroParamFunctionNames @@ -308,24 +300,28 @@ describe("Creating Formulas", () => { }); } - describe("Invertible 0-param", () => { - invertibleZeroParamFunctionNames.forEach(names => - describe(names, () => { - checkFormula(names, [0] as const); - testValues.forEach(i => testFormulaCall(names, [i] as const)); - }) + describe("0-param", () => { + [...invertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames].forEach( + names => + describe(names, () => { + checkFormula(names, [0] as const); + testValues.forEach(i => testFormulaCall(names, [i] as const)); + }) ); }); - describe("Non-Invertible 0-param", () => { - nonInvertibleZeroParamFunctionNames.forEach(names => - describe(names, () => { - checkFormula(names, [0] as const); - testValues.forEach(i => testFormulaCall(names, [i] as const)); - }) - ); - }); - describe("Invertible 1-param", () => { - invertibleOneParamFunctionNames.forEach(names => + describe("1-param", () => { + ( + [ + ...invertibleOneParamFunctionNames, + ...nonInvertibleOneParamFunctionNames, + "max", + "min", + "maxabs", + "minabs", + "clampMin", + "clampMax" + ] as const + ).forEach(names => describe(names, () => { checkFormula(names, [0, 0] as const); testValues.forEach(i => @@ -334,30 +330,14 @@ describe("Creating Formulas", () => { }) ); }); - describe("Non-Invertible 1-param", () => { - nonInvertibleOneParamFunctionNames.forEach(names => - describe(names, () => { - checkFormula(names, [0, 0] as const); - testValues.forEach(i => - testValues.forEach(j => testFormulaCall(names, [i, j] as const)) - ); - }) - ); - }); - describe("Invertible 2-param", () => { - invertibleTwoParamFunctionNames.forEach(names => - describe(names, () => { - checkFormula(names, [0, 0, 0] as const); - testValues.forEach(i => - testValues.forEach(j => - testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) - ) - ); - }) - ); - }); - describe("Non-Invertible 2-param", () => { - nonInvertibleTwoParamFunctionNames.forEach(names => + describe("2-param", () => { + ( + [ + ...invertibleTwoParamFunctionNames, + ...nonInvertibleTwoParamFunctionNames, + "clamp" + ] as const + ).forEach(names => describe(names, () => { checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => @@ -527,6 +507,21 @@ describe("Inverting", () => { ); }); + describe("Inverting pass-throughs", () => { + test("max", () => expect(Formula.max(variable, constant).invert(10)).compare_tolerance(10)); + test("min", () => expect(Formula.min(variable, constant).invert(10)).compare_tolerance(10)); + test("minabs", () => + expect(Formula.minabs(variable, constant).invert(10)).compare_tolerance(10)); + test("maxabs", () => + expect(Formula.maxabs(variable, constant).invert(10)).compare_tolerance(10)); + test("clampMax", () => + expect(Formula.clampMax(variable, constant).invert(10)).compare_tolerance(10)); + test("clampMin", () => + expect(Formula.clampMin(variable, constant).invert(10)).compare_tolerance(10)); + test("clamp", () => + expect(Formula.clamp(variable, constant, constant).invert(10)).compare_tolerance(10)); + }); + test("Inverting nested formulas", () => { const formula = Formula.add(variable, constant).times(constant); expect(formula.invert(100)).compare_tolerance(0); @@ -705,6 +700,25 @@ describe("Inverting integrals", () => { const formula = Formula.add(variable, constant).times(constant); expect(formula.invertIntegral(1500)).compare_tolerance(10); }); + + describe("Inverting integral pass-throughs", () => { + test("max", () => + expect(Formula.max(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("min", () => + expect(Formula.min(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("minabs", () => + expect(Formula.minabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("maxabs", () => + expect(Formula.maxabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("clampMax", () => + expect(Formula.clampMax(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("clampMin", () => + expect(Formula.clampMin(variable, constant).invertIntegral(10)).compare_tolerance(10)); + test("clamp", () => + expect( + Formula.clamp(variable, constant, constant).invertIntegral(10) + ).compare_tolerance(10)); + }); }); describe("Step-wise", () => { @@ -980,7 +994,7 @@ describe("Buy Max", () => { }); describe("Without spending", () => { test("Throws on non-invertible formula", () => { - const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false); + const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p From 8987c0c69f91572ff0a069e1374ae189148528cd Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 13:00:31 -0600 Subject: [PATCH 32/56] More test fixes --- src/game/formulas.ts | 4 ++-- tests/game/formulas.test.ts | 41 ++++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 080dba8..e812de3 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1006,7 +1006,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value], evaluate: evalStep, - invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined }); } @@ -1047,7 +1047,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> { return new Formula({ inputs: [value], evaluate: evalStep, - invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined }); } /** @see {@link if} */ diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index f5f12a3..0be2795 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -647,8 +647,11 @@ describe("Inverting integrals", () => { checkFormula(Formula[name](variable, constant))); test(`${name}(const, var) is marked as having an invertible integral`, () => checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as having an invertible integral`, () => - checkFormula(Formula[name](variable, variable))); + test(`${name}(var, var) is marked as not having an invertible integral`, () => { + const formula = Formula[name](variable, variable); + expect(formula.isIntegralInvertible()).toBe(false); + expect(() => formula.invertIntegral(10)).to.throw(); + }); }); }); }); @@ -659,35 +662,35 @@ describe("Inverting integrals", () => { } nonInvertibleIntegralZeroPramFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var) is marked as not integrable`, () => + test(`${name}(var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable))); }); }); nonInvertibleIntegralOneParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const) is marked as not integrable`, () => + test(`${name}(var, const) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as not integrable`, () => + test(`${name}(const, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as not integrable`, () => + test(`${name}(var, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, variable))); }); }); nonInvertibleIntegralTwoParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const, const) is marked as not integrable`, () => + test(`${name}(var, const, const) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as not integrable`, () => + test(`${name}(const, var, const) is marked as not having an invertible integral`, () => checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as not integrable`, () => + test(`${name}(const, const, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not integrable`, () => + test(`${name}(var, var, const) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, variable, constant))); - test(`${name}(var, const, var) is marked as not integrable`, () => + test(`${name}(var, const, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, constant, variable))); - test(`${name}(const, var, var) is marked as not integrable`, () => + test(`${name}(const, var, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](constant, variable, variable))); - test(`${name}(var, var, var) is marked as not integrable`, () => + test(`${name}(var, var, var) is marked as not having an invertible integral`, () => checkFormula(Formula[name](variable, variable, variable))); }); }); @@ -890,7 +893,7 @@ describe("Custom Formulas", () => { inputs: [], evaluate: () => 6, invert: value => value, - variable: ref(10) + hasVariable: true }).invert(10) ).compare_tolerance(10)); test("One input inverts correctly", () => @@ -899,7 +902,7 @@ describe("Custom Formulas", () => { inputs: [1], evaluate: () => 10, invert: (value, v1) => v1, - variable: ref(10) + hasVariable: true }).invert(10) ).compare_tolerance(1)); test("Two inputs inverts correctly", () => @@ -908,7 +911,7 @@ describe("Custom Formulas", () => { inputs: [1, 2], evaluate: () => 10, invert: (value, v1, v2) => v2, - variable: ref(10) + hasVariable: true }).invert(10) ).compare_tolerance(2)); }); @@ -947,7 +950,7 @@ describe("Custom Formulas", () => { inputs: [], evaluate: () => 10, invertIntegral: () => 1, - variable: ref(10) + hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); test("One input inverts integral correctly", () => @@ -956,7 +959,7 @@ describe("Custom Formulas", () => { inputs: [1], evaluate: () => 10, invertIntegral: val => 1, - variable: ref(10) + hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); test("Two inputs inverts integral correctly", () => @@ -965,7 +968,7 @@ describe("Custom Formulas", () => { inputs: [1, 2], evaluate: (v1, v2) => 10, invertIntegral: (v1, v2) => 1, - variable: ref(10) + hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); }); From ccb8e76eaffb2ea9ac4e54eacc1a254820fd35ee Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 13:23:59 -0600 Subject: [PATCH 33/56] Renamed buyables to repeatables --- src/features/{buyable.tsx => repeatable.tsx} | 142 ++++++++++--------- 1 file changed, 74 insertions(+), 68 deletions(-) rename src/features/{buyable.tsx => repeatable.tsx} (60%) diff --git a/src/features/buyable.tsx b/src/features/repeatable.tsx similarity index 60% rename from src/features/buyable.tsx rename to src/features/repeatable.tsx index 89dcada..34ce057 100644 --- a/src/features/buyable.tsx +++ b/src/features/repeatable.tsx @@ -25,9 +25,9 @@ import { coerceComponent, isCoercableComponent } from "util/vue"; import type { Ref } from "vue"; import { computed, unref } from "vue"; -export const BuyableType = Symbol("Buyable"); +export const RepeatableType = Symbol("Repeatable"); -export type BuyableDisplay = +export type RepeatableDisplay = | CoercableComponent | { title?: CoercableComponent; @@ -36,7 +36,7 @@ export type BuyableDisplay = showAmount?: boolean; }; -export interface BuyableOptions { +export interface RepeatableOptions { visibility?: Computable<Visibility>; requirements: Requirements; purchaseLimit?: Computable<DecimalSource>; @@ -46,24 +46,24 @@ export interface BuyableOptions { mark?: Computable<boolean | string>; small?: Computable<boolean>; buyMax?: Computable<boolean>; - display?: Computable<BuyableDisplay>; + display?: Computable<RepeatableDisplay>; onPurchase?: VoidFunction; } -export interface BaseBuyable { +export interface BaseRepeatable { id: string; amount: Persistent<DecimalSource>; maxed: Ref<boolean>; canClick: ProcessedComputable<boolean>; onClick: VoidFunction; purchase: VoidFunction; - type: typeof BuyableType; + type: typeof RepeatableType; [Component]: typeof ClickableComponent; [GatherProps]: () => Record<string, unknown>; } -export type Buyable<T extends BuyableOptions> = Replace< - T & BaseBuyable, +export type Repeatable<T extends RepeatableOptions> = Replace< + T & BaseRepeatable, { visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>; requirements: GetComputableType<T["requirements"]>; @@ -77,90 +77,96 @@ export type Buyable<T extends BuyableOptions> = Replace< } >; -export type GenericBuyable = Replace< - Buyable<BuyableOptions>, +export type GenericRepeatable = Replace< + Repeatable<RepeatableOptions>, { visibility: ProcessedComputable<Visibility>; purchaseLimit: ProcessedComputable<DecimalSource>; } >; -export function createBuyable<T extends BuyableOptions>( - optionsFunc: OptionsFunc<T, BaseBuyable, GenericBuyable> -): Buyable<T> { +export function createRepeatable<T extends RepeatableOptions>( + optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable> +): Repeatable<T> { const amount = persistent<DecimalSource>(0); return createLazyProxy(() => { - const buyable = optionsFunc(); + const repeatable = optionsFunc(); - buyable.id = getUniqueID("buyable-"); - buyable.type = BuyableType; - buyable[Component] = ClickableComponent; + repeatable.id = getUniqueID("repeatable-"); + repeatable.type = RepeatableType; + repeatable[Component] = ClickableComponent; - buyable.amount = amount; - buyable.amount[DefaultValue] = buyable.initialValue ?? 0; + repeatable.amount = amount; + repeatable.amount[DefaultValue] = repeatable.initialValue ?? 0; const limitRequirement = { requirementMet: computed(() => Decimal.sub( - unref((buyable as GenericBuyable).purchaseLimit), - (buyable as GenericBuyable).amount.value + unref((repeatable as GenericRepeatable).purchaseLimit), + (repeatable as GenericRepeatable).amount.value ) ), 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); + const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable); + if (isArray(repeatable.requirements)) { + repeatable.requirements.unshift(visibilityRequirement); + repeatable.requirements.push(limitRequirement); } else { - buyable.requirements = [visibilityRequirement, buyable.requirements, limitRequirement]; + repeatable.requirements = [ + visibilityRequirement, + repeatable.requirements, + limitRequirement + ]; } - buyable.maxed = computed(() => + repeatable.maxed = computed(() => Decimal.gte( - (buyable as GenericBuyable).amount.value, - unref((buyable as GenericBuyable).purchaseLimit) + (repeatable as GenericRepeatable).amount.value, + unref((repeatable as GenericRepeatable).purchaseLimit) ) ); - processComputable(buyable as T, "classes"); - const classes = buyable.classes as ProcessedComputable<Record<string, boolean>> | undefined; - buyable.classes = computed(() => { + processComputable(repeatable as T, "classes"); + const classes = repeatable.classes as + | ProcessedComputable<Record<string, boolean>> + | undefined; + repeatable.classes = computed(() => { const currClasses = unref(classes) || {}; - if ((buyable as GenericBuyable).maxed.value) { + if ((repeatable as GenericRepeatable).maxed.value) { currClasses.bought = true; } return currClasses; }); - buyable.canClick = computed(() => requirementsMet(buyable.requirements)); - buyable.onClick = buyable.purchase = - buyable.onClick ?? - buyable.purchase ?? - function (this: GenericBuyable) { - const genericBuyable = buyable as GenericBuyable; - if (!unref(genericBuyable.canClick)) { + repeatable.canClick = computed(() => requirementsMet(repeatable.requirements)); + repeatable.onClick = repeatable.purchase = + repeatable.onClick ?? + repeatable.purchase ?? + function (this: GenericRepeatable) { + const genericRepeatable = repeatable as GenericRepeatable; + if (!unref(genericRepeatable.canClick)) { return; } payRequirements( - buyable.requirements, - unref(genericBuyable.buyMax) - ? maxRequirementsMet(genericBuyable.requirements) + repeatable.requirements, + unref(genericRepeatable.buyMax) + ? maxRequirementsMet(genericRepeatable.requirements) : 1 ); - genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); - genericBuyable.onPurchase?.(); + genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1); + genericRepeatable.onPurchase?.(); }; - processComputable(buyable as T, "display"); - const display = buyable.display; - buyable.display = jsx(() => { + processComputable(repeatable as T, "display"); + const display = repeatable.display; + repeatable.display = jsx(() => { // TODO once processComputable types correctly, remove this "as X" - const currDisplay = unref(display) as BuyableDisplay; + const currDisplay = unref(display) as RepeatableDisplay; if (isCoercableComponent(currDisplay)) { const CurrDisplay = coerceComponent(currDisplay); return <CurrDisplay />; } if (currDisplay != null) { - const genericBuyable = buyable as GenericBuyable; + const genericRepeatable = repeatable as GenericRepeatable; const Title = coerceComponent(currDisplay.title ?? "", "h3"); const Description = coerceComponent(currDisplay.description ?? ""); const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? ""); @@ -176,12 +182,12 @@ export function createBuyable<T extends BuyableOptions>( {currDisplay.showAmount === false ? null : ( <div> <br /> - {unref(genericBuyable.purchaseLimit) === Decimal.dInf ? ( - <>Amount: {formatWhole(genericBuyable.amount.value)}</> + {unref(genericRepeatable.purchaseLimit) === Decimal.dInf ? ( + <>Amount: {formatWhole(genericRepeatable.amount.value)}</> ) : ( <> - Amount: {formatWhole(genericBuyable.amount.value)} /{" "} - {formatWhole(unref(genericBuyable.purchaseLimit))} + Amount: {formatWhole(genericRepeatable.amount.value)} /{" "} + {formatWhole(unref(genericRepeatable.purchaseLimit))} </> )} </div> @@ -192,13 +198,13 @@ export function createBuyable<T extends BuyableOptions>( Currently: <EffectDisplay /> </div> )} - {genericBuyable.maxed.value ? null : ( + {genericRepeatable.maxed.value ? null : ( <div> <br /> {displayRequirements( - genericBuyable.requirements, - unref(genericBuyable.buyMax) - ? maxRequirementsMet(genericBuyable.requirements) + genericRepeatable.requirements, + unref(genericRepeatable.buyMax) + ? maxRequirementsMet(genericRepeatable.requirements) : 1 )} </div> @@ -209,16 +215,16 @@ export function createBuyable<T extends BuyableOptions>( return ""; }); - processComputable(buyable as T, "visibility"); - setDefault(buyable, "visibility", Visibility.Visible); - processComputable(buyable as T, "purchaseLimit"); - setDefault(buyable, "purchaseLimit", Decimal.dInf); - processComputable(buyable as T, "style"); - processComputable(buyable as T, "mark"); - processComputable(buyable as T, "small"); - processComputable(buyable as T, "buyMax"); + processComputable(repeatable as T, "visibility"); + setDefault(repeatable, "visibility", Visibility.Visible); + processComputable(repeatable as T, "purchaseLimit"); + setDefault(repeatable, "purchaseLimit", Decimal.dInf); + processComputable(repeatable as T, "style"); + processComputable(repeatable as T, "mark"); + processComputable(repeatable as T, "small"); + processComputable(repeatable as T, "buyMax"); - buyable[GatherProps] = function (this: GenericBuyable) { + repeatable[GatherProps] = function (this: GenericRepeatable) { const { display, visibility, style, classes, onClick, canClick, small, mark, id } = this; return { @@ -234,6 +240,6 @@ export function createBuyable<T extends BuyableOptions>( }; }; - return buyable as unknown as Buyable<T>; + return repeatable as unknown as Repeatable<T>; }); } From 712fcf7eb0751b333988c6a3b1d8738540cd3dfe Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 21:35:05 -0600 Subject: [PATCH 34/56] Rename repeatable.purchaseLimit to limit and initialValue to initialAmount --- src/features/repeatable.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 34ce057..ddfe965 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -39,8 +39,8 @@ export type RepeatableDisplay = export interface RepeatableOptions { visibility?: Computable<Visibility>; requirements: Requirements; - purchaseLimit?: Computable<DecimalSource>; - initialValue?: DecimalSource; + limit?: Computable<DecimalSource>; + initialAmount?: DecimalSource; classes?: Computable<Record<string, boolean>>; style?: Computable<StyleValue>; mark?: Computable<boolean | string>; @@ -67,7 +67,7 @@ export type Repeatable<T extends RepeatableOptions> = Replace< { visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>; requirements: GetComputableType<T["requirements"]>; - purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>; + limit: GetComputableTypeWithDefault<T["limit"], Decimal>; classes: GetComputableType<T["classes"]>; style: GetComputableType<T["style"]>; mark: GetComputableType<T["mark"]>; @@ -81,7 +81,7 @@ export type GenericRepeatable = Replace< Repeatable<RepeatableOptions>, { visibility: ProcessedComputable<Visibility>; - purchaseLimit: ProcessedComputable<DecimalSource>; + limit: ProcessedComputable<DecimalSource>; } >; @@ -97,12 +97,12 @@ export function createRepeatable<T extends RepeatableOptions>( repeatable[Component] = ClickableComponent; repeatable.amount = amount; - repeatable.amount[DefaultValue] = repeatable.initialValue ?? 0; + repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0; const limitRequirement = { requirementMet: computed(() => Decimal.sub( - unref((repeatable as GenericRepeatable).purchaseLimit), + unref((repeatable as GenericRepeatable).limit), (repeatable as GenericRepeatable).amount.value ) ), @@ -124,7 +124,7 @@ export function createRepeatable<T extends RepeatableOptions>( repeatable.maxed = computed(() => Decimal.gte( (repeatable as GenericRepeatable).amount.value, - unref((repeatable as GenericRepeatable).purchaseLimit) + unref((repeatable as GenericRepeatable).limit) ) ); processComputable(repeatable as T, "classes"); @@ -182,12 +182,12 @@ export function createRepeatable<T extends RepeatableOptions>( {currDisplay.showAmount === false ? null : ( <div> <br /> - {unref(genericRepeatable.purchaseLimit) === Decimal.dInf ? ( + {unref(genericRepeatable.limit) === Decimal.dInf ? ( <>Amount: {formatWhole(genericRepeatable.amount.value)}</> ) : ( <> Amount: {formatWhole(genericRepeatable.amount.value)} /{" "} - {formatWhole(unref(genericRepeatable.purchaseLimit))} + {formatWhole(unref(genericRepeatable.limit))} </> )} </div> @@ -217,8 +217,8 @@ export function createRepeatable<T extends RepeatableOptions>( processComputable(repeatable as T, "visibility"); setDefault(repeatable, "visibility", Visibility.Visible); - processComputable(repeatable as T, "purchaseLimit"); - setDefault(repeatable, "purchaseLimit", Decimal.dInf); + processComputable(repeatable as T, "limit"); + setDefault(repeatable, "limit", Decimal.dInf); processComputable(repeatable as T, "style"); processComputable(repeatable as T, "mark"); processComputable(repeatable as T, "small"); From 4fb2d90dbbbff01585f7ed00c8b58586e4d9837b Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 21:47:18 -0600 Subject: [PATCH 35/56] buyMax -> maximize --- src/features/repeatable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index ddfe965..55d29bf 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -45,7 +45,7 @@ export interface RepeatableOptions { style?: Computable<StyleValue>; mark?: Computable<boolean | string>; small?: Computable<boolean>; - buyMax?: Computable<boolean>; + maximize?: Computable<boolean>; display?: Computable<RepeatableDisplay>; onPurchase?: VoidFunction; } @@ -72,7 +72,7 @@ export type Repeatable<T extends RepeatableOptions> = Replace< style: GetComputableType<T["style"]>; mark: GetComputableType<T["mark"]>; small: GetComputableType<T["small"]>; - buyMax: GetComputableType<T["buyMax"]>; + maximize: GetComputableType<T["maximize"]>; display: Ref<CoercableComponent>; } >; @@ -149,7 +149,7 @@ export function createRepeatable<T extends RepeatableOptions>( } payRequirements( repeatable.requirements, - unref(genericRepeatable.buyMax) + unref(genericRepeatable.maximize) ? maxRequirementsMet(genericRepeatable.requirements) : 1 ); @@ -203,7 +203,7 @@ export function createRepeatable<T extends RepeatableOptions>( <br /> {displayRequirements( genericRepeatable.requirements, - unref(genericRepeatable.buyMax) + unref(genericRepeatable.maximize) ? maxRequirementsMet(genericRepeatable.requirements) : 1 )} @@ -222,7 +222,7 @@ export function createRepeatable<T extends RepeatableOptions>( processComputable(repeatable as T, "style"); processComputable(repeatable as T, "mark"); processComputable(repeatable as T, "small"); - processComputable(repeatable as T, "buyMax"); + processComputable(repeatable as T, "maximize"); repeatable[GatherProps] = function (this: GenericRepeatable) { const { display, visibility, style, classes, onClick, canClick, small, mark, id } = From dcb3bc949d94cc0caa190dbf39b9301f74915968 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Tue, 14 Feb 2023 22:29:31 -0600 Subject: [PATCH 36/56] Change buyMax to maximize on repeatables, and make requirements report ability to maximize Also fixes up some of the requirements tests that weren't actually asserting --- src/game/requirements.tsx | 36 ++++++++++++++--------------- tests/game/requirements.test.ts | 40 ++++++++++++++++----------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index ca68843..780afca 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -11,7 +11,12 @@ import { import { createLazyProxy } from "util/proxies"; import { joinJSX, renderJSX } from "util/vue"; import { computed, unref } from "vue"; -import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from "./formulas"; +import Formula, { + calculateCost, + calculateMaxAffordable, + GenericFormula, + InvertibleFormula +} from "./formulas"; /** * An object that can be used to describe a requirement to perform some purchase or other action. @@ -39,9 +44,9 @@ export interface Requirement { */ requiresPay: ProcessedComputable<boolean>; /** - * Whether or not this requirement can have multiple levels of requirements that can be completed at once. + * Whether or not this requirement can have multiple levels of requirements that can be met at once. Requirement is assumed to not have multiple levels if this property not present. */ - buyMax?: ProcessedComputable<boolean>; + canMaximize?: ProcessedComputable<boolean>; /** * Perform any effects to the game state that should happen when the requirement gets triggered. * @param amount The amount of levels of requirements to pay for. @@ -61,7 +66,7 @@ export interface CostRequirementOptions { */ resource: Resource; /** - * The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case {@link buyMax} will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support buyMax by passing a custom {@link pay} function. + * The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case maximizing will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support maximizing by passing a custom {@link pay} function. */ cost: Computable<DecimalSource> | GenericFormula; /** @@ -72,19 +77,14 @@ export interface CostRequirementOptions { * Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}. */ requiresPay?: Computable<boolean>; - /** - * Pass-through to {@link Requirement.buyMax}. - * @see {@link cost} for restrictions on buying max support. - */ - buyMax?: Computable<boolean>; /** * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions. * @see {Formula} */ spendResources?: Computable<boolean>; /** - * Pass-through to {@link Requirement.pay}. May be required for buying max support. - * @see {@link cost} for restrictions on buying max support. + * Pass-through to {@link Requirement.pay}. May be required for maximizing support. + * @see {@link cost} for restrictions on maximizing support. */ pay?: (amount?: DecimalSource) => void; } @@ -159,16 +159,12 @@ export function createCostRequirement<T extends CostRequirementOptions>( : unref(req.cost as ProcessedComputable<DecimalSource>); req.resource.value = Decimal.sub(req.resource.value, cost).max(0); }); - processComputable(req as T, "buyMax"); - if ( - "buyMax" in req && - req.buyMax !== false && - req.cost instanceof Formula && - req.cost.isInvertible() - ) { + req.canMaximize = req.cost instanceof Formula && req.cost.isInvertible(); + + if (req.canMaximize) { req.requirementMet = calculateMaxAffordable( - req.cost, + req.cost as InvertibleFormula, req.resource, unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true ); @@ -244,6 +240,8 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource { const reqsMet = unref(requirements.requirementMet); if (typeof reqsMet === "boolean") { return reqsMet ? Infinity : 0; + } else if (Decimal.gt(reqsMet, 1) && unref(requirements.canMaximize) !== true) { + return 1; } return reqsMet; } diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts index 73d539f..fa9ae5e 100644 --- a/tests/game/requirements.test.ts +++ b/tests/game/requirements.test.ts @@ -27,23 +27,23 @@ describe("Creating cost requirement", () => { }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("resource pass-through", () => (requirement as any).resource === resource); + test("resource pass-through", () => expect((requirement as any).resource).toBe(resource)); // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("cost pass-through", () => (requirement as any).cost === 10); + test("cost pass-through", () => expect((requirement as any).cost).toBe(10)); test("partialDisplay exists", () => - requirement.partialDisplay != null && typeof requirement.partialDisplay === "function"); - test("display exists", () => - requirement.display != null && typeof requirement.display === "function"); - test("pay exists", () => requirement.pay != null && typeof requirement.pay === "function"); - test("requirementMet exists", () => - requirement.requirementMet != null && isRef(requirement.requirementMet)); - test("is visible", () => requirement.visibility === Visibility.Visible); - test("requires pay", () => requirement.requiresPay === true); + expect(typeof requirement.partialDisplay).toBe("function")); + test("display exists", () => expect(typeof requirement.display).toBe("function")); + test("pay exists", () => expect(typeof requirement.pay).toBe("function")); + test("requirementMet exists", () => { + expect(requirement.requirementMet).not.toBeNull(); + expect(isRef(requirement.requirementMet)).toBe(true); + }); + test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible)); + test("requires pay", () => expect(requirement.requiresPay).toBe(true)); // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("spends resources", () => (requirement as any).spendResources !== false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("does not buy max", () => (requirement as any).buyMax !== true); + test("spends resources", () => expect((requirement as any).spendResources).toBe(true)); + test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false)); }); describe("Fully customized", () => { @@ -56,7 +56,7 @@ describe("Creating cost requirement", () => { cost: 10, visibility: Visibility.None, requiresPay: false, - buyMax: true, + maximize: true, spendResources: false, // eslint-disable-next-line @typescript-eslint/no-empty-function pay() {} @@ -67,12 +67,12 @@ describe("Creating cost requirement", () => { requirement.pay != null && typeof requirement.pay === "function" && requirement.pay.length === 1); - test("is not visible", () => requirement.visibility === Visibility.None); - test("does not require pay", () => requirement.requiresPay === false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("does not spend resources", () => (requirement as any).spendResources); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("buys max", () => (requirement as any).buyMax); + test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None)); + test("does not require pay", () => expect(requirement.requiresPay).toBe(false)); + test("does not spend resources", () => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((requirement as any).spendResources).toBe(false)); + test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true)); }); test("Requirement met when meeting the cost", () => { From 909c7a5f5e2b80eb3315efc1471f14320449ecf3 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 00:09:06 -0600 Subject: [PATCH 37/56] Make modifier section take options func and display bad numbers in red --- src/data/common.tsx | 26 ++++++--- src/game/modifiers.tsx | 116 ++++++++++++++++++++++++++++++++--------- 2 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index f1a5f6d..3f68007 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -256,9 +256,11 @@ export interface Section { * Takes an array of modifier "sections", and creates a JSXFunction that can render all those sections, and allow each section to be collapsed. * Also returns a list of persistent refs that are used to control which sections are currently collapsed. * @param sectionsFunc A function that returns the sections to display. + * @param smallerIsBetter Determines whether numbers larger or smaller than the base should be displayed as red. */ export function createCollapsibleModifierSections( - sectionsFunc: () => Section[] + sectionsFunc: () => Section[], + smallerIsBetter = false ): [JSXFunction, Persistent<Record<number, boolean>>] { const sections: Section[] = []; const processed: @@ -326,6 +328,9 @@ export function createCollapsibleModifierSections( const hasPreviousSection = !firstVisibleSection; firstVisibleSection = false; + const base = unref(processed.base[i]) ?? 1; + const total = s.modifier.apply(base); + return ( <> {hasPreviousSection ? <br /> : null} @@ -335,11 +340,20 @@ export function createCollapsibleModifierSections( {modifiers} <hr /> <div class="modifier-container"> - <span class="modifier-description"> - Total - </span> - <span class="modifier-amount"> - {format(s.modifier.apply(unref(processed.base[i]) ?? 1))} + <span class="modifier-description">Total</span> + <span + class="modifier-amount" + style={ + ( + smallerIsBetter === true + ? Decimal.gt(total, base ?? 1) + : Decimal.lt(total, base ?? 1) + ) + ? "color: var(--danger)" + : "" + } + > + {formatSmall(total)} {s.unit} </span> </div> diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index 830e937..eb4ec2e 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -52,6 +52,8 @@ export interface AdditiveModifierOptions { description?: Computable<CoercableComponent> | undefined; /** A computable that will be processed and passed directly into the returned modifier. */ enabled?: Computable<boolean> | undefined; + /** Determines if numbers larger or smaller than 0 should be displayed as red. */ + smallerIsBetter?: boolean; } /** @@ -62,7 +64,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions>( optionsFunc: () => T ): ModifierFromOptionalParams<T["description"], T["enabled"]> { return createLazyProxy(() => { - const { addend, description, enabled } = optionsFunc(); + const { addend, description, enabled, smallerIsBetter } = optionsFunc(); const processedAddend = convertComputable(addend); const processedDescription = convertComputable(description); @@ -82,7 +84,18 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions>( {renderJSX(unref(processedDescription)!)} </span> ) : null} - <span class="modifier-amount"> + <span + class="modifier-amount" + style={ + ( + smallerIsBetter === true + ? Decimal.gt(unref(processedAddend), 0) + : Decimal.lt(unref(processedAddend), 0) + ) + ? "color: var(--danger)" + : "" + } + > {Decimal.gte(unref(processedAddend), 0) ? "+" : ""} {formatSmall(unref(processedAddend))} </span> @@ -100,6 +113,8 @@ export interface MultiplicativeModifierOptions { description?: Computable<CoercableComponent> | undefined; /** A computable that will be processed and passed directly into the returned modifier. */ enabled?: Computable<boolean> | undefined; + /** Determines if numbers larger or smaller than 1 should be displayed as red. */ + smallerIsBetter?: boolean; } /** @@ -110,7 +125,7 @@ export function createMultiplicativeModifier<T extends MultiplicativeModifierOpt optionsFunc: () => T ): ModifierFromOptionalParams<T["description"], T["enabled"]> { return createLazyProxy(() => { - const { multiplier, description, enabled } = optionsFunc(); + const { multiplier, description, enabled, smallerIsBetter } = optionsFunc(); const processedMultiplier = convertComputable(multiplier); const processedDescription = convertComputable(description); @@ -130,7 +145,18 @@ export function createMultiplicativeModifier<T extends MultiplicativeModifierOpt {renderJSX(unref(processedDescription)!)} </span> ) : null} - <span class="modifier-amount"> + <span + class="modifier-amount" + style={ + ( + smallerIsBetter === true + ? Decimal.gt(unref(processedMultiplier), 1) + : Decimal.lt(unref(processedMultiplier), 1) + ) + ? "color: var(--danger)" + : "" + } + > ×{formatSmall(unref(processedMultiplier))} </span> </div> @@ -149,6 +175,8 @@ export interface ExponentialModifierOptions { enabled?: Computable<boolean> | undefined; /** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */ supportLowNumbers?: boolean; + /** Determines if numbers larger or smaller than 1 should be displayed as red. */ + smallerIsBetter?: boolean; } /** @@ -159,7 +187,8 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>( optionsFunc: () => T ): ModifierFromOptionalParams<T["description"], T["enabled"]> { return createLazyProxy(() => { - const { exponent, description, enabled, supportLowNumbers } = optionsFunc(); + const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } = + optionsFunc(); const processedExponent = convertComputable(exponent); const processedDescription = convertComputable(description); @@ -200,7 +229,18 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>( {supportLowNumbers ? " (+1 effective)" : null} </span> ) : null} - <span class="modifier-amount"> + <span + class="modifier-amount" + style={ + ( + smallerIsBetter === true + ? Decimal.gt(unref(processedExponent), 1) + : Decimal.lt(unref(processedExponent), 1) + ) + ? "color: var(--danger)" + : "" + } + > ^{formatSmall(unref(processedExponent))} </span> </div> @@ -252,35 +292,50 @@ export function createSequentialModifier< }) as unknown as S; } +/** An object that configures a modifier section via {@link createModifierSection}. */ +export interface ModifierSectionOptions { + /** The header for the section. */ + title: string; + /** Smaller text that appears in the header after the title. */ + subtitle?: string; + /** The modifier to render. */ + modifier: WithRequired<Modifier, "description">; + /** The base value that'll be passed into the modifier. Defaults to 1. */ + base?: DecimalSource; + /** The unit of the value being modified, if any. */ + unit?: string; + /** The label to use for the base value. Defaults to "Base". */ + baseText?: CoercableComponent; + /** Determines if numbers larger or smaller than the base should be displayed as red. */ + smallerIsBetter?: boolean; +} + /** * 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. + * @param options Modifier section options. */ -export function createModifierSection( - title: string, - subtitle: string, - modifier: WithRequired<Modifier, "description">, - base: DecimalSource = 1, - unit = "", - baseText: CoercableComponent = "Base" -) { +export function createModifierSection({ + title, + subtitle, + modifier, + base, + unit, + baseText, + smallerIsBetter +}: ModifierSectionOptions) { + const total = modifier.apply(base ?? 1); return ( <div> <h3> {title} - {subtitle ? <span class="subtitle"> ({subtitle})</span> : null} + {subtitle == null ? null : <span class="subtitle"> ({subtitle})</span>} </h3> <br /> <div class="modifier-container"> - <span class="modifier-description">{renderJSX(baseText)}</span> + <span class="modifier-description">{renderJSX(baseText ?? "Base")}</span> <span class="modifier-amount"> - {formatSmall(base)} + {formatSmall(base ?? 1)} {unit} </span> </div> @@ -288,8 +343,19 @@ export function createModifierSection( <hr /> <div class="modifier-container"> <span class="modifier-description">Total</span> - <span class="modifier-amount"> - {formatSmall(modifier.apply(base))} + <span + class="modifier-amount" + style={ + ( + smallerIsBetter === true + ? Decimal.gt(total, base ?? 1) + : Decimal.lt(total, base ?? 1) + ) + ? "color: var(--danger)" + : "" + } + > + {formatSmall(total)} {unit} </span> </div> From cdce13480e47f00b1e1e0feebc38215f36f3b769 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 00:22:24 -0600 Subject: [PATCH 38/56] Remove references to purchasing for repeatable --- src/features/repeatable.tsx | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 55d29bf..5842429 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -47,7 +47,6 @@ export interface RepeatableOptions { small?: Computable<boolean>; maximize?: Computable<boolean>; display?: Computable<RepeatableDisplay>; - onPurchase?: VoidFunction; } export interface BaseRepeatable { @@ -56,7 +55,6 @@ export interface BaseRepeatable { maxed: Ref<boolean>; canClick: ProcessedComputable<boolean>; onClick: VoidFunction; - purchase: VoidFunction; type: typeof RepeatableType; [Component]: typeof ClickableComponent; [GatherProps]: () => Record<string, unknown>; @@ -139,23 +137,21 @@ export function createRepeatable<T extends RepeatableOptions>( return currClasses; }); repeatable.canClick = computed(() => requirementsMet(repeatable.requirements)); - repeatable.onClick = repeatable.purchase = - repeatable.onClick ?? - repeatable.purchase ?? - function (this: GenericRepeatable) { - const genericRepeatable = repeatable as GenericRepeatable; - if (!unref(genericRepeatable.canClick)) { - return; - } - payRequirements( - repeatable.requirements, - unref(genericRepeatable.maximize) - ? maxRequirementsMet(genericRepeatable.requirements) - : 1 - ); - genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1); - genericRepeatable.onPurchase?.(); - }; + const onClick = repeatable.onClick; + repeatable.onClick = function (this: GenericRepeatable) { + const genericRepeatable = repeatable as GenericRepeatable; + if (!unref(genericRepeatable.canClick)) { + return; + } + payRequirements( + repeatable.requirements, + unref(genericRepeatable.maximize) + ? maxRequirementsMet(genericRepeatable.requirements) + : 1 + ); + genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1); + onClick?.(); + }; processComputable(repeatable as T, "display"); const display = repeatable.display; repeatable.display = jsx(() => { From 3dd2d965674b80caacb22ff63a9d7cccdc1607cf Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 10:17:07 -0600 Subject: [PATCH 39/56] Documented repeatables --- src/features/repeatable.tsx | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 5842429..f40f61c 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -25,41 +25,70 @@ import { coerceComponent, isCoercableComponent } from "util/vue"; import type { Ref } from "vue"; import { computed, unref } from "vue"; +/** A symbol used to identify {@link Repeatable} features. */ export const RepeatableType = Symbol("Repeatable"); +/** A type that can be used to customize the {@link Repeatable} display. */ export type RepeatableDisplay = | CoercableComponent | { + /** A header to appear at the top of the display. */ title?: CoercableComponent; + /** The main text that appears in the display. */ description?: CoercableComponent; + /** A description of the current effect of this repeatable, bsed off its amount. */ effectDisplay?: CoercableComponent; + /** Whether or not to show the current amount of this repeatable at the bottom of the display. */ showAmount?: boolean; }; +/** An object that configures a {@link Repeatable}. */ export interface RepeatableOptions { + /** Whether this repeatable should be visible. */ visibility?: Computable<Visibility>; + /** The requirement(s) to increase this repeatable. */ requirements: Requirements; + /** The maximum amount obtainable for this repeatable. */ limit?: Computable<DecimalSource>; + /** The initial amount this repeatable has on a new save / after reset. */ initialAmount?: DecimalSource; + /** Dictionary of CSS classes to apply to this feature. */ classes?: Computable<Record<string, boolean>>; + /** CSS to apply to this feature. */ style?: Computable<StyleValue>; + /** Shows a marker on the corner of the feature. */ mark?: Computable<boolean | string>; + /** Toggles a smaller design for the feature. */ small?: Computable<boolean>; + /** Whether or not clicking this repeatable should attempt to maximize amount based on the requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */ maximize?: Computable<boolean>; + /** The display to use for this repeatable. */ display?: Computable<RepeatableDisplay>; } +/** + * The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}. + */ export interface BaseRepeatable { + /** An auto-generated ID for identifying features that appear in the DOM. Will not persistent between refreshes or updates. */ id: string; + /** The current amount this repeatable has. */ amount: Persistent<DecimalSource>; + /** Whether or not this repeatable's amount is at it's limit. */ maxed: Ref<boolean>; + /** Whether or not this repeatable can be clicked. */ canClick: ProcessedComputable<boolean>; + /** A function that gets called when this repeatable is clicked. */ onClick: VoidFunction; + /** A symbol that helps identify features of the same type. */ type: typeof RepeatableType; + /** The Vue component used to render this feature. */ [Component]: typeof ClickableComponent; + /** A function to gather the props the vue component requires for this feature. */ [GatherProps]: () => Record<string, unknown>; } +/** An object that represents a feature with multiple "levels" with scaling requirements. */ export type Repeatable<T extends RepeatableOptions> = Replace< T & BaseRepeatable, { @@ -75,6 +104,7 @@ export type Repeatable<T extends RepeatableOptions> = Replace< } >; +/** A type that matches any valid {@link Repeatable} object. */ export type GenericRepeatable = Replace< Repeatable<RepeatableOptions>, { @@ -83,6 +113,10 @@ export type GenericRepeatable = Replace< } >; +/** + * Lazily creates a repeatable with the given options. + * @param optionsFunc Repeatable options. + */ export function createRepeatable<T extends RepeatableOptions>( optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable> ): Repeatable<T> { From fe25ea71b65525843013c5f35aa27cea8cb1abc2 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 13:05:43 -0600 Subject: [PATCH 40/56] Add utilities for making common cost requirement pay functions --- src/game/requirements.tsx | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 780afca..189e452 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -17,6 +17,7 @@ import Formula, { GenericFormula, InvertibleFormula } from "./formulas"; +import { DefaultValue, Persistent } from "./persistence"; /** * An object that can be used to describe a requirement to perform some purchase or other action. @@ -89,13 +90,15 @@ export interface CostRequirementOptions { pay?: (amount?: DecimalSource) => void; } +export type CostRequirement = Requirement & CostRequirementOptions; + /** * Lazily creates a requirement with the given options, that is based on meeting an amount of a resource. * @param optionsFunc Cost requirement options. */ export function createCostRequirement<T extends CostRequirementOptions>( optionsFunc: () => T -): Requirement { +): CostRequirement { return createLazyProxy(() => { const req = optionsFunc() as T & Partial<Requirement>; @@ -181,7 +184,7 @@ export function createCostRequirement<T extends CostRequirementOptions>( }); } - return req as Requirement; + return req as CostRequirement; }); } @@ -300,3 +303,24 @@ export function payRequirements(requirements: Requirements, amount: DecimalSourc requirements.pay?.(amount); } } + +export function payByDivision(this: CostRequirement, amount?: DecimalSource) { + const cost = + this.cost instanceof Formula + ? calculateCost( + this.cost, + amount ?? 1, + unref(this.spendResources as ProcessedComputable<boolean> | undefined) ?? true + ) + : unref(this.cost as ProcessedComputable<DecimalSource>); + this.resource.value = Decimal.div(this.resource.value, cost); +} + +export function payByReset(overrideDefaultValue?: DecimalSource) { + return function (this: CostRequirement) { + this.resource.value = + overrideDefaultValue ?? + (this.resource as Resource & Persistent<DecimalSource>)[DefaultValue] ?? + 0; + }; +} From 1bfa66e1c99121f78cc2f8e758f71aa2bded4ed6 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 14:57:22 -0600 Subject: [PATCH 41/56] Add mouse/touch events to more onClicks --- src/data/common.tsx | 6 +++--- src/features/grids/grid.ts | 4 ++-- src/features/repeatable.tsx | 6 +++--- src/features/trees/tree.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index 3f68007..638bc7d 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -73,7 +73,7 @@ export type ResetButton<T extends ResetButtonOptions> = Replace< display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>; canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>; minimumGain: GetComputableTypeWithDefault<T["minimumGain"], 1>; - onClick: VoidFunction; + onClick: (event?: MouseEvent | TouchEvent) => void; } >; @@ -153,7 +153,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption } const onClick = resetButton.onClick; - resetButton.onClick = function () { + resetButton.onClick = function (event?: MouseEvent | TouchEvent) { if (unref(resetButton.canClick) === false) { return; } @@ -162,7 +162,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption if (resetButton.resetTime) { resetButton.resetTime.value = resetButton.resetTime[DefaultValue]; } - onClick?.(); + onClick?.(event); }; return resetButton; diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 05890a7..2804e80 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -277,9 +277,9 @@ export function createGrid<T extends GridOptions>( if (grid.onClick) { const onClick = grid.onClick.bind(grid); - grid.onClick = function (id, state) { + grid.onClick = function (id, state, e) { if (unref((grid as GenericGrid).cells[id].canClick)) { - onClick(id, state); + onClick(id, state, e); } }; } diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index f40f61c..8bceaf6 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -79,7 +79,7 @@ export interface BaseRepeatable { /** Whether or not this repeatable can be clicked. */ canClick: ProcessedComputable<boolean>; /** A function that gets called when this repeatable is clicked. */ - onClick: VoidFunction; + onClick: (event?: MouseEvent | TouchEvent) => void; /** A symbol that helps identify features of the same type. */ type: typeof RepeatableType; /** The Vue component used to render this feature. */ @@ -172,7 +172,7 @@ export function createRepeatable<T extends RepeatableOptions>( }); repeatable.canClick = computed(() => requirementsMet(repeatable.requirements)); const onClick = repeatable.onClick; - repeatable.onClick = function (this: GenericRepeatable) { + repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) { const genericRepeatable = repeatable as GenericRepeatable; if (!unref(genericRepeatable.canClick)) { return; @@ -184,7 +184,7 @@ export function createRepeatable<T extends RepeatableOptions>( : 1 ); genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1); - onClick?.(); + onClick?.(event); }; processComputable(repeatable as T, "display"); const display = repeatable.display; diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index 0b36f74..99c759a 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -87,9 +87,9 @@ export function createTreeNode<T extends TreeNodeOptions>( if (treeNode.onClick) { const onClick = treeNode.onClick.bind(treeNode); - treeNode.onClick = function () { + treeNode.onClick = function (e) { if (unref(treeNode.canClick) !== false) { - onClick(); + onClick(e); } }; } From 7f4d57d3e4682f91abc61b06e9c26406f9fa263f Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 15:06:06 -0600 Subject: [PATCH 42/56] Added incredibly dangerous deleteLowerSaves function to window --- src/util/save.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/util/save.ts b/src/util/save.ts index 2955fd1..4137e5b 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -140,14 +140,22 @@ window.onbeforeunload = () => { declare global { /** - * Augment the window object so the save function, and the hard reset function can be accessed from the console. + * Augment the window object so the save, hard reset, and deleteLowerSaves functions can be accessed from the console. */ interface Window { save: VoidFunction; hardReset: VoidFunction; + deleteLowerSaves: VoidFunction; } } window.save = save; export const hardReset = (window.hardReset = async () => { await loadSave(newSave()); }); +export const deleteLowerSaves = (window.deleteLowerSaves = () => { + const index = Object.values(settings.saves).indexOf(player.id) + 1; + Object.values(settings.saves) + .slice(index) + .forEach(id => localStorage.removeItem(id)); + settings.saves = settings.saves.slice(0, index); +}); From 8bf68831b7ea7eff97f431dfbc01bb8ea6e81429 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 15:24:36 -0600 Subject: [PATCH 43/56] Add utility function for ETAs --- src/data/common.tsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index 638bc7d..801ae63 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -13,7 +13,7 @@ import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; import player from "game/player"; import type { DecimalSource } from "util/bignum"; -import Decimal, { format } from "util/bignum"; +import Decimal, { format, formatSmall, formatTime } from "util/bignum"; import type { WithRequired } from "util/common"; import type { Computable, @@ -416,3 +416,28 @@ export function createCollapsibleMilestones(milestones: Record<string, GenericMi display }; } + +/** + * Utility function for getting an ETA for when a target will be reached by a resource with a known (and assumed consistent) gain. + * @param resource The resource that will be increasing over time. + * @param rate The rate at which the resource is increasing. + * @param target The target amount of the resource to estimate the duration until. + */ +export function estimateTime( + resource: Resource, + rate: Computable<DecimalSource>, + target: Computable<DecimalSource> +) { + const processedRate = convertComputable(rate); + const processedTarget = convertComputable(target); + return computed(() => { + const currRate = unref(processedRate); + const currTarget = unref(processedTarget); + if (Decimal.gte(resource.value, currTarget)) { + return "Now"; + } else if (Decimal.lt(currRate, 0)) { + return "Never"; + } + return formatTime(Decimal.sub(currTarget, resource.value).div(currRate)); + }); +} From 44be53d4754e7eb8a13d38f3c9cba7e6a9ed7cd7 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 18:21:38 -0600 Subject: [PATCH 44/56] Add utility function for showing previews of how formulas will change --- src/data/common.tsx | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index 801ae63..0af59cc 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -23,8 +23,9 @@ import type { } from "util/computed"; import { convertComputable, processComputable } from "util/computed"; import { getFirstFeature, renderColJSX, renderJSX } from "util/vue"; -import type { Ref } from "vue"; +import type { ComputedRef, Ref } from "vue"; import { computed, unref } from "vue"; +import Formula, { GenericFormula } from "game/formulas"; import "./common.css"; /** An object that configures a {@link ResetButton} */ @@ -441,3 +442,45 @@ export function estimateTime( return formatTime(Decimal.sub(currTarget, resource.value).div(currRate)); }); } + +/** + * Utility function for displaying the result of a formula such that it will, when told to, preview how the formula's result will change. + * Requires a formula with a single variable inside. + * @param formula The formula to display the result of. + * @param showPreview Whether or not to preview how the formula's result will change. + * @param previewAmount The amount to _add_ to the current formula's variable amount to preview the change in result. + */ +export function createFormulaPreview( + formula: GenericFormula, + showPreview: Computable<boolean>, + previewAmount: Computable<DecimalSource> = 1 +): ComputedRef<CoercableComponent> { + const processedShowPreview = convertComputable(showPreview); + const processedPreviewAmount = convertComputable(previewAmount); + if (!formula.hasVariable()) { + throw "Cannot create formula preview if the formula does not have a variable"; + } + return computed(() => { + if (unref(processedShowPreview)) { + const curr = formatSmall(formula.evaluate()); + const preview = formatSmall( + formula.evaluate( + Decimal.add( + unref(formula.innermostVariable ?? 0), + unref(processedPreviewAmount) + ) + ) + ); + return jsx(() => ( + <> + <b> + <i> + {curr}→{preview} + </i> + </b> + </> + )); + } + return formatSmall(formula.evaluate()); + }); +} From 4a28c2f8f944644be2a9500374cd4798d224f014 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 20:00:36 -0600 Subject: [PATCH 45/56] Remove showIf and make visibility properties take booleans --- src/features/achievements/Achievement.vue | 12 ++++++----- src/features/achievements/achievement.tsx | 7 +++--- src/features/action.tsx | 2 +- src/features/bars/Bar.vue | 12 ++++++----- src/features/bars/bar.ts | 4 ++-- src/features/boards/Board.vue | 8 +++---- src/features/boards/board.ts | 8 +++---- src/features/challenges/Challenge.vue | 10 +++++---- src/features/challenges/challenge.tsx | 18 +++++++++++----- src/features/clickables/Clickable.vue | 10 +++++---- src/features/clickables/clickable.ts | 4 ++-- src/features/feature.ts | 19 +++++++++++------ src/features/grids/Grid.vue | 10 ++++----- src/features/grids/GridCell.vue | 12 ++++++----- src/features/grids/grid.ts | 10 ++++----- src/features/infoboxes/Infobox.vue | 12 ++++++----- src/features/infoboxes/infobox.ts | 4 ++-- src/features/milestones/Milestone.vue | 12 ++++++----- src/features/milestones/milestone.tsx | 18 +++++++++++----- src/features/repeatable.tsx | 4 ++-- src/features/tabs/TabButton.vue | 14 ++++++------ src/features/tabs/TabFamily.vue | 12 ++++++----- src/features/tabs/tabFamily.ts | 26 ++++++++++++----------- src/features/trees/TreeNode.vue | 12 ++++++----- src/features/trees/tree.ts | 8 +++---- src/features/upgrades/Upgrade.vue | 12 ++++++----- src/features/upgrades/upgrade.ts | 4 ++-- src/game/requirements.tsx | 12 +++++------ src/util/vue.tsx | 15 ++++++++----- 29 files changed, 181 insertions(+), 130 deletions(-) diff --git a/src/features/achievements/Achievement.vue b/src/features/achievements/Achievement.vue index 5b3c7a0..78ac17b 100644 --- a/src/features/achievements/Achievement.vue +++ b/src/features/achievements/Achievement.vue @@ -1,9 +1,9 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined, + visibility: isHidden(visibility) ? 'hidden' : undefined, backgroundImage: (earned && image && `url(${image})`) || '' }, unref(style) ?? [] @@ -27,7 +27,7 @@ import "components/common/features.css"; import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { CoercableComponent } from "features/feature"; -import { Visibility } from "features/feature"; +import { Visibility, isHidden, isVisible } from "features/feature"; import { computeOptionalComponent, processedPropType } from "util/vue"; import type { StyleValue } from "vue"; import { defineComponent, toRefs, unref } from "vue"; @@ -35,7 +35,7 @@ import { defineComponent, toRefs, unref } from "vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, display: processedPropType<CoercableComponent>(Object, String, Function), @@ -62,7 +62,9 @@ export default defineComponent({ return { component: computeOptionalComponent(display), unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 92e9e88..500e93f 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -4,6 +4,7 @@ import { Component, GatherProps, getUniqueID, + isVisible, OptionsFunc, Replace, setDefault, @@ -32,7 +33,7 @@ const toast = useToast(); export const AchievementType = Symbol("Achievement"); export interface AchievementOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; shouldEarn?: () => boolean; display?: Computable<CoercableComponent>; mark?: Computable<boolean | string>; @@ -66,7 +67,7 @@ export type Achievement<T extends AchievementOptions> = Replace< export type GenericAchievement = Replace< Achievement<AchievementOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; @@ -104,7 +105,7 @@ export function createAchievement<T extends AchievementOptions>( if (settings.active !== player.id) return; if ( !genericAchievement.earned.value && - unref(genericAchievement.visibility) === Visibility.Visible && + isVisible(genericAchievement.visibility) && genericAchievement.shouldEarn?.() ) { genericAchievement.earned.value = true; diff --git a/src/features/action.tsx b/src/features/action.tsx index 18643b8..5d7e555 100644 --- a/src/features/action.tsx +++ b/src/features/action.tsx @@ -71,7 +71,7 @@ export type GenericAction = Replace< Action<ActionOptions>, { autoStart: ProcessedComputable<boolean>; - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; canClick: ProcessedComputable<boolean>; } >; diff --git a/src/features/bars/Bar.vue b/src/features/bars/Bar.vue index df140c0..3c4b91c 100644 --- a/src/features/bars/Bar.vue +++ b/src/features/bars/Bar.vue @@ -1,11 +1,11 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { width: unref(width) + 'px', height: unref(height) + 'px', - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? {} ]" @@ -44,7 +44,7 @@ <script lang="ts"> import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; -import { CoercableComponent, Visibility } from "features/feature"; +import { CoercableComponent, isHidden, isVisible, Visibility } from "features/feature"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; import { Direction } from "util/common"; @@ -72,7 +72,7 @@ export default defineComponent({ }, display: processedPropType<CoercableComponent>(Object, String, Function), visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, style: processedPropType<StyleValue>(Object, String, Array), @@ -136,7 +136,9 @@ export default defineComponent({ barStyle, component, unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts index 60c86e4..f0436f4 100644 --- a/src/features/bars/bar.ts +++ b/src/features/bars/bar.ts @@ -22,7 +22,7 @@ import { unref } from "vue"; export const BarType = Symbol("Bar"); export interface BarOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; width: Computable<number>; height: Computable<number>; direction: Computable<Direction>; @@ -66,7 +66,7 @@ export type Bar<T extends BarOptions> = Replace< export type GenericBar = Replace< Bar<BarOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; diff --git a/src/features/boards/Board.vue b/src/features/boards/Board.vue index 4fd026b..f1f8b83 100644 --- a/src/features/boards/Board.vue +++ b/src/features/boards/Board.vue @@ -1,7 +1,7 @@ <template> <panZoom - v-if="unref(visibility) !== Visibility.None" - v-show="unref(visibility) === Visibility.Visible" + v-if="isVisible(visibility)" + v-show="isHidden(visibility)" :style="[ { width, @@ -60,7 +60,7 @@ import type { } from "features/boards/board"; import { getNodeProperty } from "features/boards/board"; import type { StyleValue } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import type { ProcessedComputable } from "util/computed"; import { computed, ref, Ref, toRefs, unref } from "vue"; import BoardLinkVue from "./BoardLink.vue"; @@ -70,7 +70,7 @@ const _props = defineProps<{ nodes: Ref<BoardNode[]>; types: Record<string, GenericNodeType>; state: Ref<BoardData>; - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; width?: ProcessedComputable<string>; height?: ProcessedComputable<string>; style?: ProcessedComputable<StyleValue>; diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index 689659d..7f79110 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -129,7 +129,7 @@ export type GenericNodeType = Replace< export interface BoardNodeActionOptions { id: string; - visibility?: NodeComputable<Visibility>; + visibility?: NodeComputable<Visibility | boolean>; icon: NodeComputable<string>; fillColor?: NodeComputable<string>; tooltip: NodeComputable<string>; @@ -155,12 +155,12 @@ export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace< export type GenericBoardNodeAction = Replace< BoardNodeAction<BoardNodeActionOptions>, { - visibility: NodeComputable<Visibility>; + visibility: NodeComputable<Visibility | boolean>; } >; export interface BoardOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; height?: Computable<string>; width?: Computable<string>; classes?: Computable<Record<string, boolean>>; @@ -199,7 +199,7 @@ export type Board<T extends BoardOptions> = Replace< export type GenericBoard = Replace< Board<BoardOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; state: ProcessedComputable<BoardData>; links: ProcessedComputable<BoardNodeLink[] | null>; } diff --git a/src/features/challenges/Challenge.vue b/src/features/challenges/Challenge.vue index f68d9cf..f64f203 100644 --- a/src/features/challenges/Challenge.vue +++ b/src/features/challenges/Challenge.vue @@ -1,9 +1,9 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, notifyStyle, unref(style) ?? {} @@ -36,7 +36,7 @@ import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { GenericChallenge } from "features/challenges/challenge"; import type { StyleValue } from "features/feature"; -import { jsx, Visibility } from "features/feature"; +import { isHidden, isVisible, jsx, Visibility } from "features/feature"; import { getHighNotifyStyle, getNotifyStyle } from "game/notifications"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue"; import type { Component, PropType, UnwrapRef } from "vue"; @@ -62,7 +62,7 @@ export default defineComponent({ Function ), visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, style: processedPropType<StyleValue>(String, Object, Array), @@ -167,6 +167,8 @@ export default defineComponent({ notifyStyle, comp, Visibility, + isVisible, + isHidden, unref }; } diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index f7322ab..3063b33 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -2,7 +2,15 @@ import { isArray } from "@vue/shared"; import Toggle from "components/fields/Toggle.vue"; import ChallengeComponent from "features/challenges/Challenge.vue"; import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; -import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; +import { + Component, + GatherProps, + getUniqueID, + isVisible, + jsx, + setDefault, + Visibility +} from "features/feature"; import type { GenericReset } from "features/reset"; import type { Resource } from "features/resources/resource"; import { globalBus } from "game/events"; @@ -25,7 +33,7 @@ import { computed, unref, watch } from "vue"; export const ChallengeType = Symbol("ChallengeType"); export interface ChallengeOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; canStart?: Computable<boolean>; reset?: GenericReset; canComplete?: Computable<boolean | DecimalSource>; @@ -81,7 +89,7 @@ export type Challenge<T extends ChallengeOptions> = Replace< export type GenericChallenge = Replace< Challenge<ChallengeOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; canStart: ProcessedComputable<boolean>; canComplete: ProcessedComputable<boolean | DecimalSource>; completionLimit: ProcessedComputable<DecimalSource>; @@ -145,7 +153,7 @@ export function createChallenge<T extends ChallengeOptions>( genericChallenge.reset?.reset(); } else if ( unref(genericChallenge.canStart) && - unref(genericChallenge.visibility) === Visibility.Visible && + isVisible(genericChallenge.visibility) && !genericChallenge.maxed.value ) { genericChallenge.reset?.reset(); @@ -179,7 +187,7 @@ export function createChallenge<T extends ChallengeOptions>( }; processComputable(challenge as T, "visibility"); setDefault(challenge, "visibility", Visibility.Visible); - const visibility = challenge.visibility as ProcessedComputable<Visibility>; + const visibility = challenge.visibility as ProcessedComputable<Visibility | boolean>; challenge.visibility = computed(() => { if (settings.hideChallenges === true && unref(challenge.maxed)) { return Visibility.None; diff --git a/src/features/clickables/Clickable.vue b/src/features/clickables/Clickable.vue index a2ec971..ac001a2 100644 --- a/src/features/clickables/Clickable.vue +++ b/src/features/clickables/Clickable.vue @@ -1,8 +1,8 @@ <template> <button - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ - { visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined }, + { visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? [] ]" @click="onClick" @@ -33,7 +33,7 @@ import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { GenericClickable } from "features/clickables/clickable"; import type { StyleValue } from "features/feature"; -import { jsx, Visibility } from "features/feature"; +import { isHidden, isVisible, jsx, Visibility } from "features/feature"; import { coerceComponent, isCoercableComponent, @@ -55,7 +55,7 @@ export default defineComponent({ required: true }, visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, style: processedPropType<StyleValue>(Object, String, Array), @@ -115,6 +115,8 @@ export default defineComponent({ stop, comp, Visibility, + isVisible, + isHidden, unref }; } diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts index 82d4bd9..c7c83e3 100644 --- a/src/features/clickables/clickable.ts +++ b/src/features/clickables/clickable.ts @@ -22,7 +22,7 @@ import { computed, unref } from "vue"; export const ClickableType = Symbol("Clickable"); export interface ClickableOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; canClick?: Computable<boolean>; classes?: Computable<Record<string, boolean>>; style?: Computable<StyleValue>; @@ -61,7 +61,7 @@ export type Clickable<T extends ClickableOptions> = Replace< export type GenericClickable = Replace< Clickable<ClickableOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; canClick: ProcessedComputable<boolean>; } >; diff --git a/src/features/feature.ts b/src/features/feature.ts index afa251f..d234997 100644 --- a/src/features/feature.ts +++ b/src/features/feature.ts @@ -1,7 +1,7 @@ import Decimal from "util/bignum"; -import { DoNotCache } from "util/computed"; +import { DoNotCache, ProcessedComputable } from "util/computed"; import type { CSSProperties, DefineComponent } from "vue"; -import { isRef } from "vue"; +import { isRef, unref } from "vue"; /** * A symbol to use as a key for a vue component a feature can be rendered with @@ -67,6 +67,16 @@ export enum Visibility { None } +export function isVisible(visibility: ProcessedComputable<Visibility | boolean>) { + const currVisibility = unref(visibility); + return currVisibility !== Visibility.None && currVisibility !== false; +} + +export function isHidden(visibility: ProcessedComputable<Visibility | boolean>) { + const currVisibility = unref(visibility); + return currVisibility === Visibility.Hidden; +} + /** * Takes a function and marks it as JSX so it won't get auto-wrapped into a ComputedRef. * The function may also return empty string as empty JSX tags cause issues. @@ -76,11 +86,6 @@ export function jsx(func: () => JSX.Element | ""): JSXFunction { return func as JSXFunction; } -/** Utility function to convert a boolean value into a Visbility value */ -export function showIf(condition: boolean, otherwise = Visibility.None): Visibility { - return condition ? Visibility.Visible : otherwise; -} - /** Utility function to set a property on an object if and only if it doesn't already exist */ export function setDefault<T, K extends keyof T>( object: T, diff --git a/src/features/grids/Grid.vue b/src/features/grids/Grid.vue index b6315b0..2950604 100644 --- a/src/features/grids/Grid.vue +++ b/src/features/grids/Grid.vue @@ -1,8 +1,8 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="{ - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }" class="table-grid" > @@ -19,7 +19,7 @@ <script lang="ts"> import "components/common/table.css"; import themes from "data/themes"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import type { GridCell } from "features/grids/grid"; import settings from "game/settings"; import { processedPropType } from "util/vue"; @@ -29,7 +29,7 @@ import GridCellVue from "./GridCell.vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, rows: { @@ -54,7 +54,7 @@ export default defineComponent({ return { visibility, onClick, onHold, display, title, style, canClick, id }; } - return { unref, gatherCellProps, Visibility, mergeAdjacent }; + return { unref, gatherCellProps, Visibility, mergeAdjacent, isVisible, isHidden }; } }); </script> diff --git a/src/features/grids/GridCell.vue b/src/features/grids/GridCell.vue index c9bdbe6..876b7c3 100644 --- a/src/features/grids/GridCell.vue +++ b/src/features/grids/GridCell.vue @@ -1,10 +1,10 @@ <template> <button - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :class="{ feature: true, tile: true, can: unref(canClick), locked: !unref(canClick) }" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? {} ]" @@ -26,7 +26,7 @@ import "components/common/features.css"; import Node from "components/Node.vue"; import type { CoercableComponent, StyleValue } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import { computeComponent, computeOptionalComponent, @@ -39,7 +39,7 @@ import { defineComponent, toRefs, unref } from "vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, onClick: Function as PropType<(e?: MouseEvent | TouchEvent) => void>, @@ -76,7 +76,9 @@ export default defineComponent({ titleComponent, component, Visibility, - unref + unref, + isVisible, + isHidden }; } }); diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 2804e80..eda80da 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -171,7 +171,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> { export interface GridCell { id: string; - visibility: Visibility; + visibility: Visibility | boolean; canClick: boolean; startState: State; state: State; @@ -184,10 +184,10 @@ export interface GridCell { } export interface GridOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; rows: Computable<number>; cols: Computable<number>; - getVisibility?: CellComputable<Visibility>; + getVisibility?: CellComputable<Visibility | boolean>; getCanClick?: CellComputable<boolean>; getStartState: Computable<State> | ((id: string | number) => State); getStyle?: CellComputable<StyleValue>; @@ -229,8 +229,8 @@ export type Grid<T extends GridOptions> = Replace< export type GenericGrid = Replace< Grid<GridOptions>, { - visibility: ProcessedComputable<Visibility>; - getVisibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; + getVisibility: ProcessedComputable<Visibility | boolean>; getCanClick: ProcessedComputable<boolean>; } >; diff --git a/src/features/infoboxes/Infobox.vue b/src/features/infoboxes/Infobox.vue index 2271c1b..302d985 100644 --- a/src/features/infoboxes/Infobox.vue +++ b/src/features/infoboxes/Infobox.vue @@ -1,11 +1,11 @@ <template> <div class="infobox" - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { borderColor: unref(color), - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? {} ]" @@ -33,7 +33,7 @@ import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTrans import Node from "components/Node.vue"; import themes from "data/themes"; import type { CoercableComponent } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import settings from "game/settings"; import { computeComponent, processedPropType } from "util/vue"; import type { PropType, Ref, StyleValue } from "vue"; @@ -42,7 +42,7 @@ import { computed, defineComponent, toRefs, unref } from "vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, display: { @@ -83,7 +83,9 @@ export default defineComponent({ bodyComponent, stacked, unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts index f30e31c..43e3ffa 100644 --- a/src/features/infoboxes/infobox.ts +++ b/src/features/infoboxes/infobox.ts @@ -16,7 +16,7 @@ import { unref } from "vue"; export const InfoboxType = Symbol("Infobox"); export interface InfoboxOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; color?: Computable<string>; style?: Computable<StyleValue>; titleStyle?: Computable<StyleValue>; @@ -51,7 +51,7 @@ export type Infobox<T extends InfoboxOptions> = Replace< export type GenericInfobox = Replace< Infobox<InfoboxOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; diff --git a/src/features/milestones/Milestone.vue b/src/features/milestones/Milestone.vue index 8cc4331..e5dac97 100644 --- a/src/features/milestones/Milestone.vue +++ b/src/features/milestones/Milestone.vue @@ -1,9 +1,9 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? {} ]" @@ -18,7 +18,7 @@ import "components/common/features.css"; import Node from "components/Node.vue"; import type { StyleValue } from "features/feature"; -import { jsx, Visibility } from "features/feature"; +import { isHidden, isVisible, jsx, Visibility } from "features/feature"; import type { GenericMilestone } from "features/milestones/milestone"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue"; import type { Component, UnwrapRef } from "vue"; @@ -27,7 +27,7 @@ import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, display: { @@ -92,7 +92,9 @@ export default defineComponent({ return { comp, unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index 388bbd1..7dfe7f1 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -6,7 +6,15 @@ import type { Replace, StyleValue } from "features/feature"; -import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; +import { + Component, + GatherProps, + getUniqueID, + isVisible, + jsx, + setDefault, + Visibility +} from "features/feature"; import MilestoneComponent from "features/milestones/Milestone.vue"; import { globalBus } from "game/events"; import "game/notifications"; @@ -40,7 +48,7 @@ export enum MilestoneDisplay { } export interface MilestoneOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; shouldEarn?: () => boolean; style?: Computable<StyleValue>; classes?: Computable<Record<string, boolean>>; @@ -79,7 +87,7 @@ export type Milestone<T extends MilestoneOptions> = Replace< export type GenericMilestone = Replace< Milestone<MilestoneOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; @@ -118,7 +126,7 @@ export function createMilestone<T extends MilestoneOptions>( processComputable(milestone as T, "visibility"); setDefault(milestone, "visibility", Visibility.Visible); - const visibility = milestone.visibility as ProcessedComputable<Visibility>; + const visibility = milestone.visibility as ProcessedComputable<Visibility | boolean>; milestone.visibility = computed(() => { const display = unref((milestone as GenericMilestone).display); switch (settings.msDisplay) { @@ -163,7 +171,7 @@ export function createMilestone<T extends MilestoneOptions>( if (settings.active !== player.id) return; if ( !genericMilestone.earned.value && - unref(genericMilestone.visibility) === Visibility.Visible && + isVisible(genericMilestone.visibility) && genericMilestone.shouldEarn?.() ) { genericMilestone.earned.value = true; diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 8bceaf6..96bea11 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -45,7 +45,7 @@ export type RepeatableDisplay = /** An object that configures a {@link Repeatable}. */ export interface RepeatableOptions { /** Whether this repeatable should be visible. */ - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; /** The requirement(s) to increase this repeatable. */ requirements: Requirements; /** The maximum amount obtainable for this repeatable. */ @@ -108,7 +108,7 @@ export type Repeatable<T extends RepeatableOptions> = Replace< export type GenericRepeatable = Replace< Repeatable<RepeatableOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; limit: ProcessedComputable<DecimalSource>; } >; diff --git a/src/features/tabs/TabButton.vue b/src/features/tabs/TabButton.vue index c9347d1..fcd8942 100644 --- a/src/features/tabs/TabButton.vue +++ b/src/features/tabs/TabButton.vue @@ -1,11 +1,11 @@ <template> <button - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" @click="selectTab" class="tabButton" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, glowColorStyle, unref(style) ?? {} @@ -21,7 +21,7 @@ <script lang="ts"> import type { CoercableComponent, StyleValue } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import { getNotifyStyle } from "game/notifications"; import { computeComponent, processedPropType, unwrapRef } from "util/vue"; import { computed, defineComponent, toRefs, unref } from "vue"; @@ -29,7 +29,7 @@ import { computed, defineComponent, toRefs, unref } from "vue"; export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, display: { @@ -50,7 +50,7 @@ export default defineComponent({ const glowColorStyle = computed(() => { const color = unwrapRef(glowColor); - if (!color) { + if (color != null) { return {}; } if (unref(floating)) { @@ -68,7 +68,9 @@ export default defineComponent({ component, glowColorStyle, unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/tabs/TabFamily.vue b/src/features/tabs/TabFamily.vue index 437581b..436f81a 100644 --- a/src/features/tabs/TabFamily.vue +++ b/src/features/tabs/TabFamily.vue @@ -1,11 +1,11 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" class="tab-family-container" :class="{ ...unref(classes), ...tabClasses }" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? [], tabStyle ?? [] @@ -37,7 +37,7 @@ import Sticky from "components/layout/Sticky.vue"; import themes from "data/themes"; import type { CoercableComponent, StyleValue } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import type { GenericTab } from "features/tabs/tab"; import TabButton from "features/tabs/TabButton.vue"; import type { GenericTabButton } from "features/tabs/tabFamily"; @@ -49,7 +49,7 @@ import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } fro export default defineComponent({ props: { visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, activeTab: { @@ -123,7 +123,9 @@ export default defineComponent({ Visibility, component, gatherButtonProps, - unref + unref, + isVisible, + isHidden }; } }); diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index a1281e0..51d298e 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -1,5 +1,12 @@ import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; -import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature"; +import { + Component, + GatherProps, + getUniqueID, + isVisible, + setDefault, + Visibility +} from "features/feature"; import TabButtonComponent from "features/tabs/TabButton.vue"; import TabFamilyComponent from "features/tabs/TabFamily.vue"; import type { Persistent } from "game/persistence"; @@ -20,7 +27,7 @@ export const TabButtonType = Symbol("TabButton"); export const TabFamilyType = Symbol("TabFamily"); export interface TabButtonOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; tab: Computable<GenericTab | CoercableComponent>; display: Computable<CoercableComponent>; classes?: Computable<Record<string, boolean>>; @@ -48,12 +55,12 @@ export type TabButton<T extends TabButtonOptions> = Replace< export type GenericTabButton = Replace< TabButton<TabButtonOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; export interface TabFamilyOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; classes?: Computable<Record<string, boolean>>; style?: Computable<StyleValue>; buttonContainerClasses?: Computable<Record<string, boolean>>; @@ -81,7 +88,7 @@ export type TabFamily<T extends TabFamilyOptions> = Replace< export type GenericTabFamily = Replace< TabFamily<TabFamilyOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; @@ -123,15 +130,10 @@ export function createTabFamily<T extends TabFamilyOptions>( tabFamily.selected = selected; tabFamily.activeTab = computed(() => { const tabs = unref(processedTabFamily.tabs); - if ( - selected.value in tabs && - unref(tabs[selected.value].visibility) === Visibility.Visible - ) { + if (selected.value in tabs && isVisible(tabs[selected.value].visibility)) { return unref(tabs[selected.value].tab); } - const firstTab = Object.values(tabs).find( - tab => unref(tab.visibility) === Visibility.Visible - ); + const firstTab = Object.values(tabs).find(tab => isVisible(tab.visibility)); if (firstTab) { return unref(firstTab.tab); } diff --git a/src/features/trees/TreeNode.vue b/src/features/trees/TreeNode.vue index 6c6cb09..ea48eeb 100644 --- a/src/features/trees/TreeNode.vue +++ b/src/features/trees/TreeNode.vue @@ -1,7 +1,7 @@ <template> <div - v-if="unref(visibility) !== Visibility.None" - :style="{ visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined }" + v-if="isVisible(visibility)" + :style="{ visibility: isHidden(visibility) ? 'hidden' : undefined }" :class="{ treeNode: true, can: unref(canClick), @@ -37,7 +37,7 @@ import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { CoercableComponent, StyleValue } from "features/feature"; -import { Visibility } from "features/feature"; +import { isHidden, isVisible, Visibility } from "features/feature"; import { computeOptionalComponent, isCoercableComponent, @@ -51,7 +51,7 @@ export default defineComponent({ props: { display: processedPropType<CoercableComponent>(Object, String, Function), visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, style: processedPropType<StyleValue>(String, Object, Array), @@ -87,7 +87,9 @@ export default defineComponent({ comp, unref, Visibility, - isCoercableComponent + isCoercableComponent, + isVisible, + isHidden }; } }); diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index 99c759a..bcbf333 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -23,7 +23,7 @@ export const TreeNodeType = Symbol("TreeNode"); export const TreeType = Symbol("Tree"); export interface TreeNodeOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; canClick?: Computable<boolean>; color?: Computable<string>; display?: Computable<CoercableComponent>; @@ -60,7 +60,7 @@ export type TreeNode<T extends TreeNodeOptions> = Replace< export type GenericTreeNode = Replace< TreeNode<TreeNodeOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; canClick: ProcessedComputable<boolean>; } >; @@ -141,7 +141,7 @@ export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> { } export interface TreeOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; nodes: Computable<GenericTreeNode[][]>; leftSideNodes?: Computable<GenericTreeNode[]>; rightSideNodes?: Computable<GenericTreeNode[]>; @@ -175,7 +175,7 @@ export type Tree<T extends TreeOptions> = Replace< export type GenericTree = Replace< Tree<TreeOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; diff --git a/src/features/upgrades/Upgrade.vue b/src/features/upgrades/Upgrade.vue index 7c45d75..d1fb6c3 100644 --- a/src/features/upgrades/Upgrade.vue +++ b/src/features/upgrades/Upgrade.vue @@ -1,9 +1,9 @@ <template> <button - v-if="unref(visibility) !== Visibility.None" + v-if="isVisible(visibility)" :style="[ { - visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined + visibility: isHidden(visibility) ? 'hidden' : undefined }, unref(style) ?? {} ]" @@ -29,7 +29,7 @@ import "components/common/features.css"; import MarkNode from "components/MarkNode.vue"; import Node from "components/Node.vue"; import type { StyleValue } from "features/feature"; -import { jsx, Visibility } from "features/feature"; +import { isHidden, isVisible, jsx, Visibility } from "features/feature"; import type { GenericUpgrade } from "features/upgrades/upgrade"; import { displayRequirements, Requirements } from "game/requirements"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue"; @@ -43,7 +43,7 @@ export default defineComponent({ required: true }, visibility: { - type: processedPropType<Visibility>(Number), + type: processedPropType<Visibility | boolean>(Number, Boolean), required: true }, style: processedPropType<StyleValue>(String, Object, Array), @@ -115,7 +115,9 @@ export default defineComponent({ return { component, unref, - Visibility + Visibility, + isVisible, + isHidden }; } }); diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 656d891..5eda0e0 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -39,7 +39,7 @@ import { computed, unref } from "vue"; export const UpgradeType = Symbol("Upgrade"); export interface UpgradeOptions { - visibility?: Computable<Visibility>; + visibility?: Computable<Visibility | boolean>; classes?: Computable<Record<string, boolean>>; style?: Computable<StyleValue>; display?: Computable< @@ -80,7 +80,7 @@ export type Upgrade<T extends UpgradeOptions> = Replace< export type GenericUpgrade = Replace< Upgrade<UpgradeOptions>, { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; } >; diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 189e452..9412e22 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -1,5 +1,5 @@ import { isArray } from "@vue/shared"; -import { CoercableComponent, jsx, setDefault, Visibility } from "features/feature"; +import { CoercableComponent, isVisible, jsx, setDefault, Visibility } from "features/feature"; import { displayResource, Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "lib/break_eternity"; import { @@ -35,7 +35,7 @@ export interface Requirement { /** * Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property. */ - visibility: ProcessedComputable<Visibility.Visible | Visibility.None>; + visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>; /** * Whether or not this requirement has been met. */ @@ -73,7 +73,7 @@ export interface CostRequirementOptions { /** * Pass-through to {@link Requirement.visibility}. */ - visibility?: Computable<Visibility.Visible | Visibility.None>; + visibility?: Computable<Visibility.Visible | Visibility.None | boolean>; /** * Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}. */ @@ -193,10 +193,10 @@ export function createCostRequirement<T extends CostRequirementOptions>( * @param feature The feature to check the visibility of */ export function createVisibilityRequirement(feature: { - visibility: ProcessedComputable<Visibility>; + visibility: ProcessedComputable<Visibility | boolean>; }): Requirement { return createLazyProxy(() => ({ - requirementMet: computed(() => unref(feature.visibility) === Visibility.Visible), + requirementMet: computed(() => isVisible(feature.visibility)), visibility: Visibility.None, requiresPay: false })); @@ -256,7 +256,7 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource { */ export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (isArray(requirements)) { - requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible); + requirements = requirements.filter(r => isVisible(r.visibility)); if (requirements.length === 1) { requirements = requirements[0]; } diff --git a/src/util/vue.tsx b/src/util/vue.tsx index 3c8c20f..20c9b5a 100644 --- a/src/util/vue.tsx +++ b/src/util/vue.tsx @@ -1,7 +1,14 @@ import Col from "components/layout/Column.vue"; import Row from "components/layout/Row.vue"; import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature"; -import { Component as ComponentKey, GatherProps, jsx, Visibility } from "features/feature"; +import { + Component as ComponentKey, + GatherProps, + isVisible, + jsx, + Visibility +} from "features/feature"; +import settings from "game/settings"; import type { ProcessedComputable } from "util/computed"; import { DoNotCache } from "util/computed"; import type { Component, ComputedRef, DefineComponent, PropType, Ref, ShallowRef } from "vue"; @@ -147,7 +154,7 @@ export function setupHoldToClick( } export function getFirstFeature< - T extends VueFeature & { visibility: ProcessedComputable<Visibility> } + T extends VueFeature & { visibility: ProcessedComputable<Visibility | boolean> } >( features: T[], filter: (feature: T) => boolean @@ -157,9 +164,7 @@ export function getFirstFeature< hasCollapsedContent: Ref<boolean>; } { const filteredFeatures = computed(() => - features.filter( - feature => unref(feature.visibility) === Visibility.Visible && filter(feature) - ) + features.filter(feature => isVisible(feature.visibility) && filter(feature)) ); return { firstFeature: computed(() => filteredFeatures.value[0]), From c85e3a0096a3037e93cdf6c63aee60d0da2ab885 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 20:58:53 -0600 Subject: [PATCH 46/56] Update package-lock.json --- package-lock.json | 1655 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- 2 files changed, 1583 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34e4ace..7082e7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.17.1", + "vitest": "^0.28.5", "vue-tsc": "^0.38.1" }, "engines": { @@ -1606,6 +1606,166 @@ "resolved": "https://registry.npmjs.org/@cush/relative/-/relative-1.0.0.tgz", "integrity": "sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==" }, + "node_modules/@esbuild/android-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-loong64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", @@ -1621,6 +1781,182 @@ "node": ">=12" } }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -2079,9 +2415,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", "dev": true }, "node_modules/@types/chai-subset": { @@ -2381,6 +2717,77 @@ "node": ">=12.0.0" } }, + "node_modules/@vitest/expect": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz", + "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "chai": "^4.3.7" + } + }, + "node_modules/@vitest/runner": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz", + "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.28.5", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/spy": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz", + "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==", + "dev": true, + "dependencies": { + "tinyspy": "^1.0.2" + } + }, + "node_modules/@vitest/utils": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz", + "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "diff": "^5.1.0", + "loupe": "^2.3.6", + "picocolors": "^1.0.0", + "pretty-format": "^27.5.1" + } + }, "node_modules/@volar/code-gen": { "version": "0.38.9", "resolved": "https://registry.npmjs.org/@volar/code-gen/-/code-gen-0.38.9.tgz", @@ -2584,9 +2991,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "bin": { "acorn": "bin/acorn" }, @@ -2863,6 +3270,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2911,14 +3327,14 @@ ] }, "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", - "deep-eql": "^3.0.1", + "deep-eql": "^4.1.2", "get-func-name": "^2.0.0", "loupe": "^2.3.1", "pathval": "^1.1.1", @@ -2950,6 +3366,22 @@ "node": "*" } }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3130,15 +3562,15 @@ "dev": true }, "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "dependencies": { "type-detect": "^4.0.0" }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, "node_modules/deep-is": { @@ -3179,6 +3611,15 @@ "node": ">=0.4.0" } }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3221,6 +3662,12 @@ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "peer": true }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ejs": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", @@ -3240,6 +3687,12 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz", "integrity": "sha512-ICHvGaCIQR3P88uK8aRtx8gmejbVJyC6bB4LEC3anzBrIzdzC7aiZHY4iFfXhN4st6I7lMO0x4sgBHf/7kBvRw==" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/entities": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.1.tgz", @@ -4770,6 +5223,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5152,6 +5617,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -5249,9 +5720,9 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", "dev": true, "dependencies": { "get-func-name": "^2.0.0" @@ -5347,6 +5818,18 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "node_modules/mlly": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz", + "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==", + "dev": true, + "dependencies": { + "acorn": "^8.8.1", + "pathe": "^1.0.0", + "pkg-types": "^1.0.1", + "ufo": "^1.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5568,6 +6051,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -5593,10 +6082,21 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz", + "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.0.0", + "pathe": "^1.0.0" + } + }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "funding": [ { "type": "opencollective", @@ -5677,6 +6177,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -5728,6 +6254,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "node_modules/recrawl-sync": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.2.tgz", @@ -6016,6 +6548,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6024,6 +6562,34 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/sortablejs": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", @@ -6059,6 +6625,62 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", + "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -6156,6 +6778,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6237,19 +6871,25 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinybench": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", + "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "dev": true + }, "node_modules/tinypool": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.4.tgz", - "integrity": "sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", + "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-0.3.3.tgz", - "integrity": "sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", + "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6378,6 +7018,12 @@ "node": ">=4.2.0" } }, + "node_modules/ufo": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz", + "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6555,6 +7201,149 @@ } } }, + "node_modules/vite-node": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", + "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.1.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "source-map-support": "^0.5.21", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" + } + }, + "node_modules/vite-node/node_modules/rollup": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", + "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", + "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "dev": true, + "dependencies": { + "esbuild": "^0.16.14", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.10.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vite-plugin-pwa": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.12.3.tgz", @@ -6591,20 +7380,35 @@ } }, "node_modules/vitest": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.17.1.tgz", - "integrity": "sha512-d6NsFC6FPmZ5XdiSYfW5rwJ/b8060wqe2steNNlVbhO69HWma6CucIm5g7PXlCSkmKvrdEbUsZHPAarlH83VGw==", + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz", + "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==", "dev": true, "dependencies": { - "@types/chai": "^4.3.1", + "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "chai": "^4.3.6", + "@vitest/expect": "0.28.5", + "@vitest/runner": "0.28.5", + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "acorn": "^8.8.1", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", "debug": "^4.3.4", - "local-pkg": "^0.4.1", - "tinypool": "^0.2.1", - "tinyspy": "^0.3.3", - "vite": "^2.9.12 || ^3.0.0-0" + "local-pkg": "^0.4.2", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "std-env": "^3.3.1", + "strip-literal": "^1.0.0", + "tinybench": "^2.3.1", + "tinypool": "^0.3.1", + "tinyspy": "^1.0.2", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.28.5", + "why-is-node-running": "^2.2.2" }, "bin": { "vitest": "vitest.mjs" @@ -6617,8 +7421,8 @@ }, "peerDependencies": { "@edge-runtime/vm": "*", + "@vitest/browser": "*", "@vitest/ui": "*", - "c8": "*", "happy-dom": "*", "jsdom": "*" }, @@ -6626,10 +7430,10 @@ "@edge-runtime/vm": { "optional": true }, - "@vitest/ui": { + "@vitest/browser": { "optional": true }, - "c8": { + "@vitest/ui": { "optional": true }, "happy-dom": { @@ -6640,6 +7444,133 @@ } } }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" + } + }, + "node_modules/vitest/node_modules/rollup": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", + "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", + "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "dev": true, + "dependencies": { + "esbuild": "^0.16.14", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.10.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, "node_modules/vue": { "version": "3.2.37", "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz", @@ -6905,6 +7836,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -8277,12 +9224,159 @@ "resolved": "https://registry.npmjs.org/@cush/relative/-/relative-1.0.0.tgz", "integrity": "sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==" }, + "@esbuild/android-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", + "dev": true, + "optional": true + }, "@esbuild/linux-loong64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", "optional": true }, + "@esbuild/linux-mips64el": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -8623,9 +9717,9 @@ "dev": true }, "@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", "dev": true }, "@types/chai-subset": { @@ -8819,6 +9913,67 @@ "hash-sum": "^2.0.0" } }, + "@vitest/expect": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz", + "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==", + "dev": true, + "requires": { + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "chai": "^4.3.7" + } + }, + "@vitest/runner": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz", + "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==", + "dev": true, + "requires": { + "@vitest/utils": "0.28.5", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "dependencies": { + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "@vitest/spy": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz", + "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==", + "dev": true, + "requires": { + "tinyspy": "^1.0.2" + } + }, + "@vitest/utils": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz", + "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==", + "dev": true, + "requires": { + "cli-truncate": "^3.1.0", + "diff": "^5.1.0", + "loupe": "^2.3.6", + "picocolors": "^1.0.0", + "pretty-format": "^27.5.1" + } + }, "@volar/code-gen": { "version": "0.38.9", "resolved": "https://registry.npmjs.org/@volar/code-gen/-/code-gen-0.38.9.tgz", @@ -9008,9 +10163,9 @@ "dev": true }, "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==" + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" }, "acorn-globals": { "version": "6.0.0", @@ -9216,6 +10371,12 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" }, + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9242,14 +10403,14 @@ "integrity": "sha512-OO+pPubxx16lkI7TVrbFpde8XHz66SMwstl1YWpg6uMGw56XnhYVwtPIjvX4kYpzwMwQKr4DDce394E03dQPGg==" }, "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", - "deep-eql": "^3.0.1", + "deep-eql": "^4.1.2", "get-func-name": "^2.0.0", "loupe": "^2.3.1", "pathval": "^1.1.1", @@ -9272,6 +10433,16 @@ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, + "cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9415,9 +10586,9 @@ "dev": true }, "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "requires": { "type-detect": "^4.0.0" @@ -9449,6 +10620,12 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -9482,6 +10659,12 @@ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "peer": true }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ejs": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", @@ -9495,6 +10678,12 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.225.tgz", "integrity": "sha512-ICHvGaCIQR3P88uK8aRtx8gmejbVJyC6bB4LEC3anzBrIzdzC7aiZHY4iFfXhN4st6I7lMO0x4sgBHf/7kBvRw==" }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "entities": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.1.tgz", @@ -10510,6 +11699,12 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -10777,6 +11972,12 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -10850,9 +12051,9 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", "dev": true, "requires": { "get-func-name": "^2.0.0" @@ -10927,6 +12128,18 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "mlly": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz", + "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==", + "dev": true, + "requires": { + "acorn": "^8.8.1", + "pathe": "^1.0.0", + "pkg-types": "^1.0.1", + "ufo": "^1.0.1" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11091,6 +12304,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true + }, "pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -11107,10 +12326,21 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "pkg-types": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz", + "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.0.0", + "pathe": "^1.0.0" + } + }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "requires": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", @@ -11154,6 +12384,25 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz", "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==" }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -11184,6 +12433,12 @@ "safe-buffer": "^5.1.0" } }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "recrawl-sync": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.2.tgz", @@ -11390,11 +12645,35 @@ "object-inspect": "^1.9.0" } }, + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + } + } + }, "sortablejs": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", @@ -11424,6 +12703,46 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", + "integrity": "sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, "string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -11494,6 +12813,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "requires": { + "acorn": "^8.8.2" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -11551,16 +12879,22 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tinybench": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.1.tgz", + "integrity": "sha512-hGYWYBMPr7p4g5IarQE7XhlyWveh1EKhy4wUBS1LrHXCKYgvz+4/jCqgmJqZxxldesn05vccrtME2RLLZNW7iA==", + "dev": true + }, "tinypool": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.4.tgz", - "integrity": "sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", + "integrity": "sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==", "dev": true }, "tinyspy": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-0.3.3.tgz", - "integrity": "sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.1.1.tgz", + "integrity": "sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==", "dev": true }, "to-fast-properties": { @@ -11646,6 +12980,12 @@ "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true }, + "ufo": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz", + "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", + "dev": true + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -11760,6 +13100,83 @@ "rollup": ">=2.59.0 <2.78.0" } }, + "vite-node": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", + "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.1.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "source-map-support": "^0.5.21", + "vite": "^3.0.0 || ^4.0.0" + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" + } + }, + "rollup": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", + "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "vite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", + "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "dev": true, + "requires": { + "esbuild": "^0.16.14", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.10.0" + } + } + } + }, "vite-plugin-pwa": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.12.3.tgz", @@ -11785,20 +13202,102 @@ } }, "vitest": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.17.1.tgz", - "integrity": "sha512-d6NsFC6FPmZ5XdiSYfW5rwJ/b8060wqe2steNNlVbhO69HWma6CucIm5g7PXlCSkmKvrdEbUsZHPAarlH83VGw==", + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz", + "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==", "dev": true, "requires": { - "@types/chai": "^4.3.1", + "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "chai": "^4.3.6", + "@vitest/expect": "0.28.5", + "@vitest/runner": "0.28.5", + "@vitest/spy": "0.28.5", + "@vitest/utils": "0.28.5", + "acorn": "^8.8.1", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", "debug": "^4.3.4", - "local-pkg": "^0.4.1", - "tinypool": "^0.2.1", - "tinyspy": "^0.3.3", - "vite": "^2.9.12 || ^3.0.0-0" + "local-pkg": "^0.4.2", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "source-map": "^0.6.1", + "std-env": "^3.3.1", + "strip-literal": "^1.0.0", + "tinybench": "^2.3.1", + "tinypool": "^0.3.1", + "tinyspy": "^1.0.2", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.28.5", + "why-is-node-running": "^2.2.2" + }, + "dependencies": { + "@esbuild/linux-loong64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", + "dev": true, + "optional": true + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "esbuild": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" + } + }, + "rollup": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", + "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "vite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", + "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "dev": true, + "requires": { + "esbuild": "^0.16.14", + "fsevents": "~2.3.2", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.10.0" + } + } } }, "vue": { @@ -12008,6 +13507,16 @@ "is-symbol": "^1.0.3" } }, + "why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index b548099..8403380 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.28.6", + "vitest": "^0.28.5", "vue-tsc": "^0.38.1" }, "engines": { From 839baaf7084e799075ad5458daa6cf785d1164d3 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 21:05:26 -0600 Subject: [PATCH 47/56] Update some VSC settings --- .vscode/settings.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 77377bd..d46602a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "vitest.commandLine": "npx vitest" -} \ No newline at end of file + "vitest.commandLine": "npx vitest", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "git.ignoreLimitWarning": true, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "typescript.tsdk": "node_modules/typescript/lib" +} From d3f5e3bed7078457fd25a65df1a0dc10980aa2d7 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 21:08:10 -0600 Subject: [PATCH 48/56] Fix some merge issues --- src/data/common.tsx | 4 ++-- src/util/vue.tsx | 1 - tests/game/formulas.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index 0af59cc..b190f0a 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -5,9 +5,10 @@ import type { GenericConversion } from "features/conversion"; import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature"; import { jsx, setDefault } from "features/feature"; import { GenericMilestone } from "features/milestones/milestone"; -import { displayResource } from "features/resources/resource"; +import { displayResource, Resource } from "features/resources/resource"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree"; +import { GenericFormula } from "game/formulas"; import type { Modifier } from "game/modifiers"; import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; @@ -25,7 +26,6 @@ import { convertComputable, processComputable } from "util/computed"; import { getFirstFeature, renderColJSX, renderJSX } from "util/vue"; import type { ComputedRef, Ref } from "vue"; import { computed, unref } from "vue"; -import Formula, { GenericFormula } from "game/formulas"; import "./common.css"; /** An object that configures a {@link ResetButton} */ diff --git a/src/util/vue.tsx b/src/util/vue.tsx index 20c9b5a..a3897a5 100644 --- a/src/util/vue.tsx +++ b/src/util/vue.tsx @@ -8,7 +8,6 @@ import { jsx, Visibility } from "features/feature"; -import settings from "game/settings"; import type { ProcessedComputable } from "util/computed"; import { DoNotCache } from "util/computed"; import type { Component, ComputedRef, DefineComponent, PropType, Ref, ShallowRef } from "vue"; diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 0be2795..ddc0bee 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -930,7 +930,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [1], evaluate: () => 10, - integrate: val => val ?? 20 + integrate: (val, v1) => val ?? 20 }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => @@ -958,7 +958,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [1], evaluate: () => 10, - invertIntegral: val => 1, + invertIntegral: (val, v1) => 1, hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); From e0e325e048b85f5779780bc3352f92a373996e43 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 21:58:06 -0600 Subject: [PATCH 49/56] Settings overhaul --- src/components/Nav.vue | 4 +- src/components/Options.vue | 118 +++++++++++++++++++++----- src/components/common/modifiers.css | 5 +- src/components/fields/Select.vue | 4 + src/components/fields/Toggle.vue | 8 +- src/data/common.tsx | 8 +- src/features/challenges/challenge.tsx | 7 +- src/features/milestones/milestone.tsx | 7 +- src/game/modifiers.tsx | 3 +- src/game/settings.ts | 8 +- 10 files changed, 140 insertions(+), 32 deletions(-) diff --git a/src/components/Nav.vue b/src/components/Nav.vue index d2017a8..b265635 100644 --- a/src/components/Nav.vue +++ b/src/components/Nav.vue @@ -42,7 +42,7 @@ </Tooltip> </div> <div @click="options?.open()"> - <Tooltip display="Options" :direction="Direction.Down" xoffset="-66px"> + <Tooltip display="Settings" :direction="Direction.Down" xoffset="-66px"> <span class="material-icons">settings</span> </Tooltip> </div> @@ -59,7 +59,7 @@ </Tooltip> </div> <div @click="options?.open()"> - <Tooltip display="Options" :direction="Direction.Right"> + <Tooltip display="Settings" :direction="Direction.Right"> <span class="material-icons">settings</span> </Tooltip> </div> diff --git a/src/components/Options.vue b/src/components/Options.vue index bd480eb..93b57fa 100644 --- a/src/components/Options.vue +++ b/src/components/Options.vue @@ -2,18 +2,27 @@ <Modal v-model="isOpen"> <template v-slot:header> <div class="header"> - <h2>Options</h2> + <h2>Settings</h2> + <div class="option-tabs"> + <button :class="{selected: isTab('behaviour')}" @click="setTab('behaviour')">Behaviour</button> + <button :class="{selected: isTab('appearance')}" @click="setTab('appearance')">Appearance</button> + </div> </div> </template> <template v-slot:body> - <Select title="Theme" :options="themes" v-model="theme" /> - <component :is="settingFieldsComponent" /> - <Toggle title="Show TPS" v-model="showTPS" /> - <hr /> - <Toggle title="Unthrottled" v-model="unthrottled" /> - <Toggle :title="offlineProdTitle" v-model="offlineProd" /> - <Toggle :title="autosaveTitle" v-model="autosave" /> - <Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" /> + <div v-if="isTab('behaviour')"> + <Toggle :title="unthrottledTitle" v-model="unthrottled" /> + <Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" /> + <Toggle :title="offlineProdTitle" v-model="offlineProd" /> + <Toggle :title="autosaveTitle" v-model="autosave" /> + <FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton> + </div> + <div v-if="isTab('appearance')"> + <Select :title="themeTitle" :options="themes" v-model="theme" /> + <component :is="settingFieldsComponent" /> + <Toggle :title="showTPSTitle" v-model="showTPS" /> + <Toggle :title="alignModifierUnitsTitle" v-model="alignUnits" /> + </div> </template> </Modal> </template> @@ -21,20 +30,34 @@ <script setup lang="tsx"> import Modal from "components/Modal.vue"; import projInfo from "data/projInfo.json"; +import { save } from "util/save"; import rawThemes from "data/themes"; import { jsx } from "features/feature"; import Tooltip from "features/tooltips/Tooltip.vue"; import player from "game/player"; import settings, { settingFields } from "game/settings"; -import { camelToTitle } from "util/common"; +import { camelToTitle, Direction } from "util/common"; import { coerceComponent, render } from "util/vue"; import { computed, ref, toRefs } from "vue"; import Select from "./fields/Select.vue"; import Toggle from "./fields/Toggle.vue"; +import FeedbackButton from "./fields/FeedbackButton.vue"; const isOpen = ref(false); +const currentTab = ref("behaviour"); + +function isTab(tab: string): boolean { + return tab == currentTab.value; +} + +function setTab(tab: string) { + currentTab.value = tab; +} defineExpose({ + isTab, + setTab, + save, open() { isOpen.value = true; } @@ -49,7 +72,7 @@ const settingFieldsComponent = computed(() => { return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>))); }); -const { showTPS, theme, unthrottled } = toRefs(settings); +const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings); const { autosave, offlineProd } = toRefs(player); const isPaused = computed({ get() { @@ -60,30 +83,85 @@ const isPaused = computed({ } }); +const unthrottledTitle = jsx(() => ( + <span class="option-title"> + Unthrottled + <desc>Allow the game to run as fast as possible. Not battery friendly.</desc> + </span> +)); const offlineProdTitle = jsx(() => ( - <span> - Offline Production<Tooltip display="Save-specific">*</Tooltip> + <span class="option-title"> + Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> + <desc>Simulate production that occurs while the game is closed.</desc> </span> )); const autosaveTitle = jsx(() => ( - <span> - Autosave<Tooltip display="Save-specific">*</Tooltip> + <span class="option-title"> + Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> + <desc>Automatically save the game every second or when the game is closed.</desc> </span> )); const isPausedTitle = jsx(() => ( - <span> - Pause game<Tooltip display="Save-specific">*</Tooltip> + <span class="option-title"> + Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip> + <desc>Stop everything from moving.</desc> + </span> +)); +const themeTitle = jsx(() => ( + <span class="option-title"> + Theme + <desc>How the game looks.</desc> + </span> +)); +const showTPSTitle = jsx(() => ( + <span class="option-title"> + Show TPS + <desc>Show TPS meter at the bottom-left corner of the page.</desc> + </span> +)); +const alignModifierUnitsTitle = jsx(() => ( + <span class="option-title"> + Align modifier units + <desc>Align numbers to the beginning of the unit in modifier view.</desc> </span> )); </script> -<style scoped> -.header { +<style> +.option-tabs { + border-bottom: 2px solid var(--outline); + margin-top: 10px; margin-bottom: -10px; } -*:deep() .tooltip-container { +.option-tabs button { + background-color: transparent; + color: var(--foreground); + margin-bottom: -2px; + font-size: 14px; + cursor: pointer; + padding: 5px 20px; + border: none; + border-bottom: 2px solid var(--foreground); +} + +.option-tabs button:not(.selected) { + border-bottom-color: transparent; +} + +.option-title .tooltip-container { display: inline; margin-left: 5px; } +.option-title desc { + display: block; + opacity: 0.6; + font-size: small; + width: 300px; + margin-left: 0; +} + +.save-button { + text-align: right; +} </style> diff --git a/src/components/common/modifiers.css b/src/components/common/modifiers.css index 6842e0c..dc9e66d 100644 --- a/src/components/common/modifiers.css +++ b/src/components/common/modifiers.css @@ -8,10 +8,13 @@ } .modifier-amount { - flex-basis: 100px; flex-shrink: 0; text-align: right; } +:not(:first-of-type, :last-of-type) > .modifier-amount::after { + content: var(--unit); + opacity: 0; +} .modifier-description { flex-grow: 1; diff --git a/src/components/fields/Select.vue b/src/components/fields/Select.vue index f08c40f..72a31b2 100644 --- a/src/components/fields/Select.vue +++ b/src/components/fields/Select.vue @@ -87,6 +87,10 @@ function onUpdate(value: SelectOption) { background-color: var(--bought); } +.vue-input input { + font-size: inherit; +} + .vue-input input::placeholder { color: var(--link); } diff --git a/src/components/fields/Toggle.vue b/src/components/fields/Toggle.vue index 564a2ca..4017033 100644 --- a/src/components/fields/Toggle.vue +++ b/src/components/fields/Toggle.vue @@ -43,14 +43,16 @@ input { span { width: 100%; + padding-right: 41px; position: relative; } /* track */ input + span::before { content: ""; - float: right; - margin: 5px 0 5px 10px; + position: absolute; + top: calc(50% - 7px); + right: 0px; border-radius: 7px; width: 36px; height: 14px; @@ -64,7 +66,7 @@ input + span::before { input + span::after { content: ""; position: absolute; - top: 2px; + top: calc(50% - 10px); right: 16px; border-radius: 50%; width: 20px; diff --git a/src/data/common.tsx b/src/data/common.tsx index b190f0a..9f1436f 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -13,6 +13,7 @@ import type { Modifier } from "game/modifiers"; import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; import player from "game/player"; +import settings from "game/settings"; import type { DecimalSource } from "util/bignum"; import Decimal, { format, formatSmall, formatTime } from "util/bignum"; import type { WithRequired } from "util/common"; @@ -335,7 +336,12 @@ export function createCollapsibleModifierSections( return ( <> {hasPreviousSection ? <br /> : null} - <div> + <div + style={{ + "--unit": + settings.alignUnits && s.unit != null ? "'" + s.unit + "'" : "" + }} + > {header} <br /> {modifiers} diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 3063b33..ef4c77d 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -313,7 +313,12 @@ globalBus.on("loadSettings", settings => { registerSettingField( jsx(() => ( <Toggle - title="Hide Maxed Challenges" + title={jsx(() => ( + <span class="option-title"> + Hide maxed challenges + <desc>Hide challenges that have been fully completed.</desc> + </span> + ))} onUpdate:modelValue={value => (settings.hideChallenges = value)} modelValue={settings.hideChallenges} /> diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index 7dfe7f1..6ea736b 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -221,7 +221,12 @@ const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({ registerSettingField( jsx(() => ( <Select - title="Show Milestones" + title={jsx(() => ( + <span class="option-title"> + Show milestones + <desc>Select which milestones to display based on criterias.</desc> + </span> + ))} options={msDisplayOptions} onUpdate:modelValue={value => (settings.msDisplay = value as MilestoneDisplay)} modelValue={settings.msDisplay} diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index eb4ec2e..fd96c77 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -1,6 +1,7 @@ import "components/common/modifiers.css"; import type { CoercableComponent } from "features/feature"; import { jsx } from "features/feature"; +import settings from "game/settings"; import type { DecimalSource } from "util/bignum"; import Decimal, { formatSmall } from "util/bignum"; import type { WithRequired } from "util/common"; @@ -326,7 +327,7 @@ export function createModifierSection({ }: ModifierSectionOptions) { const total = modifier.apply(base ?? 1); return ( - <div> + <div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}> <h3> {title} {subtitle == null ? null : <span class="subtitle"> ({subtitle})</span>} diff --git a/src/game/settings.ts b/src/game/settings.ts index cad4438..0748d68 100644 --- a/src/game/settings.ts +++ b/src/game/settings.ts @@ -18,6 +18,8 @@ export interface Settings { theme: Themes; /** Whether or not to cap the project at 20 ticks per second. */ unthrottled: boolean; + /** Whether to align modifiers to the unit. */ + alignUnits: boolean; } const state = reactive<Partial<Settings>>({ @@ -25,7 +27,8 @@ const state = reactive<Partial<Settings>>({ saves: [], showTPS: true, theme: Themes.Nordic, - unthrottled: false + unthrottled: false, + alignUnits: false }); watch( @@ -57,7 +60,8 @@ export const hardResetSettings = (window.hardResetSettings = () => { active: "", saves: [], showTPS: true, - theme: Themes.Nordic + theme: Themes.Nordic, + alignUnits: false }; globalBus.emit("loadSettings", settings); Object.assign(state, settings); From 940fd4c2ebad81091d69137f0648ed8db8010c41 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Mon, 12 Dec 2022 23:50:35 -0600 Subject: [PATCH 50/56] Add minimizedDisplay --- src/components/Game.vue | 5 +++-- src/components/Layer.vue | 12 ++++++++---- src/game/layers.tsx | 9 ++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/Game.vue b/src/components/Game.vue index 6f9bb66..9dd1e27 100644 --- a/src/components/Game.vue +++ b/src/components/Game.vue @@ -36,8 +36,9 @@ const layerKeys = computed(() => Object.keys(layers)); const useHeader = projInfo.useHeader; function gatherLayerProps(layer: GenericLayer) { - const { display, minimized, minWidth, name, color, minimizable, nodes } = layer; - return { display, minimized, minWidth, name, color, minimizable, nodes }; + const { display, minimized, minWidth, name, color, minimizable, nodes, minimizedDisplay } = + layer; + return { display, minimized, minWidth, name, color, minimizable, nodes, minimizedDisplay }; } </script> diff --git a/src/components/Layer.vue b/src/components/Layer.vue index a64a255..abdf2bf 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -2,7 +2,8 @@ <div class="layer-container" :style="{ '--layer-color': unref(color) }"> <button v-if="showGoBack" class="goBack" @click="goBack">←</button> <button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false"> - <div>{{ unref(name) }}</div> + <component v-if="minimizedComponent" :is="minimizedComponent" /> + <div v-else>{{ unref(name) }}</div> </button> <div class="layer-tab" :class="{ showGoBack }" v-else> <Context @update-nodes="updateNodes"> @@ -21,7 +22,7 @@ import type { CoercableComponent } from "features/feature"; import type { FeatureNode } from "game/layers"; import type { Persistent } from "game/persistence"; import player from "game/player"; -import { computeComponent, processedPropType, wrapRef } from "util/vue"; +import { computeComponent, computeOptionalComponent, processedPropType, wrapRef } from "util/vue"; import type { PropType, Ref } from "vue"; import { computed, defineComponent, nextTick, toRefs, unref, watch } from "vue"; import Context from "./Context.vue"; @@ -41,6 +42,7 @@ export default defineComponent({ type: processedPropType<CoercableComponent>(Object, String, Function), required: true }, + minimizedDisplay: processedPropType<CoercableComponent>(Object, String, Function), minimized: { type: Object as PropType<Persistent<boolean>>, required: true @@ -61,9 +63,10 @@ export default defineComponent({ } }, setup(props) { - const { display, index, minimized, minWidth, tab } = toRefs(props); + const { display, index, minimized, minWidth, tab, minimizedDisplay } = toRefs(props); const component = computeComponent(display); + const minimizedComponent = computeOptionalComponent(minimizedDisplay); const showGoBack = computed( () => projInfo.allowGoBack && index.value > 0 && !minimized.value ); @@ -106,6 +109,7 @@ export default defineComponent({ return { component, + minimizedComponent, showGoBack, updateNodes, unref, @@ -155,7 +159,7 @@ export default defineComponent({ background-color: transparent; } -.layer-tab.minimized div { +.layer-tab.minimized > * { margin: 0; writing-mode: vertical-rl; padding-left: 10px; diff --git a/src/game/layers.tsx b/src/game/layers.tsx index 404d72c..5435a2d 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -109,7 +109,7 @@ export interface LayerOptions { color?: Computable<string>; /** * The layout of this layer's features. - * When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is display. + * When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is displayed. */ display: Computable<CoercableComponent>; /** An object of classes that should be applied to the display. */ @@ -126,6 +126,11 @@ export interface LayerOptions { * Defaults to true. */ minimizable?: Computable<boolean>; + /** + * The layout of this layer's features. + * When the layer is open in {@link game/player.PlayerData.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed. + */ + minimizedDisplay?: Computable<CoercableComponent>; /** * Whether or not to force the go back button to be hidden. * If true, go back will be hidden regardless of {@link data/projInfo.allowGoBack}. @@ -170,6 +175,7 @@ export type Layer<T extends LayerOptions> = Replace< name: GetComputableTypeWithDefault<T["name"], string>; minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>; minimizable: GetComputableTypeWithDefault<T["minimizable"], true>; + minimizedDisplay: GetComputableType<T["minimizedDisplay"]>; forceHideGoBack: GetComputableType<T["forceHideGoBack"]>; } >; @@ -231,6 +237,7 @@ export function createLayer<T extends LayerOptions>( setDefault(layer, "minWidth", 600); processComputable(layer as T, "minimizable"); setDefault(layer, "minimizable", true); + processComputable(layer as T, "minimizedDisplay"); return layer as unknown as Layer<T>; }); From 832517d192975c9704d2b08bb8541a038f2da911 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Fri, 23 Dec 2022 23:34:49 -0600 Subject: [PATCH 51/56] Potential fix for some tab weirdness --- src/components/Game.vue | 5 ++-- src/components/Layer.vue | 55 +++++----------------------------------- src/game/layers.tsx | 32 ++++++++++++++++++++++- 3 files changed, 39 insertions(+), 53 deletions(-) diff --git a/src/components/Game.vue b/src/components/Game.vue index 9dd1e27..8fa3141 100644 --- a/src/components/Game.vue +++ b/src/components/Game.vue @@ -36,9 +36,8 @@ const layerKeys = computed(() => Object.keys(layers)); const useHeader = projInfo.useHeader; function gatherLayerProps(layer: GenericLayer) { - const { display, minimized, minWidth, name, color, minimizable, nodes, minimizedDisplay } = - layer; - return { display, minimized, minWidth, name, color, minimizable, nodes, minimizedDisplay }; + const { display, minimized, name, color, minimizable, nodes, minimizedDisplay } = layer; + return { display, minimized, name, color, minimizable, nodes, minimizedDisplay }; } </script> diff --git a/src/components/Layer.vue b/src/components/Layer.vue index abdf2bf..2d89ed2 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -1,7 +1,8 @@ <template> <div class="layer-container" :style="{ '--layer-color': unref(color) }"> - <button v-if="showGoBack" class="goBack" @click="goBack">←</button> - <button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false"> + <button v-if="showGoBack" class="goBack" @click="goBack">❌</button> + + <button class="layer-tab minimized" v-if="unref(minimized)" @click="setMinimized(false)"> <component v-if="minimizedComponent" :is="minimizedComponent" /> <div v-else>{{ unref(name) }}</div> </button> @@ -34,10 +35,6 @@ export default defineComponent({ type: Number, required: true }, - tab: { - type: Function as PropType<() => HTMLElement | undefined>, - required: true - }, display: { type: processedPropType<CoercableComponent>(Object, String, Function), required: true @@ -47,10 +44,6 @@ export default defineComponent({ type: Object as PropType<Persistent<boolean>>, required: true }, - minWidth: { - type: processedPropType<number | string>(Number, String), - required: true - }, name: { type: processedPropType<string>(String), required: true @@ -63,7 +56,7 @@ export default defineComponent({ } }, setup(props) { - const { display, index, minimized, minWidth, tab, minimizedDisplay } = toRefs(props); + const { display, index, minimized, minimizedDisplay } = toRefs(props); const component = computeComponent(display); const minimizedComponent = computeOptionalComponent(minimizedDisplay); @@ -75,60 +68,24 @@ export default defineComponent({ player.tabs.splice(unref(props.index), Infinity); } - nextTick(() => updateTab(minimized.value, unref(minWidth.value))); - watch([minimized, wrapRef(minWidth)], ([minimized, minWidth]) => - updateTab(minimized, minWidth) - ); - function updateNodes(nodes: Record<string, FeatureNode | undefined>) { props.nodes.value = nodes; } - function updateTab(minimized: boolean, minWidth: number | string) { - const width = - typeof minWidth === "number" || Number.isNaN(parseInt(minWidth)) - ? minWidth + "px" - : minWidth; - const tabValue = tab.value(); - if (tabValue != undefined) { - if (minimized) { - tabValue.style.flexGrow = "0"; - tabValue.style.flexShrink = "0"; - tabValue.style.width = "60px"; - tabValue.style.minWidth = tabValue.style.flexBasis = ""; - tabValue.style.margin = "0"; - } else { - tabValue.style.flexGrow = ""; - tabValue.style.flexShrink = ""; - tabValue.style.width = ""; - tabValue.style.minWidth = tabValue.style.flexBasis = width; - tabValue.style.margin = ""; - } - } - } - return { component, minimizedComponent, showGoBack, updateNodes, unref, - goBack + goBack, + setMinimized }; } }); </script> <style scoped> -.layer-container { - min-width: 100%; - min-height: 100%; - margin: 0; - flex-grow: 1; - display: flex; - isolation: isolate; -} - .layer-tab:not(.minimized) { padding-top: 20px; padding-bottom: 20px; diff --git a/src/game/layers.tsx b/src/game/layers.tsx index 5435a2d..c689235 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -21,7 +21,7 @@ import type { } from "util/computed"; import { processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; -import type { InjectionKey, Ref } from "vue"; +import { computed, InjectionKey, Ref } from "vue"; import { ref, shallowReactive, unref } from "vue"; /** A feature's node in the DOM that has its size tracked. */ @@ -231,6 +231,8 @@ export function createLayer<T extends LayerOptions>( processComputable(layer as T, "color"); processComputable(layer as T, "display"); + processComputable(layer as T, "classes"); + processComputable(layer as T, "style"); processComputable(layer as T, "name"); setDefault(layer, "name", layer.id); processComputable(layer as T, "minWidth"); @@ -239,6 +241,34 @@ export function createLayer<T extends LayerOptions>( setDefault(layer, "minimizable", true); processComputable(layer as T, "minimizedDisplay"); + const style = layer.style as ProcessedComputable<StyleValue> | undefined; + layer.style = computed(() => { + let width = unref(layer.minWidth as ProcessedComputable<number | string>); + if (typeof width === "number" || !Number.isNaN(parseInt(width))) { + width = width + "px"; + } + return [ + unref(style) ?? "", + layer.minimized?.value + ? { + flexGrow: "0", + flexShrink: "0", + width: "60px", + minWidth: "", + flexBasis: "", + margin: "0" + } + : { + flexGrow: "", + flexShrink: "", + width: "", + minWidth: width, + flexBasis: width, + margin: "" + } + ]; + }) as Ref<StyleValue>; + return layer as unknown as Layer<T>; }); } From cd36549cbefe45ed6a73850069e7e6198ce5e77a Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 24 Dec 2022 08:38:36 -0600 Subject: [PATCH 52/56] Don't set values on ref prop --- src/components/Game.vue | 3 +-- src/components/Layer.vue | 15 ++++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Game.vue b/src/components/Game.vue index 8fa3141..680273a 100644 --- a/src/components/Game.vue +++ b/src/components/Game.vue @@ -4,7 +4,6 @@ v-for="(tab, index) in tabs" :key="index" class="tab" - :ref="`tab-${index}`" :style="unref(layers[tab]?.style)" :class="unref(layers[tab]?.classes)" > @@ -14,7 +13,7 @@ v-if="layerKeys.includes(tab)" v-bind="gatherLayerProps(layers[tab]!)" :index="index" - :tab="() => (($refs[`tab-${index}`] as HTMLElement[] | undefined)?.[0])" + @set-minimized="value => (layers[tab]!.minimized.value = value)" /> <component :is="tab" :index="index" v-else /> </div> diff --git a/src/components/Layer.vue b/src/components/Layer.vue index 2d89ed2..fd7c4e4 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -2,7 +2,11 @@ <div class="layer-container" :style="{ '--layer-color': unref(color) }"> <button v-if="showGoBack" class="goBack" @click="goBack">❌</button> - <button class="layer-tab minimized" v-if="unref(minimized)" @click="setMinimized(false)"> + <button + class="layer-tab minimized" + v-if="unref(minimized)" + @click="$emit('setMinimized', false)" + > <component v-if="minimizedComponent" :is="minimizedComponent" /> <div v-else>{{ unref(name) }}</div> </button> @@ -11,7 +15,8 @@ <component :is="component" /> </Context> </div> - <button v-if="unref(minimizable)" class="minimize" @click="minimized.value = true"> + + <button v-if="unref(minimizable)" class="minimize" @click="$emit('setMinimized', true)"> ▼ </button> </div> @@ -41,7 +46,7 @@ export default defineComponent({ }, minimizedDisplay: processedPropType<CoercableComponent>(Object, String, Function), minimized: { - type: Object as PropType<Persistent<boolean>>, + type: Object as PropType<Ref<boolean>>, required: true }, name: { @@ -55,6 +60,7 @@ export default defineComponent({ required: true } }, + emits: ["setMinimized"], setup(props) { const { display, index, minimized, minimizedDisplay } = toRefs(props); @@ -78,8 +84,7 @@ export default defineComponent({ showGoBack, updateNodes, unref, - goBack, - setMinimized + goBack }; } }); From aa1acf8d70c58e1d435c3fc3a96584a0997c0cb1 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Sat, 24 Dec 2022 10:24:14 -0600 Subject: [PATCH 53/56] Fixed go back button sometimes appearing erroneously --- src/components/Layer.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Layer.vue b/src/components/Layer.vue index fd7c4e4..f4f11b8 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -26,11 +26,10 @@ import projInfo from "data/projInfo.json"; import type { CoercableComponent } from "features/feature"; import type { FeatureNode } from "game/layers"; -import type { Persistent } from "game/persistence"; import player from "game/player"; -import { computeComponent, computeOptionalComponent, processedPropType, wrapRef } from "util/vue"; +import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue"; import type { PropType, Ref } from "vue"; -import { computed, defineComponent, nextTick, toRefs, unref, watch } from "vue"; +import { computed, defineComponent, toRefs, unref } from "vue"; import Context from "./Context.vue"; export default defineComponent({ @@ -67,7 +66,7 @@ export default defineComponent({ const component = computeComponent(display); const minimizedComponent = computeOptionalComponent(minimizedDisplay); const showGoBack = computed( - () => projInfo.allowGoBack && index.value > 0 && !minimized.value + () => projInfo.allowGoBack && index.value > 0 && !unwrapRef(minimized) ); function goBack() { From e6dd4bf33269906fa48c179441c326867ad1e705 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 22:43:44 -0600 Subject: [PATCH 54/56] Fix some Layer styling --- src/components/Layer.vue | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/Layer.vue b/src/components/Layer.vue index f4f11b8..a3fffda 100644 --- a/src/components/Layer.vue +++ b/src/components/Layer.vue @@ -73,6 +73,10 @@ export default defineComponent({ player.tabs.splice(unref(props.index), Infinity); } + function setMinimized(min: boolean) { + minimized.value = min; + } + function updateNodes(nodes: Record<string, FeatureNode | undefined>) { props.nodes.value = nodes; } @@ -90,6 +94,15 @@ export default defineComponent({ </script> <style scoped> +.layer-container { + min-width: 100%; + min-height: 100%; + margin: 0; + flex-grow: 1; + display: flex; + isolation: isolate; +} + .layer-tab:not(.minimized) { padding-top: 20px; padding-bottom: 20px; @@ -123,6 +136,7 @@ export default defineComponent({ .layer-tab.minimized > * { margin: 0; writing-mode: vertical-rl; + text-align: left; padding-left: 10px; width: 50px; } @@ -166,8 +180,8 @@ export default defineComponent({ .goBack { position: sticky; - top: 6px; - left: 20px; + top: 10px; + left: 10px; line-height: 30px; margin-top: -50px; margin-left: -35px; @@ -176,7 +190,7 @@ export default defineComponent({ box-shadow: var(--background) 0 2px 3px 5px; border-radius: 50%; color: var(--foreground); - font-size: 40px; + font-size: 30px; cursor: pointer; z-index: 7; } @@ -186,3 +200,10 @@ export default defineComponent({ text-shadow: 0 0 7px var(--foreground); } </style> + +<style> +.layer-tab.minimized > * > .desc { + color: var(--accent1); + font-size: 30px; +} +</style> From fb02699cb00ab8b07f7a80d9d2e4642aed1e7ba8 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Wed, 15 Feb 2023 23:15:55 -0600 Subject: [PATCH 55/56] Fix tests not running --- src/game/events.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/game/events.ts b/src/game/events.ts index cf7ae65..a167042 100644 --- a/src/game/events.ts +++ b/src/game/events.ts @@ -54,4 +54,8 @@ export interface GlobalEvents { /** A global event bus for hooking into {@link GlobalEvents}. */ export const globalBus = createNanoEvents<GlobalEvents>(); -document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded"); +if ("fonts" in document) { + // This line breaks tests + // JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document + document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded"); +} From 922b138a5bc62c838d1f60dcbd6beb6f3752bf84 Mon Sep 17 00:00:00 2001 From: thepaperpilot <thepaperpilot@gmail.com> Date: Thu, 16 Feb 2023 20:30:39 -0600 Subject: [PATCH 56/56] Fix test --- tests/game/requirements.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts index fa9ae5e..57e574b 100644 --- a/tests/game/requirements.test.ts +++ b/tests/game/requirements.test.ts @@ -103,7 +103,7 @@ describe("Creating visibility requirement", () => { test("Requirement not met when not visible", () => { let requirement = createVisibilityRequirement({ visibility: Visibility.None }); expect(unref(requirement.requirementMet)).toBe(false); - requirement = createVisibilityRequirement({ visibility: Visibility.Hidden }); + requirement = createVisibilityRequirement({ visibility: false }); expect(unref(requirement.requirementMet)).toBe(false); }); });