diff --git a/src/App.vue b/src/App.vue index 5c8b783..2605d13 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,7 @@ @@ -22,9 +24,8 @@ import AddictionWarning from "components/modals/AddictionWarning.vue"; import CloudSaveResolver from "components/modals/CloudSaveResolver.vue"; import GameOverScreen from "components/modals/GameOverScreen.vue"; import NaNScreen from "components/modals/NaNScreen.vue"; -import { jsx } from "features/feature"; import state from "game/state"; -import { coerceComponent, render } from "util/vue"; +import { render } from "util/vue"; import type { CSSProperties } from "vue"; import { computed, toRef, unref } from "vue"; import Game from "./components/Game.vue"; @@ -40,9 +41,7 @@ const theme = computed(() => themes[settings.theme].variables as CSSProperties); const showTPS = toRef(settings, "showTPS"); const appErrors = toRef(state, "errors"); -const gameComponent = computed(() => { - return coerceComponent(jsx(() => (<>{gameComponents.map(render)}))); -}); +const GameComponent = () => gameComponents.map(c => render(c)); diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts deleted file mode 100644 index 1148cf0..0000000 --- a/src/features/trees/tree.ts +++ /dev/null @@ -1,387 +0,0 @@ -import { GenericDecorator } from "features/decorators/common"; -import type { - CoercableComponent, - GenericComponent, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; -import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature"; -import type { Link } from "features/links/links"; -import type { GenericReset } from "features/reset"; -import type { Resource } from "features/resources/resource"; -import { displayResource } from "features/resources/resource"; -import TreeComponent from "features/trees/Tree.vue"; -import TreeNodeComponent from "features/trees/TreeNode.vue"; -import type { DecimalSource } from "util/bignum"; -import Decimal, { format, formatWhole } from "util/bignum"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { convertComputable, processComputable } from "util/computed"; -import { createLazyProxy } from "util/proxies"; -import type { Ref } from "vue"; -import { computed, ref, shallowRef, unref } from "vue"; - -/** A symbol used to identify {@link TreeNode} features. */ -export const TreeNodeType = Symbol("TreeNode"); -/** A symbol used to identify {@link Tree} features. */ -export const TreeType = Symbol("Tree"); - -/** - * An object that configures a {@link TreeNode}. - */ -export interface TreeNodeOptions { - /** Whether this tree node should be visible. */ - visibility?: Computable; - /** Whether or not this tree node can be clicked. */ - canClick?: Computable; - /** The background color for this node. */ - color?: Computable; - /** The label to display on this tree node. */ - display?: Computable; - /** The color of the glow effect shown to notify the user there's something to do with this node. */ - glowColor?: Computable; - /** Dictionary of CSS classes to apply to this feature. */ - classes?: Computable>; - /** CSS to apply to this feature. */ - style?: Computable; - /** Shows a marker on the corner of the feature. */ - mark?: Computable; - /** A reset object attached to this node, used for propagating resets through the tree. */ - reset?: GenericReset; - /** A function that is called when the tree node is clicked. */ - onClick?: (e?: MouseEvent | TouchEvent) => void; - /** A function that is called when the tree node is held down. */ - onHold?: VoidFunction; -} - -/** - * The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}. - */ -export interface BaseTreeNode { - /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ - id: string; - /** A symbol that helps identify features of the same type. */ - type: typeof TreeNodeType; - /** The Vue component used to render this feature. */ - [Component]: GenericComponent; - /** A function to gather the props the vue component requires for this feature. */ - [GatherProps]: () => Record; -} - -/** An object that represents a node on a tree. */ -export type TreeNode = Replace< - T & BaseTreeNode, - { - visibility: GetComputableTypeWithDefault; - canClick: GetComputableTypeWithDefault; - color: GetComputableType; - display: GetComputableType; - glowColor: GetComputableType; - classes: GetComputableType; - style: GetComputableType; - mark: GetComputableType; - } ->; - -/** A type that matches any valid {@link TreeNode} object. */ -export type GenericTreeNode = Replace< - TreeNode, - { - visibility: ProcessedComputable; - canClick: ProcessedComputable; - } ->; - -/** - * Lazily creates a tree node with the given options. - * @param optionsFunc Tree Node options. - */ -export function createTreeNode( - optionsFunc?: OptionsFunc, - ...decorators: GenericDecorator[] -): TreeNode { - const decoratedData = decorators.reduce( - (current, next) => Object.assign(current, next.getPersistentData?.()), - {} - ); - return createLazyProxy(feature => { - const treeNode = - optionsFunc?.call(feature, feature) ?? - ({} as ReturnType>); - treeNode.id = getUniqueID("treeNode-"); - treeNode.type = TreeNodeType; - treeNode[Component] = TreeNodeComponent as GenericComponent; - - for (const decorator of decorators) { - decorator.preConstruct?.(treeNode); - } - - Object.assign(decoratedData); - - processComputable(treeNode as T, "visibility"); - setDefault(treeNode, "visibility", Visibility.Visible); - processComputable(treeNode as T, "canClick"); - setDefault(treeNode, "canClick", true); - processComputable(treeNode as T, "color"); - processComputable(treeNode as T, "display"); - processComputable(treeNode as T, "glowColor"); - processComputable(treeNode as T, "classes"); - processComputable(treeNode as T, "style"); - processComputable(treeNode as T, "mark"); - - for (const decorator of decorators) { - decorator.postConstruct?.(treeNode); - } - - if (treeNode.onClick) { - const onClick = treeNode.onClick.bind(treeNode); - treeNode.onClick = function (e) { - if ( - unref(treeNode.canClick as ProcessedComputable) !== false - ) { - onClick(e); - } - }; - } - if (treeNode.onHold) { - const onHold = treeNode.onHold.bind(treeNode); - treeNode.onHold = function () { - if ( - unref(treeNode.canClick as ProcessedComputable) !== false - ) { - onHold(); - } - }; - } - - const decoratedProps = decorators.reduce( - (current, next) => Object.assign(current, next.getGatheredProps?.(treeNode)), - {} - ); - treeNode[GatherProps] = function (this: GenericTreeNode) { - const { - display, - visibility, - style, - classes, - onClick, - onHold, - color, - glowColor, - canClick, - mark, - id - } = this; - return { - display, - visibility, - style, - classes, - onClick, - onHold, - color, - glowColor, - canClick, - mark, - id, - ...decoratedProps - }; - }; - - return treeNode as unknown as TreeNode; - }); -} - -/** Represents a branch between two nodes in a tree. */ -export interface TreeBranch extends Omit { - startNode: GenericTreeNode; - endNode: GenericTreeNode; -} - -/** - * An object that configures a {@link Tree}. - */ -export interface TreeOptions { - /** Whether this clickable should be visible. */ - visibility?: Computable; - /** The nodes within the tree, in a 2D array. */ - nodes: Computable; - /** Nodes to show on the left side of the tree. */ - leftSideNodes?: Computable; - /** Nodes to show on the right side of the tree. */ - rightSideNodes?: Computable; - /** The branches between nodes within this tree. */ - branches?: Computable; - /** How to propagate resets through the tree. */ - resetPropagation?: ResetPropagation; - /** A function that is called when a node within the tree is reset. */ - onReset?: (node: GenericTreeNode) => void; -} - -export interface BaseTree { - /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ - id: string; - /** The link objects for each of the branches of the tree. */ - links: Ref; - /** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */ - reset: (node: GenericTreeNode) => void; - /** A flag that is true while the reset is still propagating through the tree. */ - isResetting: Ref; - /** A reference to the node that caused the currently propagating reset. */ - resettingNode: Ref; - /** A symbol that helps identify features of the same type. */ - type: typeof TreeType; - /** The Vue component used to render this feature. */ - [Component]: GenericComponent; - /** A function to gather the props the vue component requires for this feature. */ - [GatherProps]: () => Record; -} - -/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */ -export type Tree = Replace< - T & BaseTree, - { - visibility: GetComputableTypeWithDefault; - nodes: GetComputableType; - leftSideNodes: GetComputableType; - rightSideNodes: GetComputableType; - branches: GetComputableType; - } ->; - -/** A type that matches any valid {@link Tree} object. */ -export type GenericTree = Replace< - Tree, - { - visibility: ProcessedComputable; - } ->; - -/** - * Lazily creates a tree with the given options. - * @param optionsFunc Tree options. - */ -export function createTree( - optionsFunc: OptionsFunc -): Tree { - return createLazyProxy(feature => { - const tree = optionsFunc.call(feature, feature); - tree.id = getUniqueID("tree-"); - tree.type = TreeType; - tree[Component] = TreeComponent as GenericComponent; - - tree.isResetting = ref(false); - tree.resettingNode = shallowRef(null); - - tree.reset = function (node) { - const genericTree = tree as GenericTree; - genericTree.isResetting.value = true; - genericTree.resettingNode.value = node; - genericTree.resetPropagation?.(genericTree, node); - genericTree.onReset?.(node); - genericTree.isResetting.value = false; - genericTree.resettingNode.value = null; - }; - tree.links = computed(() => { - const genericTree = tree as GenericTree; - return unref(genericTree.branches) ?? []; - }); - - processComputable(tree as T, "visibility"); - setDefault(tree, "visibility", Visibility.Visible); - processComputable(tree as T, "nodes"); - processComputable(tree as T, "leftSideNodes"); - processComputable(tree as T, "rightSideNodes"); - processComputable(tree as T, "branches"); - - tree[GatherProps] = function (this: GenericTree) { - const { nodes, leftSideNodes, rightSideNodes, branches } = this; - return { nodes, leftSideNodes, rightSideNodes, branches }; - }; - - return tree as unknown as Tree; - }); -} - -/** A function that is used to propagate resets through a tree. */ -export type ResetPropagation = { - (tree: GenericTree, resettingNode: GenericTreeNode): void; -}; - -/** Propagate resets down the tree by resetting every node in a lower row. */ -export const defaultResetPropagation = function ( - tree: GenericTree, - resettingNode: GenericTreeNode -): void { - const nodes = unref(tree.nodes); - const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) - 1; - for (let x = row; x >= 0; x--) { - nodes[x].forEach(node => node.reset?.reset()); - } -}; - -/** Propagate resets down the tree by resetting every node in a lower row. */ -export const invertedResetPropagation = function ( - tree: GenericTree, - resettingNode: GenericTreeNode -): void { - const nodes = unref(tree.nodes); - const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) + 1; - for (let x = row; x < nodes.length; x++) { - nodes[x].forEach(node => node.reset?.reset()); - } -}; - -/** Propagate resets down the branches of the tree. */ -export const branchedResetPropagation = function ( - tree: GenericTree, - resettingNode: GenericTreeNode -): void { - const links = unref(tree.branches); - if (links == null) return; - const reset: GenericTreeNode[] = []; - let current = [resettingNode]; - while (current.length !== 0) { - const next: GenericTreeNode[] = []; - for (const node of current) { - for (const link of links.filter(link => link.startNode === node)) { - if ([...reset, ...current].includes(link.endNode)) continue; - next.push(link.endNode); - link.endNode.reset?.reset(); - } - } - reset.push(...current); - current = next; - } -}; - -/** - * Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource. - * It sounds oddly specific, but comes up a lot. - */ -export function createResourceTooltip( - resource: Resource, - requiredResource: Resource | null = null, - requirement: Computable = 0 -): Ref { - const req = convertComputable(requirement); - return computed(() => { - if (requiredResource == null || Decimal.gte(resource.value, unref(req))) { - return displayResource(resource) + " " + resource.displayName; - } - return `Reach ${ - Decimal.eq(requiredResource.precision, 0) - ? formatWhole(unref(req)) - : format(unref(req), requiredResource.precision) - } ${requiredResource.displayName} to unlock (You have ${ - Decimal.eq(requiredResource.precision, 0) - ? formatWhole(requiredResource.value) - : format(requiredResource.value, requiredResource.precision) - })`; - }); -} diff --git a/src/features/trees/tree.tsx b/src/features/trees/tree.tsx new file mode 100644 index 0000000..7d2ded6 --- /dev/null +++ b/src/features/trees/tree.tsx @@ -0,0 +1,279 @@ +import type { OptionsFunc, Replace } from "features/feature"; +import { Link } from "features/links/links"; +import type { Reset } from "features/reset"; +import type { Resource } from "features/resources/resource"; +import { displayResource } from "features/resources/resource"; +import Tree from "features/trees/Tree.vue"; +import TreeNode from "features/trees/TreeNode.vue"; +import type { DecimalSource } from "util/bignum"; +import Decimal, { format, formatWhole } from "util/bignum"; +import { ProcessedRefOrGetter, processGetter } from "util/computed"; +import { createLazyProxy } from "util/proxies"; +import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; +import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue"; +import { computed, ref, shallowRef, unref } from "vue"; + +/** A symbol used to identify {@link TreeNode} features. */ +export const TreeNodeType = Symbol("TreeNode"); +/** A symbol used to identify {@link Tree} features. */ +export const TreeType = Symbol("Tree"); + +/** + * An object that configures a {@link TreeNode}. + */ +export interface TreeNodeOptions extends VueFeatureOptions { + /** Whether or not this tree node can be clicked. */ + canClick?: MaybeRefOrGetter; + /** The background color for this node. */ + color?: MaybeRefOrGetter; + /** The label to display on this tree node. */ + display?: MaybeRefOrGetter; + /** The color of the glow effect shown to notify the user there's something to do with this node. */ + glowColor?: MaybeRefOrGetter; + /** A reset object attached to this node, used for propagating resets through the tree. */ + reset?: Reset; + /** A function that is called when the tree node is clicked. */ + onClick?: (e?: MouseEvent | TouchEvent) => void; + /** A function that is called when the tree node is held down. */ + onHold?: VoidFunction; +} + +/** + * The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}. + */ +export interface BaseTreeNode extends VueFeature { + /** A symbol that helps identify features of the same type. */ + type: typeof TreeNodeType; +} + +/** An object that represents a node on a tree. */ +export type TreeNode = Replace< + TreeNodeOptions & BaseTreeNode, + { + canClick: MaybeRef; + color: ProcessedRefOrGetter; + display: ProcessedRefOrGetter; + glowColor: ProcessedRefOrGetter; + } +>; + +/** + * Lazily creates a tree node with the given options. + * @param optionsFunc Tree Node options. + */ +export function createTreeNode( + optionsFunc?: OptionsFunc +) { + return createLazyProxy(feature => { + const options = optionsFunc?.call(feature, feature as TreeNode) ?? ({} as T); + const { canClick, color, display, glowColor, onClick, onHold, ...props } = options; + + const treeNode = { + type: TreeNodeType, + ...(props as Omit), + ...vueFeatureMixin("treeNode", options, () => ( + + )), + canClick: processGetter(canClick) ?? true, + color: processGetter(color), + display: processGetter(display), + glowColor: processGetter(glowColor), + onClick: + onClick == null + ? undefined + : function (e) { + if (unref(treeNode.canClick) !== false) { + onClick.call(treeNode, e); + } + }, + onHold: + onHold == null + ? undefined + : function () { + if (unref(treeNode.canClick) !== false) { + onHold.call(treeNode); + } + } + } satisfies TreeNode; + + return treeNode; + }); +} + +/** Represents a branch between two nodes in a tree. */ +export interface TreeBranch extends Omit { + startNode: TreeNode; + endNode: TreeNode; +} + +/** + * An object that configures a {@link Tree}. + */ +export interface TreeOptions extends VueFeatureOptions { + /** The nodes within the tree, in a 2D array. */ + nodes: MaybeRefOrGetter; + /** Nodes to show on the left side of the tree. */ + leftSideNodes?: MaybeRefOrGetter; + /** Nodes to show on the right side of the tree. */ + rightSideNodes?: MaybeRefOrGetter; + /** The branches between nodes within this tree. */ + branches?: MaybeRefOrGetter; + /** How to propagate resets through the tree. */ + resetPropagation?: ResetPropagation; + /** A function that is called when a node within the tree is reset. */ + onReset?: (node: TreeNode) => void; +} + +export interface BaseTree extends VueFeature { + /** The link objects for each of the branches of the tree. */ + links: Ref; + /** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */ + reset: (node: TreeNode) => void; + /** A flag that is true while the reset is still propagating through the tree. */ + isResetting: Ref; + /** A reference to the node that caused the currently propagating reset. */ + resettingNode: Ref; + /** A symbol that helps identify features of the same type. */ + type: typeof TreeType; +} + +/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */ +export type Tree = Replace< + TreeOptions & BaseTree, + { + nodes: ProcessedRefOrGetter; + leftSideNodes: ProcessedRefOrGetter; + rightSideNodes: ProcessedRefOrGetter; + branches: ProcessedRefOrGetter; + } +>; + +/** + * Lazily creates a tree with the given options. + * @param optionsFunc Tree options. + */ +export function createTree(optionsFunc: OptionsFunc) { + return createLazyProxy(feature => { + const options = optionsFunc.call(feature, feature as Tree); + const { + branches, + nodes, + leftSideNodes, + rightSideNodes, + reset, + resetPropagation, + onReset, + ...props + } = options; + + const tree = { + type: TreeType, + ...(props as Omit), + ...vueFeatureMixin("tree", options, () => ( + + )), + branches: processGetter(branches), + isResetting: ref(false), + resettingNode: shallowRef(null), + nodes: processGetter(nodes), + leftSideNodes: processGetter(leftSideNodes), + rightSideNodes: processGetter(rightSideNodes), + links: processGetter(branches) ?? [], + resetPropagation, + onReset, + reset: + reset ?? + function (node: TreeNode) { + tree.isResetting.value = true; + tree.resettingNode.value = node; + tree.resetPropagation?.(tree, node); + tree.onReset?.(node); + tree.isResetting.value = false; + tree.resettingNode.value = null; + } + } satisfies Tree; + + return tree; + }); +} + +/** A function that is used to propagate resets through a tree. */ +export type ResetPropagation = { + (tree: Tree, resettingNode: TreeNode): void; +}; + +/** Propagate resets down the tree by resetting every node in a lower row. */ +export const defaultResetPropagation = function (tree: Tree, resettingNode: TreeNode): void { + const nodes = unref(tree.nodes); + const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) - 1; + for (let x = row; x >= 0; x--) { + nodes[x].forEach(node => node.reset?.reset()); + } +}; + +/** Propagate resets down the tree by resetting every node in a lower row. */ +export const invertedResetPropagation = function (tree: Tree, resettingNode: TreeNode): void { + const nodes = unref(tree.nodes); + const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) + 1; + for (let x = row; x < nodes.length; x++) { + nodes[x].forEach(node => node.reset?.reset()); + } +}; + +/** Propagate resets down the branches of the tree. */ +export const branchedResetPropagation = function (tree: Tree, resettingNode: TreeNode): void { + const links = unref(tree.branches); + if (links == null) return; + const reset: TreeNode[] = []; + let current = [resettingNode]; + while (current.length !== 0) { + const next: TreeNode[] = []; + for (const node of current) { + for (const link of links.filter(link => link.startNode === node)) { + if ([...reset, ...current].includes(link.endNode)) continue; + next.push(link.endNode); + link.endNode.reset?.reset(); + } + } + reset.push(...current); + current = next; + } +}; + +/** + * Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource. + * It sounds oddly specific, but comes up a lot. + */ +export function createResourceTooltip( + resource: Resource, + requiredResource: Resource | null = null, + requirement: MaybeRefOrGetter = 0 +): Ref { + const req = processGetter(requirement); + return computed(() => { + if (requiredResource == null || Decimal.gte(resource.value, unref(req))) { + return displayResource(resource) + " " + resource.displayName; + } + return `Reach ${ + Decimal.eq(requiredResource.precision, 0) + ? formatWhole(unref(req)) + : format(unref(req), requiredResource.precision) + } ${requiredResource.displayName} to unlock (You have ${ + Decimal.eq(requiredResource.precision, 0) + ? formatWhole(requiredResource.value) + : format(requiredResource.value, requiredResource.precision) + })`; + }); +} diff --git a/src/features/upgrades/Upgrade.vue b/src/features/upgrades/Upgrade.vue deleted file mode 100644 index b2e3b5e..0000000 --- a/src/features/upgrades/Upgrade.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts deleted file mode 100644 index a3075e7..0000000 --- a/src/features/upgrades/upgrade.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { GenericDecorator } from "features/decorators/common"; -import type { - CoercableComponent, - GenericComponent, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; -import { - Component, - GatherProps, - Visibility, - findFeatures, - getUniqueID, - setDefault -} from "features/feature"; -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 { - Requirements, - createVisibilityRequirement, - payRequirements, - requirementsMet -} from "game/requirements"; -import { isFunction } from "util/common"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { processComputable } from "util/computed"; -import { createLazyProxy } from "util/proxies"; -import type { Ref } from "vue"; -import { computed, unref } from "vue"; - -/** A symbol used to identify {@link Upgrade} features. */ -export const UpgradeType = Symbol("Upgrade"); - -/** - * An object that configures a {@link Upgrade}. - */ -export interface UpgradeOptions { - /** Whether this clickable should be visible. */ - visibility?: Computable; - /** Dictionary of CSS classes to apply to this feature. */ - classes?: Computable>; - /** CSS to apply to this feature. */ - style?: Computable; - /** Shows a marker on the corner of the feature. */ - mark?: Computable; - /** The display to use for this clickable. */ - display?: Computable< - | 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 the achievement. Useful when the effect changes dynamically. */ - effectDisplay?: CoercableComponent; - } - >; - /** The requirements to purchase this upgrade. */ - requirements: Requirements; - /** A function that is called when the upgrade is purchased. */ - onPurchase?: VoidFunction; -} - -/** - * The properties that are added onto a processed {@link UpgradeOptions} to create an {@link Upgrade}. - */ -export interface BaseUpgrade { - /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ - id: string; - /** Whether or not this upgrade has been purchased. */ - bought: Persistent; - /** Whether or not the upgrade can currently be purchased. */ - canPurchase: Ref; - /** Purchase the upgrade */ - purchase: VoidFunction; - /** A symbol that helps identify features of the same type. */ - type: typeof UpgradeType; - /** The Vue component used to render this feature. */ - [Component]: GenericComponent; - /** A function to gather the props the vue component requires for this feature. */ - [GatherProps]: () => Record; -} - -/** An object that represents a feature that can be purchased a single time. */ -export type Upgrade = Replace< - T & BaseUpgrade, - { - visibility: GetComputableTypeWithDefault; - classes: GetComputableType; - style: GetComputableType; - display: GetComputableType; - requirements: GetComputableType; - mark: GetComputableType; - } ->; - -/** A type that matches any valid {@link Upgrade} object. */ -export type GenericUpgrade = Replace< - Upgrade, - { - visibility: ProcessedComputable; - } ->; - -/** - * Lazily creates an upgrade with the given options. - * @param optionsFunc Upgrade options. - */ -export function createUpgrade( - optionsFunc: OptionsFunc, - ...decorators: GenericDecorator[] -): Upgrade { - const bought = persistent(false, false); - const decoratedData = decorators.reduce( - (current, next) => Object.assign(current, next.getPersistentData?.()), - {} - ); - return createLazyProxy(feature => { - const upgrade = optionsFunc.call(feature, feature); - upgrade.id = getUniqueID("upgrade-"); - upgrade.type = UpgradeType; - upgrade[Component] = UpgradeComponent as GenericComponent; - - for (const decorator of decorators) { - decorator.preConstruct?.(upgrade); - } - - upgrade.bought = bought; - Object.assign(upgrade, decoratedData); - - upgrade.canPurchase = computed( - () => !bought.value && requirementsMet(upgrade.requirements) - ); - upgrade.purchase = function () { - const genericUpgrade = upgrade as GenericUpgrade; - if (!unref(genericUpgrade.canPurchase)) { - return; - } - payRequirements(upgrade.requirements); - bought.value = true; - genericUpgrade.onPurchase?.(); - }; - - const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade); - if (Array.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"); - - for (const decorator of decorators) { - decorator.postConstruct?.(upgrade); - } - - const decoratedProps = decorators.reduce( - (current, next) => Object.assign(current, next.getGatheredProps?.(upgrade)), - {} - ); - upgrade[GatherProps] = function (this: GenericUpgrade) { - const { - display, - visibility, - style, - classes, - requirements, - canPurchase, - bought, - mark, - id, - purchase - } = this; - return { - display, - visibility, - style: unref(style), - classes, - requirements, - canPurchase, - bought, - mark, - id, - purchase, - ...decoratedProps - }; - }; - - return upgrade as unknown as Upgrade; - }); -} - -/** - * Utility to auto purchase a list of upgrades whenever they're affordable. - * @param layer The layer the upgrades are apart of - * @param autoActive Whether or not the upgrades should currently be auto-purchasing - * @param upgrades The specific upgrades to upgrade. If unspecified, uses all upgrades on the layer. - */ -export function setupAutoPurchase( - layer: GenericLayer, - autoActive: Computable, - upgrades: GenericUpgrade[] = [] -): void { - upgrades = - upgrades.length === 0 ? (findFeatures(layer, UpgradeType) as GenericUpgrade[]) : upgrades; - const isAutoActive: ProcessedComputable = isFunction(autoActive) - ? computed(autoActive) - : autoActive; - layer.on("update", () => { - if (unref(isAutoActive)) { - upgrades.forEach(upgrade => upgrade.purchase()); - } - }); -} diff --git a/src/game/boards/Draggable.vue b/src/game/boards/Draggable.vue index b181b8b..9b4a2fa 100644 --- a/src/game/boards/Draggable.vue +++ b/src/game/boards/Draggable.vue @@ -7,23 +7,17 @@ @mouseup="e => mouseUp(e)" @touchend.passive="e => mouseUp(e)" > - + diff --git a/src/game/boards/board.tsx b/src/game/boards/board.tsx index b877d92..a0b90ac 100644 --- a/src/game/boards/board.tsx +++ b/src/game/boards/board.tsx @@ -1,13 +1,11 @@ -import { Component, GatherProps, GenericComponent, jsx } from "features/feature"; import { globalBus } from "game/events"; import { Persistent, persistent } from "game/persistence"; import type { PanZoom } from "panzoom"; import { Direction, isFunction } from "util/common"; -import type { Computable, ProcessedComputable } from "util/computed"; -import { convertComputable } from "util/computed"; +import { processGetter } from "util/computed"; import { VueFeature } from "util/vue"; -import type { ComponentPublicInstance, Ref } from "vue"; -import { computed, nextTick, ref, unref, watchEffect } from "vue"; +import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter, Ref } from "vue"; +import { computed, ref, unref, watchEffect } from "vue"; import panZoom from "vue-panzoom"; import { JSX } from "vue/jsx-runtime"; import Board from "./Board.vue"; @@ -20,10 +18,10 @@ globalBus.on("setupVue", app => panZoom.install(app)); export type NodePosition = { x: number; y: number }; /** - * A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board. + * A type representing a MaybeRefOrGetter value for a node on the board. Used for node types to return different values based on the given node and the state of the board. */ -export type NodeComputable = - | Computable +export type NodeMaybeRefOrGetter = + | MaybeRefOrGetter | ((node: T, ...args: S) => R); /** @@ -32,11 +30,11 @@ export type NodeComputable = * @param node The node to get the property of */ export function unwrapNodeRef( - property: NodeComputable, + property: NodeMaybeRefOrGetter, node: T, ...args: S ): R { - return isFunction>(property) + return isFunction>(property) ? property(node, ...args) : unref(property); } @@ -46,8 +44,8 @@ export function unwrapNodeRef( * @param nodes The list of current nodes with IDs as properties * @returns A computed ref that will give the value of the next unique ID */ -export function setupUniqueIds(nodes: Computable<{ id: number }[]>) { - const processedNodes = convertComputable(nodes); +export function setupUniqueIds(nodes: MaybeRefOrGetter<{ id: number }[]>) { + const processedNodes = processGetter(nodes); return computed(() => Math.max(-1, ...unref(processedNodes).map(node => node.id)) + 1); } @@ -60,9 +58,9 @@ export interface DraggableNodeOptions { /** Setter function to update the position of a node. */ setPosition: (node: T, position: NodePosition) => void; /** A list of nodes that the currently dragged node can be dropped upon. */ - receivingNodes?: NodeComputable; + receivingNodes?: NodeMaybeRefOrGetter; /** The maximum distance (in pixels, before zoom) away a node can be and still drop onto a receiving node. */ - dropAreaRadius?: NodeComputable; + dropAreaRadius?: NodeMaybeRefOrGetter; /** A callback for when a node gets dropped upon a receiving node. */ onDrop?: (acceptingNode: T, draggingNode: T) => void; } @@ -262,12 +260,12 @@ export interface MakeDraggableOptions { * @param element The vue feature to make draggable. * @param options The options to configure the dragging behavior. */ -export function makeDraggable( - element: T, - options: MakeDraggableOptions -): asserts element is T & { position: Persistent } { +export function makeDraggable( + element: VueFeature, + options: MakeDraggableOptions +): asserts element is VueFeature & { position: Persistent } { const position = persistent(options.initialPosition ?? { x: 0, y: 0 }); - (element as T & { position: Persistent }).position = position; + (element as VueFeature & { position: Persistent }).position = position; const computedPosition = computed(() => { if (options.nodeBeingDragged.value === options.id) { return { @@ -292,36 +290,25 @@ export function makeDraggable( options.onMouseUp?.(e); } - nextTick(() => { - const elementComponent = element[Component]; - const elementGatherProps = element[GatherProps].bind(element); - element[Component] = Draggable as GenericComponent; - element[GatherProps] = function gatherTooltipProps(this: typeof options) { - return { - element: { - [Component]: elementComponent, - [GatherProps]: elementGatherProps - }, - mouseDown: handleMouseDown, - mouseUp: handleMouseUp, - position: computedPosition - }; - }.bind(options); - }); + element.wrappers.push(el => ( + + {el} + + )); } /** An object that configures how to setup a list of actions using {@link setupActions}. */ export interface SetupActionsOptions { /** The node to display actions upon, or undefined when the actions should be hidden. */ - node: Computable; + node: MaybeRefOrGetter; /** Whether or not to currently display the actions. */ - shouldShowActions?: NodeComputable; + shouldShowActions?: NodeMaybeRefOrGetter; /** The list of actions to display. Actions are arbitrary JSX elements. */ - actions: NodeComputable JSX.Element)[]>; + actions: NodeMaybeRefOrGetter JSX.Element)[]>; /** The distance from the node to place the actions. */ - distance: NodeComputable; + distance: NodeMaybeRefOrGetter; /** The arc length to place between actions, in radians. */ - arcLength?: NodeComputable; + arcLength?: NodeMaybeRefOrGetter; } /** @@ -330,8 +317,8 @@ export interface SetupActionsOptions { * @returns A JSX function to render the actions. */ export function setupActions(options: SetupActionsOptions) { - const node = convertComputable(options.node); - return jsx(() => { + const node = processGetter(options.node) as MaybeRef; + return computed(() => { const currNode = unref(node); if (currNode == null) { return ""; diff --git a/src/game/events.ts b/src/game/events.ts index 9e74714..c89cca3 100644 --- a/src/game/events.ts +++ b/src/game/events.ts @@ -1,7 +1,7 @@ import type { Settings } from "game/settings"; import { createNanoEvents } from "nanoevents"; import type { App } from "vue"; -import type { GenericLayer } from "./layers"; +import type { Layer } from "./layers"; import state from "./state"; /** All types of events able to be sent or emitted from the global event bus. */ @@ -11,12 +11,12 @@ export interface GlobalEvents { * @param layer The layer being added. * @param saveData The layer's save data object within player. */ - addLayer: (layer: GenericLayer, saveData: Record) => void; + addLayer: (layer: Layer, saveData: Record) => void; /** * Sent whenever a layer is removed. * @param layer The layer being removed. */ - removeLayer: (layer: GenericLayer) => void; + removeLayer: (layer: Layer) => void; /** * Sent every game tick. Runs the life cycle of the project. * @param diff The delta time since last tick, in ms. diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index d9b0a78..03a53a7 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,7 +1,7 @@ import { Resource } from "features/resources/resource"; import { NonPersistent } from "game/persistence"; import Decimal, { DecimalSource, format } from "util/bignum"; -import { Computable, ProcessedComputable, convertComputable } from "util/computed"; +import { MaybeRefOrGetter, MaybeRef, processGetter } from "util/computed"; import { Ref, computed, ref, unref } from "vue"; import * as ops from "./operations"; import type { @@ -60,7 +60,7 @@ export abstract class InternalFormula | undefined; + public readonly innermostVariable: MaybeRef | undefined; constructor(options: FormulaOptions) { let readonlyProperties; @@ -93,7 +93,7 @@ export abstract class InternalFormula; + variable: MaybeRef; }): InternalFormulaProperties { return { inputs: [variable] as T, @@ -207,7 +207,7 @@ export abstract class InternalFormula): InvertibleIntegralFormula { + public static constant(value: MaybeRef): InvertibleIntegralFormula { return new Formula({ inputs: [value] }); } @@ -215,7 +215,7 @@ export abstract class InternalFormula): InvertibleIntegralFormula { + public static variable(value: MaybeRef): InvertibleIntegralFormula { return new Formula({ variable: value }); } @@ -248,11 +248,11 @@ export abstract class InternalFormula, + start: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula ) { const formula = formulaModifier(Formula.variable(0)); - const processedStart = convertComputable(start); + const processedStart = processGetter(start); function evalStep(lhs: DecimalSource) { if (Decimal.lt(lhs, unref(processedStart))) { return lhs; @@ -293,7 +293,7 @@ export abstract class InternalFormula, + condition: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula, elseFormulaModifier?: (value: InvertibleIntegralFormula) => GenericFormula ) { @@ -301,7 +301,7 @@ export abstract class InternalFormula, + condition: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula, elseFormulaModifier?: (value: InvertibleIntegralFormula) => GenericFormula ) { @@ -909,20 +909,20 @@ export abstract class InternalFormula, + start: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula ) { return Formula.step(this, start, formulaModifier); } public if( - condition: Computable, + condition: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula ) { return Formula.if(this, condition, formulaModifier); } public conditional( - condition: Computable, + condition: MaybeRefOrGetter, formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula ) { return Formula.if(this, condition, formulaModifier); @@ -1443,13 +1443,13 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul export function calculateMaxAffordable( formula: GenericFormula, resource: Resource, - cumulativeCost: Computable = true, - directSum?: Computable, - maxBulkAmount: Computable = Decimal.dInf + cumulativeCost: MaybeRefOrGetter = true, + directSum?: MaybeRefOrGetter, + maxBulkAmount: MaybeRefOrGetter = Decimal.dInf ) { - const computedCumulativeCost = convertComputable(cumulativeCost); - const computedDirectSum = convertComputable(directSum); - const computedmaxBulkAmount = convertComputable(maxBulkAmount); + const computedCumulativeCost = processGetter(cumulativeCost); + const computedDirectSum = processGetter(directSum); + const computedmaxBulkAmount = processGetter(maxBulkAmount); return computed(() => { const maxBulkAmount = unref(computedmaxBulkAmount); if (Decimal.eq(maxBulkAmount, 1)) { diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts index cc185a9..3f98f55 100644 --- a/src/game/formulas/types.d.ts +++ b/src/game/formulas/types.d.ts @@ -1,10 +1,10 @@ import { InternalFormula } from "game/formulas/formulas"; import { DecimalSource } from "util/bignum"; -import { ProcessedComputable } from "util/computed"; +import { MaybeRef } from "util/computed"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type GenericFormula = InternalFormula; -type FormulaSource = ProcessedComputable | GenericFormula; +type FormulaSource = MaybeRef | GenericFormula; type InvertibleFormula = GenericFormula & { invert: NonNullable; }; @@ -38,7 +38,7 @@ type SubstitutionFunction = ( ) => GenericFormula; type VariableFormulaOptions = { - variable: ProcessedComputable; + variable: MaybeRef; description?: string; }; type ConstantFormulaOptions = { @@ -67,7 +67,7 @@ type InternalFormulaProperties = { internalIntegrate?: IntegrateFunction; internalIntegrateInner?: IntegrateFunction; applySubstitution?: SubstitutionFunction; - innermostVariable?: ProcessedComputable; + innermostVariable?: MaybeRef; description?: string; }; diff --git a/src/game/layers.tsx b/src/game/layers.tsx index 7e82ead..a5f3fe2 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -1,28 +1,26 @@ import Modal from "components/modals/Modal.vue"; -import type { - CoercableComponent, - JSXFunction, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; -import { jsx, setDefault } from "features/feature"; +import type { OptionsFunc, Replace } from "features/feature"; import { globalBus } from "game/events"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; import player from "game/player"; import type { Emitter } from "nanoevents"; import { createNanoEvents } from "nanoevents"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { processComputable } from "util/computed"; +import { ProcessedRefOrGetter, processGetter } from "util/computed"; import { createLazyProxy } from "util/proxies"; -import { computed, InjectionKey, Ref } from "vue"; -import { ref, shallowReactive, unref } from "vue"; +import { Renderable } from "util/vue"; +import { + computed, + type CSSProperties, + InjectionKey, + MaybeRef, + MaybeRefOrGetter, + Ref, + ref, + shallowReactive, + unref +} from "vue"; +import { JSX } from "vue/jsx-runtime"; /** A feature's node in the DOM that has its size tracked. */ export interface FeatureNode { @@ -74,12 +72,12 @@ export interface LayerEvents { * A reference to all the current layers. * It is shallow reactive so it will update when layers are added or removed, but not interfere with the existing refs within each layer. */ -export const layers: Record | undefined> = shallowReactive({}); +export const layers: Record> = shallowReactive({}); declare global { /** Augment the window object so the layers can be accessed from the console. */ interface Window { - layers: Record | undefined>; + layers: Record | undefined>; } } window.layers = layers; @@ -106,42 +104,42 @@ export interface Position { */ export interface LayerOptions { /** The color of the layer, used to theme the entire layer's display. */ - color?: Computable; + color?: MaybeRefOrGetter; /** * The layout of this layer's features. * When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is displayed. */ - display: Computable; + display: MaybeRefOrGetter; /** An object of classes that should be applied to the display. */ - classes?: Computable>; + classes?: MaybeRefOrGetter>; /** Styles that should be applied to the display. */ - style?: Computable; + style?: MaybeRefOrGetter; /** * The name of the layer, used on minimized tabs. * Defaults to {@link BaseLayer.id}. */ - name?: Computable; + name?: MaybeRefOrGetter; /** * Whether or not the layer can be minimized. * Defaults to true. */ - minimizable?: Computable; + minimizable?: MaybeRefOrGetter; /** * 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; + minimizedDisplay?: MaybeRefOrGetter; /** * 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}. */ - forceHideGoBack?: Computable; + forceHideGoBack?: MaybeRefOrGetter; /** * A CSS min-width value that is applied to the layer. * Can be a number, in which case the unit is assumed to be px. * Defaults to 600px. */ - minWidth?: Computable; + minWidth?: MaybeRefOrGetter; } /** The properties that are added onto a processed {@link LayerOptions} to create a {@link Layer} */ @@ -165,28 +163,18 @@ export interface BaseLayer { } /** An unit of game content. Displayed to the user as a tab or modal. */ -export type Layer = Replace< - T & BaseLayer, +export type Layer = Replace< + Replace, { - color: GetComputableType; - display: GetComputableType; - classes: GetComputableType; - style: GetComputableType; - name: GetComputableTypeWithDefault; - minWidth: GetComputableTypeWithDefault; - minimizable: GetComputableTypeWithDefault; - minimizedDisplay: GetComputableType; - forceHideGoBack: GetComputableType; - } ->; - -/** A type that matches any valid {@link Layer} object. */ -export type GenericLayer = Replace< - Layer, - { - name: ProcessedComputable; - minWidth: ProcessedComputable; - minimizable: ProcessedComputable; + color?: ProcessedRefOrGetter; + display: ProcessedRefOrGetter; + classes?: ProcessedRefOrGetter; + style?: ProcessedRefOrGetter; + name: MaybeRef; + minWidth: MaybeRef; + minimizable: MaybeRef; + minimizedDisplay?: ProcessedRefOrGetter; + forceHideGoBack?: ProcessedRefOrGetter; } >; @@ -206,72 +194,85 @@ export const addingLayers: string[] = []; export function createLayer( id: string, optionsFunc: OptionsFunc -): Layer { +) { return createLazyProxy(() => { - const layer = {} as T & Partial; - const emitter = (layer.emitter = createNanoEvents()); - layer.on = emitter.on.bind(emitter); - layer.emit = emitter.emit.bind(emitter) as ( - ...args: [K, ...Parameters] - ) => void; - layer.nodes = ref({}); - layer.id = id; - + const emitter = createNanoEvents(); addingLayers.push(id); persistentRefs[id] = new Set(); - layer.minimized = persistent(false, false); - Object.assign(layer, optionsFunc.call(layer, layer as BaseLayer)); + + const baseLayer = { + id, + emitter, + ...emitter, + nodes: ref({}), + minimized: persistent(false, false) + } satisfies BaseLayer; + + const options = optionsFunc.call(baseLayer, baseLayer); + const { + color, + display, + classes, + style: _style, + name, + forceHideGoBack, + minWidth, + minimizable, + minimizedDisplay, + ...props + } = options; if ( addingLayers[addingLayers.length - 1] == null || addingLayers[addingLayers.length - 1] !== id ) { throw new Error( - `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}` + `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${id}` ); } addingLayers.pop(); - 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"); - setDefault(layer, "minWidth", 600); - processComputable(layer as T, "minimizable"); - setDefault(layer, "minimizable", true); - processComputable(layer as T, "minimizedDisplay"); + const style = processGetter(_style); - const style = layer.style as ProcessedComputable | undefined; - layer.style = computed(() => { - let width = unref(layer.minWidth as ProcessedComputable); - 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; + const layer = { + ...baseLayer, + ...(props as Omit), + color: processGetter(color), + display: processGetter(display), + classes: processGetter(classes), + style: computed((): CSSProperties => { + let width = unref(layer.minWidth); + if (typeof width === "number" || !Number.isNaN(parseInt(width))) { + width = width + "px"; + } + return { + ...unref(style), + ...(baseLayer.minimized.value + ? { + flexGrow: "0", + flexShrink: "0", + width: "60px", + minWidth: "", + flexBasis: "", + margin: "0" + } + : { + flexGrow: "", + flexShrink: "", + width: "", + minWidth: width, + flexBasis: width, + margin: "" + }) + }; + }), + name: processGetter(name) ?? id, + forceHideGoBack: processGetter(forceHideGoBack), + minWidth: processGetter(minWidth) ?? 600, + minimizable: processGetter(minimizable) ?? true, + minimizedDisplay: processGetter(minimizedDisplay) + } satisfies Layer; - return layer as unknown as Layer; + return layer; }); } @@ -284,11 +285,11 @@ export function createLayer( * @param player The player data object, which will have a data object for this layer. */ export function addLayer( - layer: GenericLayer, + layer: Layer, player: { layers?: Record> } ): void { console.info("Adding layer", layer.id); - if (layers[layer.id]) { + if (layers[layer.id] != null) { console.error( "Attempted to add layer with same ID as existing layer", layer.id, @@ -297,7 +298,7 @@ export function addLayer( return; } - setDefault(player, "layers", {}); + player.layers ??= {}; if (player.layers[layer.id] == null) { player.layers[layer.id] = {}; } @@ -310,7 +311,7 @@ export function addLayer( * Convenience method for getting a layer by its ID with correct typing. * @param layerID The ID of the layer to get. */ -export function getLayer(layerID: string): T { +export function getLayer(layerID: string): T { return layers[layerID] as T; } @@ -319,11 +320,11 @@ export function getLayer(layerID: string): T { * Note that accessing a layer/its properties does NOT require it to be enabled. * @param layer The layer to remove. */ -export function removeLayer(layer: GenericLayer): void { +export function removeLayer(layer: Layer): void { console.info("Removing layer", layer.id); globalBus.emit("removeLayer", layer); - layers[layer.id] = undefined; + delete layers[layer.id]; } /** @@ -331,7 +332,7 @@ export function removeLayer(layer: GenericLayer): void { * This is useful for layers with dynamic content, to ensure persistent refs are correctly configured. * @param layer Layer to remove and then re-add */ -export function reloadLayer(layer: GenericLayer): void { +export function reloadLayer(layer: Layer): void { removeLayer(layer); // Re-create layer @@ -343,14 +344,14 @@ export function reloadLayer(layer: GenericLayer): void { * Returns the modal itself, which can be rendered anywhere you need, as well as a function to open the modal. * @param layer The layer to display in the modal. */ -export function setupLayerModal(layer: GenericLayer): { +export function setupLayerModal(layer: Layer): { openModal: VoidFunction; - modal: JSXFunction; + modal: Ref; } { const showModal = ref(false); return { openModal: () => (showModal.value = true), - modal: jsx(() => ( + modal: computed(() => ( (showModal.value = value)} diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx index 55efccb..995b300 100644 --- a/src/game/modifiers.tsx +++ b/src/game/modifiers.tsx @@ -1,15 +1,13 @@ import "components/common/modifiers.css"; -import type { CoercableComponent, OptionsFunc } from "features/feature"; -import { jsx } from "features/feature"; +import type { OptionsFunc } from "features/feature"; import settings from "game/settings"; import type { DecimalSource } from "util/bignum"; import Decimal, { formatSmall } from "util/bignum"; import type { RequiredKeys, WithRequired } from "util/common"; -import type { Computable, ProcessedComputable } from "util/computed"; -import { convertComputable } from "util/computed"; +import { processGetter } from "util/computed"; import { createLazyProxy } from "util/proxies"; -import { renderJSX } from "util/vue"; -import { computed, unref } from "vue"; +import { render, Renderable } from "util/vue"; +import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue"; import Formula from "./formulas/formulas"; import { FormulaSource, GenericFormula } from "./formulas/types"; @@ -30,12 +28,12 @@ export interface Modifier { * Whether or not this modifier should be considered enabled. * Typically for use with modifiers passed into {@link createSequentialModifier}. */ - enabled?: ProcessedComputable; + enabled?: MaybeRef; /** * A description of this modifier. * @see {@link createModifierSection}. */ - description?: ProcessedComputable; + description?: MaybeRef; } /** Utility type that represents the output of all modifiers that represent a single operation. */ @@ -47,11 +45,11 @@ export type OperationModifier = WithRequired< /** An object that configures an additive modifier via {@link createAdditiveModifier}. */ export interface AdditiveModifierOptions { /** The amount to add to the input value. */ - addend: Computable; + addend: MaybeRefOrGetter; /** Description of what this modifier is doing. */ - description?: Computable; - /** A computable that will be processed and passed directly into the returned modifier. */ - enabled?: Computable; + description?: MaybeRefOrGetter; + /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */ + enabled?: MaybeRefOrGetter; /** Determines if numbers larger or smaller than 0 should be displayed as red. */ smallerIsBetter?: boolean; } @@ -69,25 +67,22 @@ export function createAdditiveModifier Decimal.add(gain, unref(processedAddend)), invert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)), getFormula: (gain: FormulaSource) => Formula.add(gain, processedAddend), enabled: processedEnabled, description: - description == null + processedDescription == null ? undefined - : jsx(() => ( + : computed(() => (
- {unref(processedDescription) != null ? ( - - {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} - {renderJSX(unref(processedDescription)!)} - - ) : null} + + {render(processedDescription)} + ; + multiplier: MaybeRefOrGetter; /** Description of what this modifier is doing. */ - description?: Computable | undefined; - /** A computable that will be processed and passed directly into the returned modifier. */ - enabled?: Computable | undefined; + description?: MaybeRefOrGetter | undefined; + /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */ + enabled?: MaybeRefOrGetter | undefined; /** Determines if numbers larger or smaller than 1 should be displayed as red. */ smallerIsBetter?: boolean; } @@ -135,25 +130,22 @@ export function createMultiplicativeModifier< feature ); - const processedMultiplier = convertComputable(multiplier); - const processedDescription = convertComputable(description); - const processedEnabled = enabled == null ? undefined : convertComputable(enabled); + const processedMultiplier = processGetter(multiplier); + const processedDescription = processGetter(description); + const processedEnabled = enabled == null ? undefined : processGetter(enabled); return { apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)), invert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)), getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier), enabled: processedEnabled, description: - description == null + processedDescription == null ? undefined - : jsx(() => ( + : computed(() => (
- {unref(processedDescription) != null ? ( - - {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} - {renderJSX(unref(processedDescription)!)} - - ) : null} + + {render(processedDescription)} + ; + exponent: MaybeRefOrGetter; /** Description of what this modifier is doing. */ - description?: Computable | undefined; - /** A computable that will be processed and passed directly into the returned modifier. */ - enabled?: Computable | undefined; + description?: MaybeRefOrGetter | undefined; + /** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */ + enabled?: MaybeRefOrGetter | 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. */ @@ -200,9 +192,9 @@ export function createExponentialModifier< const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } = optionsFunc.call(feature, feature); - const processedExponent = convertComputable(exponent); - const processedDescription = convertComputable(description); - const processedEnabled = enabled == null ? undefined : convertComputable(enabled); + const processedExponent = processGetter(exponent); + const processedDescription = processGetter(description); + const processedEnabled = enabled == null ? undefined : processGetter(enabled); return { apply: (gain: DecimalSource) => { let result = gain; @@ -232,17 +224,14 @@ export function createExponentialModifier< : Formula.pow(gain, processedExponent), enabled: processedEnabled, description: - description == null + processedDescription == null ? undefined - : jsx(() => ( + : computed(() => (
- {unref(processedDescription) != null ? ( - - {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} - {renderJSX(unref(processedDescription)!)} - {supportLowNumbers ? " (+1 effective)" : null} - - ) : null} + + {render(processedDescription)} + {supportLowNumbers ? " (+1 effective)" : null} + modifiers.filter(m => unref(m.enabled) !== false).length > 0) : undefined, description: modifiers.some(m => m.description != null) - ? jsx(() => ( - <> - {( - modifiers - .filter(m => unref(m.enabled) !== false) - .map(m => unref(m.description)) - .filter(d => d) as CoercableComponent[] - ).map(renderJSX)} - - )) + ? computed(() => + ( + modifiers + .filter(m => unref(m.enabled) !== false) + .map(m => unref(m.description)) + .filter(d => d) as MaybeRef[] + ).map(m => render(m)) + ) : undefined }; }) as S; @@ -332,7 +319,7 @@ export interface ModifierSectionOptions { /** The unit of the value being modified, if any. */ unit?: string; /** The label to use for the base value. Defaults to "Base". */ - baseText?: CoercableComponent; + baseText?: MaybeRefOrGetter; /** Determines if numbers larger or smaller than the base should be displayed as red. */ smallerIsBetter?: boolean; } @@ -352,6 +339,7 @@ export function createModifierSection({ smallerIsBetter }: ModifierSectionOptions) { const total = modifier.apply(base ?? 1); + const processedBaseText = processGetter(baseText); return (

@@ -360,13 +348,13 @@ export function createModifierSection({


- {renderJSX(baseText ?? "Base")} + {render(processedBaseText ?? "Base")} {formatSmall(base ?? 1)} {unit}
- {renderJSX(unref(modifier.description))} + {render(modifier.description)}
Total diff --git a/src/game/notifications.ts b/src/game/notifications.ts index 5a43c1a..46039b8 100644 --- a/src/game/notifications.ts +++ b/src/game/notifications.ts @@ -1,5 +1,5 @@ import { globalBus } from "game/events"; -import { convertComputable } from "util/computed"; +import { processGetter } from "util/computed"; import { trackHover, VueFeature } from "util/vue"; import { nextTick, Ref } from "vue"; import { ref, watch } from "vue"; @@ -37,7 +37,7 @@ export function createDismissableNotify( element: VueFeature, shouldNotify: Ref | (() => boolean) ): Ref { - const processedShouldNotify = convertComputable(shouldNotify) as Ref; + const processedShouldNotify = processGetter(shouldNotify) as Ref; const notifying = ref(false); nextTick(() => { notifying.value = processedShouldNotify.value; diff --git a/src/game/persistence.ts b/src/game/persistence.ts index 253481e..20c0534 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -1,14 +1,14 @@ import { globalBus } from "game/events"; -import type { GenericLayer } from "game/layers"; +import type { Layer } from "game/layers"; import { addingLayers, persistentRefs } from "game/layers"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; import { ProxyState } from "util/proxies"; import type { Ref, WritableComputedRef } from "vue"; import { computed, isReactive, isRef, ref } from "vue"; +import Formula from "./formulas/formulas"; import player from "./player"; import state from "./state"; -import Formula from "./formulas/formulas"; /** * A symbol used in {@link Persistent} objects. @@ -251,7 +251,7 @@ export function deletePersistent(persistent: Persistent) { persistent[Deleted] = true; } -globalBus.on("addLayer", (layer: GenericLayer, saveData: Record) => { +globalBus.on("addLayer", (layer: Layer, saveData: Record) => { const features: { type: typeof Symbol }[] = []; const handleObject = (obj: Record, path: string[] = []): boolean => { let foundPersistent = false; diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 195efa8..a3b260f 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -1,26 +1,12 @@ -import { - CoercableComponent, - isVisible, - jsx, - OptionsFunc, - Replace, - setDefault, - Visibility -} from "features/feature"; +import { isVisible, OptionsFunc, Replace, 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 { processGetter } from "util/computed"; import { createLazyProxy } from "util/proxies"; -import { joinJSX, renderJSX } from "util/vue"; -import { computed, unref } from "vue"; -import { JSX } from "vue/jsx-runtime"; +import { joinJSX, render, Renderable } from "util/vue"; +import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue"; import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas"; -import type { GenericFormula } from "./formulas/types"; +import type { GenericFormula, InvertibleIntegralFormula } from "./formulas/types"; import { DefaultValue, Persistent } from "./persistence"; /** @@ -31,27 +17,27 @@ 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?: (amount?: DecimalSource) => JSX.Element; + partialDisplay?: (amount?: DecimalSource) => Renderable; /** * The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */ - display?: (amount?: DecimalSource) => JSX.Element; + display?: (amount?: DecimalSource) => Renderable; /** * Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property. */ - visibility: ProcessedComputable; + visibility: MaybeRef; /** * Whether or not this requirement has been met. */ - requirementMet: ProcessedComputable; + requirementMet: MaybeRef; /** * Whether or not this requirement will need to affect the game state when whatever is using this requirement gets triggered. */ - requiresPay: ProcessedComputable; + requiresPay: MaybeRef; /** * 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. */ - canMaximize?: ProcessedComputable; + canMaximize?: MaybeRef; /** * 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. @@ -73,28 +59,28 @@ export interface CostRequirementOptions { /** * 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 | GenericFormula; + cost: MaybeRefOrGetter | GenericFormula; /** * Pass-through to {@link Requirement.visibility}. */ - visibility?: Computable; + visibility?: MaybeRefOrGetter; /** * 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; + requiresPay?: MaybeRefOrGetter; /** * 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} */ - cumulativeCost?: Computable; + cumulativeCost?: MaybeRefOrGetter; /** * Upper limit on levels that can be performed at once. Defaults to 1. */ - maxBulkAmount?: Computable; + maxBulkAmount?: MaybeRefOrGetter; /** * When calculating requirement for multiple levels, how many should be directly summed for increase accuracy. High numbers can cause lag. Defaults to 10 if cumulative cost, 0 otherwise. */ - directSum?: Computable; + directSum?: MaybeRefOrGetter; /** * Pass-through to {@link Requirement.pay}. May be required for maximizing support. * @see {@link cost} for restrictions on maximizing support. @@ -105,11 +91,11 @@ export interface CostRequirementOptions { export type CostRequirement = Replace< Requirement & CostRequirementOptions, { - cost: ProcessedComputable | GenericFormula; - visibility: ProcessedComputable; - requiresPay: ProcessedComputable; - cumulativeCost: ProcessedComputable; - canMaximize: ProcessedComputable; + cost: MaybeRef | GenericFormula; + visibility: MaybeRef; + requiresPay: MaybeRef; + cumulativeCost: MaybeRef; + canMaximize: MaybeRef; } >; @@ -119,116 +105,123 @@ export type CostRequirement = Replace< */ export function createCostRequirement( optionsFunc: OptionsFunc -): CostRequirement { +) { return createLazyProxy(feature => { - const req = optionsFunc.call(feature, feature) as T & Partial; + const options = optionsFunc.call(feature, feature); + const { + visibility, + cost, + resource, + requiresPay, + cumulativeCost, + maxBulkAmount, + directSum, + pay + } = options; - req.partialDisplay = amount => ( - ) - ? "" - : "color: var(--danger)" + const requirement = { + resource, + visibility: processGetter(visibility) ?? Visibility.Visible, + cost: processGetter(cost), + requiresPay: processGetter(requiresPay) ?? true, + cumulativeCost: processGetter(cumulativeCost) ?? true, + maxBulkAmount: processGetter(maxBulkAmount) ?? 1, + directSum: processGetter(directSum), + partialDisplay: (amount?: DecimalSource) => ( + + {displayResource( + resource, + requirement.cost instanceof Formula + ? calculateCost( + requirement.cost as InvertibleIntegralFormula, + amount ?? 1, + unref(requirement.cumulativeCost), + unref(requirement.directSum) + ) + : unref(requirement.cost as MaybeRef) + )}{" "} + {resource.displayName} + + ), + display: (amount?: DecimalSource) => ( +
+ {unref(requirement.requiresPay as MaybeRef) ? "Costs: " : "Requires: "} + {displayResource( + resource, + requirement.cost instanceof Formula + ? calculateCost( + requirement.cost as InvertibleIntegralFormula, + amount ?? 1, + unref(requirement.cumulativeCost), + unref(requirement.directSum) + ) + : unref(requirement.cost as MaybeRef) + )}{" "} + {resource.displayName} +
+ ), + canMaximize: computed(() => { + if (!(requirement.cost instanceof Formula)) { + return false; + } + const maxBulkAmount = unref(requirement.maxBulkAmount); + if (Decimal.lte(maxBulkAmount, 1)) { + return false; + } + const cumulativeCost = unref(requirement.cumulativeCost); + const directSum = unref(requirement.directSum) ?? (cumulativeCost ? 10 : 0); + if (Decimal.lte(maxBulkAmount, directSum)) { + return true; + } + if (!requirement.cost.isInvertible()) { + return false; + } + if (cumulativeCost === true && !requirement.cost.isIntegrable()) { + return false; } - > - {displayResource( - req.resource, - req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref(req.cumulativeCost) as boolean, - unref(req.directSum) as number - ) - : unref(req.cost as ProcessedComputable) - )}{" "} - {req.resource.displayName} -
- ); - req.display = amount => ( -
- {unref(req.requiresPay as ProcessedComputable) ? "Costs: " : "Requires: "} - {displayResource( - req.resource, - req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref(req.cumulativeCost) as boolean, - unref(req.directSum) as number - ) - : unref(req.cost as ProcessedComputable) - )}{" "} - {req.resource.displayName} -
- ); - - processComputable(req as T, "visibility"); - setDefault(req, "visibility", Visibility.Visible); - processComputable(req as T, "cost"); - processComputable(req as T, "requiresPay"); - setDefault(req, "requiresPay", true); - processComputable(req as T, "cumulativeCost"); - setDefault(req, "cumulativeCost", true); - processComputable(req as T, "maxBulkAmount"); - setDefault(req, "maxBulkAmount", 1); - processComputable(req as T, "directSum"); - setDefault(req, "pay", function (amount?: DecimalSource) { - const cost = - req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref(req.cumulativeCost as ProcessedComputable), - unref(req.directSum) as number - ) - : unref(req.cost as ProcessedComputable); - req.resource.value = Decimal.sub(req.resource.value, cost).max(0); - }); - - req.canMaximize = computed(() => { - if (!(req.cost instanceof Formula)) { - return false; - } - const maxBulkAmount = unref(req.maxBulkAmount as ProcessedComputable); - if (Decimal.lte(maxBulkAmount, 1)) { - return false; - } - const cumulativeCost = unref(req.cumulativeCost as ProcessedComputable); - const directSum = - unref(req.directSum as ProcessedComputable) ?? (cumulativeCost ? 10 : 0); - if (Decimal.lte(maxBulkAmount, directSum)) { return true; - } - if (!req.cost.isInvertible()) { - return false; - } - if (cumulativeCost === true && !req.cost.isIntegrable()) { - return false; - } - return true; - }); + }), + requirementMet: + cost instanceof Formula + ? calculateMaxAffordable( + cost, + resource, + cumulativeCost ?? true, + directSum, + maxBulkAmount + ) + : computed( + (): DecimalSource => + Decimal.gte( + resource.value, + unref(requirement.cost as MaybeRef) + ) + ? 1 + : 0 + ), + pay: + pay ?? + function (amount?: DecimalSource) { + const cost = + requirement.cost instanceof Formula + ? calculateCost( + requirement.cost, + amount ?? 1, + unref(requirement.cumulativeCost), + unref(requirement.directSum) + ) + : unref(requirement.cost as MaybeRef); + resource.value = Decimal.sub(resource.value, cost).max(0); + } + } satisfies CostRequirement; - if (req.cost instanceof Formula) { - req.requirementMet = calculateMaxAffordable( - req.cost, - req.resource, - req.cumulativeCost ?? true, - req.directSum, - req.maxBulkAmount - ); - } else { - req.requirementMet = computed(() => - Decimal.gte( - req.resource.value, - unref(req.cost as ProcessedComputable) - ) - ? 1 - : 0 - ); - } - - return req as CostRequirement; + return requirement; }); } @@ -236,11 +229,11 @@ export function createCostRequirement( * 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; -}): Requirement { +export function createVisibilityRequirement( + visibility: MaybeRef +): Requirement { return createLazyProxy(() => ({ - requirementMet: computed(() => isVisible(feature.visibility)), + requirementMet: computed(() => isVisible(visibility)), visibility: Visibility.None, requiresPay: false })); @@ -252,16 +245,20 @@ export function createVisibilityRequirement(feature: { * @param display How to display this requirement to the user */ export function createBooleanRequirement( - requirement: Computable, - display?: CoercableComponent + requirement: MaybeRefOrGetter, + display?: MaybeRefOrGetter ): 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 - })); + return createLazyProxy(() => { + const processedDisplay = processGetter(display); + return { + requirementMet: processGetter(requirement), + partialDisplay: processedDisplay == null ? undefined : () => render(processedDisplay), + display: + processedDisplay == null ? undefined : () => <>Req: {render(processedDisplay)}, + visibility: processedDisplay == null ? Visibility.None : Visibility.Visible, + requiresPay: false + }; + }); } /** @@ -300,7 +297,7 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource { */ export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) { if (Array.isArray(requirements)) { - requirements = requirements.filter(r => isVisible(r.visibility)); + requirements = requirements.filter(r => isVisible(r.visibility ?? true)); if (requirements.length === 1) { requirements = requirements[0]; } @@ -356,9 +353,9 @@ export function payByDivision(this: CostRequirement, amount?: DecimalSource) { ? calculateCost( this.cost, amount ?? 1, - unref(this.cumulativeCost as ProcessedComputable | undefined) ?? true + unref(this.cumulativeCost as MaybeRef | undefined) ?? true ) - : unref(this.cost as ProcessedComputable); + : unref(this.cost as MaybeRef); this.resource.value = Decimal.div(this.resource.value, cost); } diff --git a/src/game/settings.ts b/src/game/settings.ts index d24b810..9360001 100644 --- a/src/game/settings.ts +++ b/src/game/settings.ts @@ -1,10 +1,11 @@ import projInfo from "data/projInfo.json"; import { Themes } from "data/themes"; -import type { CoercableComponent } from "features/feature"; import { globalBus } from "game/events"; import LZString from "lz-string"; +import { processGetter } from "util/computed"; import { decodeSave, hardReset } from "util/save"; -import { reactive, watch } from "vue"; +import { Renderable } from "util/vue"; +import { MaybeRef, MaybeRefOrGetter, reactive, watch } from "vue"; /** The player's settings object. */ export interface Settings { @@ -100,22 +101,22 @@ export function loadSettings(): void { } /** A list of fields to append to the settings modal. */ -export const settingFields: CoercableComponent[] = reactive([]); +export const settingFields: MaybeRef[] = reactive([]); /** Register a field to be displayed in the settings modal. */ -export function registerSettingField(component: CoercableComponent) { - settingFields.push(component); +export function registerSettingField(component: MaybeRefOrGetter) { + settingFields.push(processGetter(component)); } /** A list of components to show in the info modal. */ -export const infoComponents: CoercableComponent[] = reactive([]); +export const infoComponents: MaybeRef[] = reactive([]); /** Register a component to be displayed in the info modal. */ -export function registerInfoComponent(component: CoercableComponent) { - infoComponents.push(component); +export function registerInfoComponent(component: MaybeRefOrGetter) { + infoComponents.push(processGetter(component)); } /** A list of components to add to the root of the page. */ -export const gameComponents: CoercableComponent[] = reactive([]); +export const gameComponents: MaybeRef[] = reactive([]); /** Register a component to be displayed at the root of the page. */ -export function registerGameComponent(component: CoercableComponent) { - gameComponents.push(component); +export function registerGameComponent(component: MaybeRefOrGetter) { + gameComponents.push(processGetter(component)); } diff --git a/src/mixins/bonusDecorator.ts b/src/mixins/bonusDecorator.ts new file mode 100644 index 0000000..b08954e --- /dev/null +++ b/src/mixins/bonusDecorator.ts @@ -0,0 +1,29 @@ +import Decimal, { DecimalSource } from "util/bignum"; +import { processGetter } from "util/computed"; +import { MaybeRefOrGetter, Ref, computed, unref } from "vue"; + +/** Allows the addition of "bonus levels" to a feature, with an accompanying "total amount". */ +export function bonusAmountMixin( + feature: { amount: Ref }, + bonusAmount: MaybeRefOrGetter +) { + const processedBonusAmount = processGetter(bonusAmount); + return { + bonusAmount, + totalAmount: computed(() => Decimal.add(unref(feature.amount), unref(processedBonusAmount))) + }; +} + +/** Allows the addition of "bonus completions" to a feature, with an accompanying "total completions". */ +export function bonusCompletionsMixin( + feature: { completions: Ref }, + bonusCompletions: MaybeRefOrGetter +) { + const processedBonusCompletions = processGetter(bonusCompletions); + return { + bonusCompletions, + totalCompletions: computed(() => + Decimal.add(unref(feature.completions), unref(processedBonusCompletions)) + ) + }; +} diff --git a/src/util/computed.ts b/src/util/computed.ts index ff11103..869b38f 100644 --- a/src/util/computed.ts +++ b/src/util/computed.ts @@ -1,57 +1,16 @@ -import type { JSXFunction } from "features/feature"; import { isFunction } from "util/common"; -import type { Ref } from "vue"; +import type { ComputedRef, MaybeRef, Ref, UnwrapRef } from "vue"; import { computed } from "vue"; -export const DoNotCache = Symbol("DoNotCache"); +export type ProcessedRefOrGetter = T extends () => infer S + ? Ref + : T extends undefined + ? undefined + : MaybeRef>>; -export type Computable = T | Ref | (() => T); -export type ProcessedComputable = T | Ref; -export type GetComputableType = T extends { [DoNotCache]: true } - ? T - : T extends () => infer S - ? Ref - : undefined extends T - ? undefined - : T; -export type GetComputableTypeWithDefault = undefined extends T - ? S - : GetComputableType>; -export type UnwrapComputableType = T extends Ref ? S : T extends () => infer S ? S : T; - -export type ComputableKeysOf = Pick< - T, - { - [K in keyof T]: T[K] extends Computable ? K : never; - }[keyof T] ->; - -// TODO fix the typing of this function, such that casting isn't necessary and can be used to -// detect if a createX function is validly written -export function processComputable>( - obj: T, - key: S -): asserts obj is T & { [K in S]: ProcessedComputable> } { - const computable = obj[key]; - 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]; - (obj[key] as unknown as JSXFunction)[DoNotCache] = true; +export function processGetter(obj: T): T extends () => infer S ? ComputedRef : T { + if (isFunction(obj)) { + return computed(obj) as ReturnType>; } -} - -export function convertComputable(obj: Computable): ProcessedComputable { - if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - obj = computed(obj); - } - return obj as ProcessedComputable; + return obj as ReturnType>; } diff --git a/src/util/galaxy.ts b/src/util/galaxy.ts index b2a4d04..b59ead5 100644 --- a/src/util/galaxy.ts +++ b/src/util/galaxy.ts @@ -82,7 +82,7 @@ function syncSaves( const saves = ( Object.keys(list) .map(slot => { - const { label, content } = list[slot as unknown as number]; + const { label, content } = list[parseInt(slot)]; try { return { slot: parseInt(slot), diff --git a/src/util/proxies.ts b/src/util/proxies.ts index 7476ba1..8447931 100644 --- a/src/util/proxies.ts +++ b/src/util/proxies.ts @@ -33,9 +33,9 @@ export type Proxied = // Takes a function that returns an object and pretends to be that object // Note that the object is lazily calculated export function createLazyProxy( - objectFunc: (this: S, baseObject: S) => T & S, + objectFunc: (this: S, baseObject: S) => T, baseObject: S = {} as S -): T { +): T & S { const obj: S & Partial = baseObject; let calculated = false; let calculating = false; diff --git a/src/util/save.ts b/src/util/save.ts index cb5c9b9..806c3fe 100644 --- a/src/util/save.ts +++ b/src/util/save.ts @@ -106,7 +106,7 @@ export async function loadSave(playerObj: Partial): Promise { for (const layer in layers) { const l = layers[layer]; - if (l) { + if (l != null) { removeLayer(l); } } diff --git a/src/util/vue.tsx b/src/util/vue.tsx index 1e2afb0..b59605d 100644 --- a/src/util/vue.tsx +++ b/src/util/vue.tsx @@ -4,122 +4,119 @@ // only apply to SFCs 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, - Visibility, - isVisible, - jsx -} from "features/feature"; -import type { ProcessedComputable } from "util/computed"; -import { DoNotCache } from "util/computed"; -import type { Component, DefineComponent, Ref, ShallowRef, UnwrapRef } from "vue"; -import { - computed, - defineComponent, - isRef, - onUnmounted, - ref, - shallowRef, - unref, - watchEffect -} from "vue"; +import { getUniqueID, Visibility } from "features/feature"; +import VueFeatureComponent from "features/VueFeature.vue"; +import { processGetter } from "util/computed"; +import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue"; +import { isRef, onUnmounted, ref, unref } from "vue"; import { JSX } from "vue/jsx-runtime"; import { camelToKebab } from "./common"; -export function coerceComponent( - component: CoercableComponent, - defaultWrapper = "span" -): DefineComponent { - if (typeof component === "function") { - return defineComponent({ render: component }); - } - if (typeof component === "string") { - if (component.length > 0) { - component = component.trim(); - if (component.charAt(0) !== "<") { - component = `<${defaultWrapper}>${component}`; - } +export const VueFeature = Symbol("VueFeature"); - return defineComponent({ template: component }); - } - return defineComponent({ render: () => ({}) }); - } - return component; +export type Renderable = JSX.Element | string; + +export interface VueFeatureOptions { + /** Whether this feature should be visible. */ + visibility?: MaybeRefOrGetter; + /** Dictionary of CSS classes to apply to this feature. */ + classes?: MaybeRefOrGetter>; + /** CSS to apply to this feature. */ + style?: MaybeRefOrGetter; } export interface VueFeature { - [ComponentKey]: GenericComponent; - [GatherProps]: () => Record; + /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ + id: string; + /** Whether this feature should be visible. */ + visibility?: MaybeRef; + /** Dictionary of CSS classes to apply to this feature. */ + classes?: MaybeRef>; + /** CSS to apply to this feature. */ + style?: MaybeRef; + /** The components to render inside the vue feature */ + components: MaybeRef[]; + /** The components to render wrapped around the vue feature */ + wrappers: ((el: () => Renderable) => Renderable)[]; + /** Used to identify Vue Features */ + [VueFeature]: true; } -export function render(object: VueFeature | CoercableComponent): JSX.Element | DefineComponent { - if (isCoercableComponent(object)) { - if (typeof object === "function") { - return (object as JSXFunction)(); - } - return coerceComponent(object); +export function vueFeatureMixin( + featureName: string, + options: VueFeatureOptions, + component?: MaybeRefOrGetter +) { + return { + id: getUniqueID(featureName), + visibility: processGetter(options.visibility), + classes: processGetter(options.classes), + style: processGetter(options.style), + components: component == null ? [] : [processGetter(component)], + wrappers: [] as ((el: () => Renderable) => Renderable)[], + [VueFeature]: true + } satisfies VueFeature; +} + +export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element; +export function render( + object: MaybeRef, + wrapper?: (el: Renderable) => T +): T; +export function render( + object: VueFeature | MaybeRef, + wrapper?: (el: Renderable) => Renderable +): Renderable; +export function render( + object: VueFeature | MaybeRef, + wrapper?: (el: Renderable) => Renderable +) { + if (typeof object === "object" && VueFeature in object) { + const { id, visibility, style, classes, components, wrappers } = object; + return ( + + ); } - const Component = object[ComponentKey]; - return ; + + object = unref(object); + return wrapper?.(object) ?? object; } -export function renderRow(...objects: (VueFeature | CoercableComponent)[]): JSX.Element { - return {objects.map(render)}; +export function renderRow(...objects: (VueFeature | MaybeRef)[]): JSX.Element { + return {objects.map(obj => render(obj))}; } -export function renderCol(...objects: (VueFeature | CoercableComponent)[]): JSX.Element { - return {objects.map(render)}; +export function renderCol(...objects: (VueFeature | MaybeRef)[]): JSX.Element { + return {objects.map(obj => render(obj))}; } -export function renderJSX(object: VueFeature | CoercableComponent): JSX.Element { - if (isCoercableComponent(object)) { - if (typeof object === "function") { - return (object as JSXFunction)(); - } - if (typeof object === "string") { - return <>{object}; - } - // TODO why is object typed as never? - const Comp = object as DefineComponent; - return ; - } - const Component = object[ComponentKey]; - return ; +export function joinJSX( + objects: (VueFeature | MaybeRef)[], + joiner: JSX.Element +): JSX.Element { + return objects.reduce( + (acc, curr) => ( + <> + {acc} + {joiner} + {render(curr)} + + ), + <> + ); } -export function renderRowJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element { - return {objects.map(renderJSX)}; -} - -export function renderColJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element { - return {objects.map(renderJSX)}; -} - -export function joinJSX(objects: JSX.Element[], joiner: JSX.Element): JSX.Element { - return objects.reduce((acc, curr) => ( - <> - {acc} - {joiner} - {curr} - - )); -} - -export function isCoercableComponent(component: unknown): component is CoercableComponent { - if (typeof component === "string") { - return true; - } else if (typeof component === "object") { - if (component == null) { - return false; - } - return "render" in component || "component" in component; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } else if (typeof component === "function" && (component as any)[DoNotCache] === true) { - return true; - } - return false; +export function isJSXElement(element: unknown): element is JSX.Element { + return ( + element != null && typeof element === "object" && "type" in element && "children" in element + ); } export function setupHoldToClick( @@ -158,61 +155,6 @@ export function setupHoldToClick( return { start, stop, handleHolding }; } -export function getFirstFeature< - T extends VueFeature & { visibility: ProcessedComputable } ->( - features: T[], - filter: (feature: T) => boolean -): { - firstFeature: Ref; - collapsedContent: JSXFunction; - hasCollapsedContent: Ref; -} { - const filteredFeatures = computed(() => - features.filter(feature => isVisible(feature.visibility) && filter(feature)) - ); - return { - firstFeature: computed(() => filteredFeatures.value[0]), - collapsedContent: jsx(() => renderCol(...filteredFeatures.value.slice(1))), - hasCollapsedContent: computed(() => filteredFeatures.value.length > 1) - }; -} - -export function computeComponent( - component: Ref, - defaultWrapper = "div" -): ShallowRef { - const comp = shallowRef(); - watchEffect(() => { - comp.value = coerceComponent(unref(component), defaultWrapper); - }); - return comp as ShallowRef; -} -export function computeOptionalComponent( - component: Ref, - defaultWrapper = "div" -): ShallowRef { - const comp = shallowRef(null); - watchEffect(() => { - const currComponent = unref(component); - comp.value = - currComponent === "" || currComponent == null - ? null - : coerceComponent(currComponent, defaultWrapper); - }); - return comp; -} - -export function deepUnref(refObject: T): { [K in keyof T]: UnwrapRef } { - return (Object.keys(refObject) as (keyof T)[]).reduce( - (acc, curr) => { - acc[curr] = unref(refObject[curr]) as UnwrapRef; - return acc; - }, - {} as { [K in keyof T]: UnwrapRef } - ); -} - export function setRefValue(ref: Ref>, value: T) { if (isRef(ref.value)) { ref.value.value = value; @@ -232,12 +174,10 @@ export type PropTypes = export function trackHover(element: VueFeature): Ref { const isHovered = ref(false); - const elementGatherProps = element[GatherProps].bind(element); - element[GatherProps] = () => ({ - ...elementGatherProps(), - onPointerenter: () => (isHovered.value = true), - onPointerleave: () => (isHovered.value = false) - }); + (element as unknown as { onPointerenter: VoidFunction }).onPointerenter = () => + (isHovered.value = true); + (element as unknown as { onPointerleave: VoidFunction }).onPointerleave = () => + (isHovered.value = true); return isHovered; } diff --git a/src/components/MarkNode.vue b/src/wrappers/marks/MarkNode.vue similarity index 81% rename from src/components/MarkNode.vue rename to src/wrappers/marks/MarkNode.vue index c730279..98f5218 100644 --- a/src/components/MarkNode.vue +++ b/src/wrappers/marks/MarkNode.vue @@ -1,12 +1,13 @@