diff --git a/src/data/common.tsx b/src/data/common.tsx
index 01d3662..289e111 100644
--- a/src/data/common.tsx
+++ b/src/data/common.tsx
@@ -3,7 +3,7 @@ import { Achievement } from "features/achievements/achievement";
import type { Clickable, ClickableOptions } from "features/clickables/clickable";
import { createClickable } from "features/clickables/clickable";
import { Conversion } from "features/conversion";
-import { getFirstFeature, type OptionsFunc, type Replace } from "features/feature";
+import { getFirstFeature } from "features/feature";
import { displayResource, Resource } from "features/resources/resource";
import type { Tree, TreeNode, TreeNodeOptions } from "features/trees/tree";
import { createTreeNode } from "features/trees/tree";
@@ -21,6 +21,7 @@ import { processGetter } from "util/computed";
import { render, Renderable, renderCol } from "util/vue";
import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from "vue";
import { computed, ref, unref } from "vue";
+import { JSX } from "vue/jsx-runtime";
import "./common.css";
/** An object that configures a {@link ResetButton} */
@@ -61,24 +62,37 @@ export interface ResetButtonOptions extends ClickableOptions {
* It will show how much can be converted currently, and can show when that amount will go up, as well as handle only being clickable when a sufficient amount of currency can be gained.
* Assumes this button is associated with a specific node on a tree, and triggers that tree's reset propagation.
*/
-export type ResetButton = Replace<
- Clickable,
- {
- resetDescription: MaybeRef;
- showNextAt: MaybeRef;
- minimumGain: MaybeRef;
- }
->;
+export interface ResetButton extends Clickable {
+ /** The conversion the button uses to calculate how much resources will be gained on click */
+ conversion: Conversion;
+ /** The tree this reset button is apart of */
+ tree: Tree;
+ /** The specific tree node associated with this reset button */
+ treeNode: TreeNode;
+ /**
+ * Text to display on low conversion amounts, describing what "resetting" is in this context.
+ * Defaults to "Reset for ".
+ */
+ resetDescription?: MaybeRef;
+ /** Whether or not to show how much currency would be required to make the gain amount increase. */
+ showNextAt?: MaybeRef;
+ /**
+ * When {@link canClick} is left to its default, minimumGain is used to only enable the reset button when a sufficient amount of currency to gain is available.
+ */
+ minimumGain?: MaybeRef;
+ /** A persistent ref to track how much time has passed since the last time this tree node was reset. */
+ resetTime?: Persistent;
+}
/**
* Lazily creates a reset button with the given options.
* @param optionsFunc A function that returns the options object for this reset button.
*/
export function createResetButton(
- optionsFunc: OptionsFunc
+ optionsFunc: () => T
) {
- const resetButton = createClickable(feature => {
- const options = optionsFunc.call(feature, feature);
+ const resetButton = createClickable(() => {
+ const options = optionsFunc();
const {
conversion,
tree,
@@ -113,41 +127,43 @@ export function createResetButton (
-
- {unref(resetButton.resetDescription)}
-
- {displayResource(
- conversion.gainResource,
- Decimal.max(
- unref(conversion.actualGain),
- unref(resetButton.minimumGain)
- )
- )}
- {" "}
- {conversion.gainResource.displayName}
- {unref(resetButton.showNextAt as MaybeRef) != null ? (
-
+ ) : null}
+
+ )
+ ),
+ onClick: function (e?: MouseEvent | TouchEvent) {
if (unref(resetButton.canClick) === false) {
return;
}
conversion.convert();
- tree.reset(resetButton.treeNode);
+ tree.reset(treeNode);
if (resetTime) {
resetTime.value = resetTime[DefaultValue];
}
@@ -173,21 +189,23 @@ export interface LayerTreeNodeOptions extends TreeNodeOptions {
}
/** A tree node that is associated with a given layer, and which opens the layer when clicked. */
-export type LayerTreeNode = Replace<
- TreeNode,
- {
- layerID: string;
- append: MaybeRef;
- }
->;
+export interface LayerTreeNode extends TreeNode {
+ /** The ID of the layer this tree node is associated with */
+ layerID: string;
+ /** Whether or not to append the layer to the tabs list.
+ * If set to false, then the tree node will instead always remove all tabs to its right and then add the layer tab.
+ * Defaults to true.
+ */
+ append?: MaybeRef;
+}
/**
* Lazily creates a tree node that's associated with a specific layer, with the given options.
* @param optionsFunc A function that returns the options object for this tree node.
*/
-export function createLayerTreeNode(optionsFunc: OptionsFunc) {
- const layerTreeNode = createTreeNode(feature => {
- const options = optionsFunc.call(feature, feature);
+export function createLayerTreeNode(optionsFunc: () => T) {
+ const layerTreeNode = createTreeNode(() => {
+ const options = optionsFunc();
const { display, append, layerID, ...props } = options;
return {
diff --git a/src/data/layers/board.tsx b/src/data/layers/board.tsx
new file mode 100644
index 0000000..d663598
--- /dev/null
+++ b/src/data/layers/board.tsx
@@ -0,0 +1,424 @@
+import { createUpgrade } from "features/clickables/upgrade";
+import { createResource } from "features/resources/resource";
+import Board from "game/boards/Board.vue";
+import CircleProgress from "game/boards/CircleProgress.vue";
+import SVGNode from "game/boards/SVGNode.vue";
+import SquareProgress from "game/boards/SquareProgress.vue";
+import {
+ Draggable,
+ MakeDraggableOptions,
+ NodePosition,
+ makeDraggable,
+ placeInAvailableSpace,
+ setupActions,
+ setupDraggableNode,
+ setupUniqueIds
+} from "game/boards/board";
+import type { BaseLayer } from "game/layers";
+import { createLayer } from "game/layers";
+import { persistent } from "game/persistence";
+import { createCostRequirement } from "game/requirements";
+import { render } from "util/vue";
+import { ComponentPublicInstance, computed, ref, watch } from "vue";
+import { setupSelectable } from "../common";
+
+const board = createLayer("board", function (this: BaseLayer) {
+ type ANode = NodePosition & { id: number; links: number[]; type: "anode"; z: number };
+ type BNode = NodePosition & { id: number; links: number[]; type: "bnode"; z: number };
+ type CNode = typeof cNode & { draggable: Draggable };
+ type NodeTypes = ANode | BNode;
+
+ const board = ref>();
+
+ const { select, deselect, selected } = setupSelectable();
+ const {
+ select: selectAction,
+ deselect: deselectAction,
+ selected: selectedAction
+ } = setupSelectable();
+
+ watch(selected, selected => {
+ if (selected == null) {
+ deselectAction();
+ }
+ });
+
+ const {
+ startDrag,
+ endDrag,
+ drag,
+ nodeBeingDragged,
+ hasDragged,
+ receivingNodes,
+ receivingNode,
+ dragDelta
+ } = setupDraggableNode({
+ board,
+ getPosition(id) {
+ return nodesById.value[id] ?? (cNode as CNode).draggable.position.value;
+ },
+ setPosition(id, position) {
+ const node = nodesById.value[id] ?? (cNode as CNode).draggable.position.value;
+ node.x = position.x;
+ node.y = position.y;
+ }
+ });
+
+ // a nodes can be slotted into b nodes to draw a branch between them, with limited connections
+ // a nodes can be selected and have an action to spawn a b node, and vice versa
+ // Newly spawned nodes should find a safe spot to spawn, and display a link to their creator
+ // a nodes use all the stuff circles used to have, and b diamonds
+ // c node also exists but is a single Upgrade element that cannot be selected, but can be dragged
+ // d nodes are a performance test - 1000 simple nodes that have no interactions
+ // Make all nodes animate in (decorator? `fadeIn(feature)?)
+ const nodes = persistent<(ANode | BNode)[]>([
+ { id: 0, x: 0, y: 0, z: 0, links: [], type: "anode" }
+ ]);
+ const nodesById = computed>(() =>
+ nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {})
+ );
+ function mouseDownNode(e: MouseEvent | TouchEvent, node: NodeTypes) {
+ const oldZ = node.z;
+ nodes.value.forEach(node => {
+ if (node.z > oldZ) {
+ node.z--;
+ }
+ });
+ node.z = nextId.value;
+ if (nodeBeingDragged.value == null) {
+ startDrag(e, node.id);
+ }
+ deselect();
+ }
+ function mouseUpNode(e: MouseEvent | TouchEvent, node: NodeTypes) {
+ if (!hasDragged.value) {
+ endDrag();
+ if (typeof node.id === "number") {
+ select(node.id);
+ }
+ e.stopPropagation();
+ }
+ }
+ function translate(node: NodePosition, isDragging: boolean) {
+ let x = node.x;
+ let y = node.y;
+ if (isDragging) {
+ x += dragDelta.value.x;
+ y += dragDelta.value.y;
+ }
+ return ` translate(${x}px,${y}px)`;
+ }
+ function rotate(rotation: number) {
+ return ` rotate(${rotation}deg) `;
+ }
+ function scale(nodeOrBool: NodeTypes | boolean) {
+ const isSelected =
+ typeof nodeOrBool === "boolean" ? nodeOrBool : selected.value === nodeOrBool.id;
+ return isSelected ? " scale(1.2)" : "";
+ }
+ function opacity(node: NodeTypes) {
+ const isDragging = selected.value !== node.id && nodeBeingDragged.value === node.id;
+ if (isDragging) {
+ return "; opacity: 0.5;";
+ }
+ return "";
+ }
+ function zIndex(node: NodeTypes) {
+ if (selected.value === node.id || nodeBeingDragged.value === node.id) {
+ return "; z-index: 100000000";
+ }
+ return "; z-index: " + node.z;
+ }
+
+ const renderANode = function (node: ANode) {
+ return (
+ mouseDownNode(e, node)}
+ onMouseUp={e => mouseUpNode(e, node)}
+ >
+
+ {receivingNodes.value.includes(node.id) && (
+
+ )}
+
+
+
+ {selected.value === node.id && selectedAction.value === 0 && (
+
+ Spawn B Node
+
+ )}
+
+ A
+
+
+ );
+ };
+ const aActions = setupActions({
+ node: () => nodesById.value[selected.value ?? ""],
+ shouldShowActions: node => node.type === "anode",
+ actions(node) {
+ return [
+ p => (
+ {
+ if (selectedAction.value === 0) {
+ spawnBNode(node as ANode);
+ } else {
+ selectAction(0);
+ }
+ }}
+ >
+
+
+ add
+
+
+ )
+ ];
+ },
+ distance: 100
+ });
+ const sqrtTwo = Math.sqrt(2);
+ const renderBNode = function (node: BNode) {
+ return (
+ mouseDownNode(e, node)}
+ onMouseUp={e => mouseUpNode(e, node)}
+ >
+
+ {receivingNodes.value.includes(node.id) && (
+
+ )}
+
+
+
+ {selected.value === node.id && selectedAction.value === 0 && (
+
+ Spawn A Node
+
+ )}
+
+ B
+
+
+ );
+ };
+ const bActions = setupActions({
+ node: () => nodesById.value[selected.value ?? ""],
+ shouldShowActions: node => node.type === "bnode",
+ actions(node) {
+ return [
+ p => (
+ {
+ if (selectedAction.value === 0) {
+ spawnANode(node as BNode);
+ } else {
+ selectAction(0);
+ }
+ }}
+ >
+
+
+ add
+
+
+ )
+ ];
+ },
+ distance: 100
+ });
+ function spawnANode(parent: ANode | BNode) {
+ const node: ANode = {
+ x: parent.x,
+ y: parent.y,
+ z: nextId.value,
+ type: "anode",
+ links: [parent.id],
+ id: nextId.value
+ };
+ placeInAvailableSpace(node, nodes.value);
+ nodes.value.push(node);
+ }
+ function spawnBNode(parent: ANode | BNode) {
+ const node: BNode = {
+ x: parent.x,
+ y: parent.y,
+ z: nextId.value,
+ type: "bnode",
+ links: [parent.id],
+ id: nextId.value
+ };
+ placeInAvailableSpace(node, nodes.value);
+ nodes.value.push(node);
+ }
+
+ const points = createResource(10);
+ const cNode = createUpgrade(() => ({
+ display:
C
,
+ // Purposefully not using noPersist
+ requirements: createCostRequirement(() => ({ cost: 10, resource: points })),
+ style: {
+ x: "100px",
+ y: "100px",
+ "--layer-color": "var(--accent1)"
+ },
+ // no-op to prevent purchasing while dragging
+ onHold: () => {}
+ }));
+ makeDraggable>(cNode, () => ({
+ id: "cnode",
+ endDrag,
+ startDrag,
+ hasDragged,
+ nodeBeingDragged,
+ dragDelta,
+ onMouseUp: cNode.purchase
+ }));
+
+ const dNodesPerAxis = 50;
+ const dNodes = (
+ <>
+ {new Array(dNodesPerAxis * dNodesPerAxis).fill(0).map((_, i) => {
+ const x = (Math.floor(i / dNodesPerAxis) - dNodesPerAxis / 2) * 100;
+ const y = ((i % dNodesPerAxis) - dNodesPerAxis / 2) * 100;
+ return (
+
+ );
+ })}
+ >
+ );
+
+ const links = computed(() => (
+ <>
+ {nodes.value
+ .reduce(
+ (acc, curr) => [
+ ...acc,
+ ...curr.links.map(l => ({ from: curr, to: nodesById.value[l] }))
+ ],
+ [] as { from: NodeTypes; to: NodeTypes }[]
+ )
+ .map(link => (
+
+ ))}
+ >
+ ));
+
+ const nextId = setupUniqueIds(() => nodes.value);
+
+ function renderNode(node: NodeTypes | typeof cNode) {
+ if (node.type === "anode") {
+ return renderANode(node);
+ } else if (node.type === "bnode") {
+ return renderBNode(node);
+ } else {
+ return render(node);
+ }
+ }
+
+ return {
+ name: "Board",
+ color: "var(--accent1)",
+ display: () => (
+ <>
+
+
+ {dNodes}
+ {links.value}
+
+ {nodes.value.map(renderNode)}
+ {render(cNode)}
+
+ {aActions.value}
+ {bActions.value}
+
+
+ >
+ ),
+ boardNodes: nodes,
+ cNode,
+ selected: persistent(selected)
+ };
+});
+
+export default board;
diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index 13f0a1e..b129c2f 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -5,8 +5,7 @@ import { branchedResetPropagation, createTree, Tree } from "features/trees/tree"
import { globalBus } from "game/events";
import type { BaseLayer, Layer } from "game/layers";
import { createLayer } from "game/layers";
-import type { Player } from "game/player";
-import player from "game/player";
+import player, { Player } from "game/player";
import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatTime } from "util/bignum";
import { render } from "util/vue";
@@ -31,17 +30,21 @@ export const main = createLayer("main", function (this: BaseLayer) {
});
const oomps = trackOOMPS(points, pointGain);
+ // Note: Casting as generic tree to avoid recursive type definitions
const tree = createTree(() => ({
nodes: [[prestige.treeNode]],
branches: [],
onReset() {
- points.value = toRaw(this.resettingNode.value) === toRaw(prestige.treeNode) ? 0 : 10;
+ points.value = toRaw(tree.resettingNode.value) === toRaw(prestige.treeNode) ? 0 : 10;
best.value = points.value;
total.value = points.value;
},
resetPropagation: branchedResetPropagation
})) as Tree;
+ // Note: layers don't _need_ a reference to everything,
+ // but I'd recommend it over trying to remember what does and doesn't need to be included.
+ // Officially all you need are anything with persistency or that you want to access elsewhere
return {
name: "Tree",
links: tree.links,
diff --git a/src/features/achievements/Achievement.vue b/src/features/achievements/Achievement.vue
index f92635c..3184822 100644
--- a/src/features/achievements/Achievement.vue
+++ b/src/features/achievements/Achievement.vue
@@ -1,5 +1,5 @@
-
-
+
diff --git a/src/features/grids/GridCell.vue b/src/features/grids/GridCell.vue
deleted file mode 100644
index bbd1513..0000000
--- a/src/features/grids/GridCell.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/features/grids/grid.tsx b/src/features/grids/grid.tsx
index ddbbb3c..4d8c7e9 100644
--- a/src/features/grids/grid.tsx
+++ b/src/features/grids/grid.tsx
@@ -1,16 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import type { OptionsFunc, Replace } from "features/feature";
-import { Visibility } from "features/feature";
-import Grid from "features/grids/Grid.vue";
+import { getUniqueID, Visibility } from "features/feature";
import type { Persistent, State } from "game/persistence";
import { persistent } from "game/persistence";
import { isFunction } from "util/common";
-import { ProcessedRefOrGetter, processGetter } from "util/computed";
+import { processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
-import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
+import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
-import { computed, unref } from "vue";
-import GridCell from "./GridCell.vue";
+import { computed, isRef, unref } from "vue";
+import Column from "components/layout/Column.vue";
+import Row from "components/layout/Row.vue";
+import Clickable from "features/clickables/Clickable.vue";
/** A symbol used to identify {@link Grid} features. */
export const GridType = Symbol("Grid");
@@ -38,8 +38,6 @@ export interface GridCell extends VueFeature {
startState: State;
/** The persistent state of this cell. */
state: State;
- /** A header to appear at the top of the display. */
- title?: MaybeRef;
/** The main text that appears in the display. */
display: MaybeRef;
/** A function that is called when the cell is clicked. */
@@ -56,30 +54,49 @@ export interface GridOptions extends VueFeatureOptions {
rows: MaybeRefOrGetter;
/** The number of columns in the grid. */
cols: MaybeRefOrGetter;
- /** A MaybeRefOrGetter to determine the visibility of a cell. */
+ /** A getter for the visibility of a cell. */
getVisibility?: CellMaybeRefOrGetter;
- /** A MaybeRefOrGetter to determine if a cell can be clicked. */
+ /** A getter for if a cell can be clicked. */
getCanClick?: CellMaybeRefOrGetter;
- /** A MaybeRefOrGetter to get the initial persistent state of a cell. */
+ /** A getter for the initial persistent state of a cell. */
getStartState: MaybeRefOrGetter | ((row: number, col: number) => State);
- /** A MaybeRefOrGetter to get the CSS styles for a cell. */
+ /** A getter for the CSS styles for a cell. */
getStyle?: CellMaybeRefOrGetter;
- /** A MaybeRefOrGetter to get the CSS classes for a cell. */
+ /** A getter for the CSS classes for a cell. */
getClasses?: CellMaybeRefOrGetter>;
- /** A MaybeRefOrGetter to get the title component for a cell. */
- getTitle?: CellMaybeRefOrGetter>;
- /** A MaybeRefOrGetter to get the display component for a cell. */
- getDisplay: CellMaybeRefOrGetter>;
+ /** A getter for the display component for a cell. */
+ getDisplay: CellMaybeRefOrGetter | {
+ getTitle?: CellMaybeRefOrGetter;
+ getDescription: CellMaybeRefOrGetter
+ };
/** A function that is called when a cell is clicked. */
onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void;
/** A function that is called when a cell is held down. */
onHold?: (row: number, col: number, state: State) => void;
}
-/**
- * The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
- */
-export interface BaseGrid extends VueFeature {
+/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
+export interface Grid extends VueFeature {
+ /** A function that is called when a cell is clicked. */
+ onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void;
+ /** A function that is called when a cell is held down. */
+ onHold?: (row: number, col: number, state: State) => void;
+ /** A getter for determine the visibility of a cell. */
+ getVisibility?: ProcessedCellRefOrGetter;
+ /** A getter for determine if a cell can be clicked. */
+ getCanClick?: ProcessedCellRefOrGetter;
+ /** The number of rows in the grid. */
+ rows: MaybeRef;
+ /** The number of columns in the grid. */
+ cols: MaybeRef;
+ /** A getter for the initial persistent state of a cell. */
+ getStartState: MaybeRef | ((row: number, col: number) => State);
+ /** A getter for the CSS styles for a cell. */
+ getStyle?: ProcessedCellRefOrGetter;
+ /** A getter for the CSS classes for a cell. */
+ getClasses?: ProcessedCellRefOrGetter>;
+ /** A getter for the display component for a cell. */
+ getDisplay: ProcessedCellRefOrGetter;
/** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
getID: (row: number, col: number, state: State) => string;
/** Get the persistent state of the given cell. */
@@ -94,39 +111,18 @@ export interface BaseGrid extends VueFeature {
type: typeof GridType;
}
-/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
-export type Grid = Replace<
- Replace,
- {
- getVisibility: ProcessedCellRefOrGetter;
- getCanClick: ProcessedCellRefOrGetter;
- rows: ProcessedRefOrGetter;
- cols: ProcessedRefOrGetter;
- getStartState: MaybeRef | ((row: number, col: number) => State);
- getStyle: ProcessedCellRefOrGetter;
- getClasses: ProcessedCellRefOrGetter;
- getTitle: ProcessedCellRefOrGetter;
- getDisplay: ProcessedCellRefOrGetter;
- }
->;
-
function getCellRowHandler(grid: Grid, row: number) {
return new Proxy({} as GridCell[], {
get(target, key) {
- if (key === "isProxy") {
- return true;
- }
-
- if (typeof key !== "string") {
- return;
- }
-
if (key === "length") {
return unref(grid.cols);
}
+ if (typeof key !== "number" && typeof key !== "string") {
+ return;
+ }
- const keyNum = parseInt(key);
- if (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols)) {
+ const keyNum = typeof key === "number" ? key : parseInt(key);
+ if (Number.isFinite(keyNum) && keyNum < unref(grid.cols)) {
if (keyNum in target) {
return target[keyNum];
}
@@ -144,20 +140,20 @@ function getCellRowHandler(grid: Grid, row: number) {
if (key === "length") {
return true;
}
- if (typeof key !== "string") {
+ if (typeof key !== "number" && typeof key !== "string") {
return false;
}
- const keyNum = parseInt(key);
+ const keyNum = typeof key === "number" ? key : parseInt(key);
if (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols)) {
return false;
}
return true;
},
getOwnPropertyDescriptor(target, key) {
- if (typeof key !== "string") {
+ if (typeof key !== "number" && typeof key !== "string") {
return;
}
- const keyNum = parseInt(key);
+ const keyNum = typeof key === "number" ? key : parseInt(key);
if (key !== "length" && (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols))) {
return;
}
@@ -200,8 +196,6 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
// The typing in this function is absolutely atrocious in order to support custom properties
get(target, key, receiver) {
switch (key) {
- case "isProxy":
- return true;
case "wrappers":
return [];
case VueFeature:
@@ -219,32 +213,28 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
case "state": {
return grid.getState(row, col);
}
+ case "id":
+ return target.id = target.id ?? getUniqueID("gridcell");
case "components":
return [
computed(() => (
-
))
];
}
-
- let prop = (grid as any)[key];
-
- if (isFunction(prop)) {
- return () => prop.call(receiver, row, col, grid.getState(row, col));
- }
- if (prop != null || typeof key === "symbol") {
- return prop;
+
+ if (typeof key === "symbol") {
+ return (grid as any)[key];
}
key = key.slice(0, 1).toUpperCase() + key.slice(1);
- prop = (grid as any)[`get${key}`];
+ let prop = (grid as any)[`get${key}`];
if (isFunction(prop)) {
if (!(key in cache)) {
cache[key] = computed(() =>
@@ -263,14 +253,24 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
return prop;
}
+ // Revert key change
+ key = key.slice(0, 1).toLowerCase() + key.slice(1);
+ prop = (grid as any)[key];
+
+ if (isFunction(prop)) {
+ return () => prop.call(receiver, row, col, grid.getState(row, col));
+ }
+
return (grid as any)[key];
},
set(target, key, value) {
+ console.log("!!?", key, value)
if (typeof key !== "string") {
return false;
}
key = `set${key.slice(0, 1).toUpperCase() + key.slice(1)}`;
- if (key in grid && isFunction((grid as any)[key]) && (grid as any)[key].length < 3) {
+ console.log(key, grid[key])
+ if (key in grid && isFunction((grid as any)[key]) && (grid as any)[key].length <= 3) {
(grid as any)[key].call(grid, row, col, value);
return true;
} else {
@@ -296,6 +296,12 @@ function getCellHandler(grid: Grid, row: number, col: number): GridCell {
});
}
+function convertCellMaybeRefOrGetter(
+ value: NonNullable>
+): ProcessedCellRefOrGetter;
+function convertCellMaybeRefOrGetter(
+ value: CellMaybeRefOrGetter | undefined
+): ProcessedCellRefOrGetter | undefined;
function convertCellMaybeRefOrGetter(
value: CellMaybeRefOrGetter
): ProcessedCellRefOrGetter {
@@ -309,10 +315,10 @@ function convertCellMaybeRefOrGetter(
* Lazily creates a grid with the given options.
* @param optionsFunc Grid options.
*/
-export function createGrid(optionsFunc: OptionsFunc) {
+export function createGrid(optionsFunc: () => T) {
const cellState = persistent>>({}, false);
- return createLazyProxy(feature => {
- const options = optionsFunc.call(feature, feature as Grid);
+ return createLazyProxy(() => {
+ const options = optionsFunc();
const {
rows,
cols,
@@ -321,40 +327,57 @@ export function createGrid(optionsFunc: OptionsFunc
+ {title}
+ {description}
+ >;
+ }
+ } else {
+ getDisplay = convertCellMaybeRefOrGetter(_getDisplay);
+ }
+
const grid = {
type: GridType,
...(props as Omit),
...vueFeatureMixin("grid", options, () => (
-
- )),
+
+ {new Array(unref(grid.rows)).fill(0).map((_, row) => (
+
+ {new Array(unref(grid.cols)).fill(0).map((_, col) =>
+ render(grid.cells[row][col]))}
+ ))}
+ )),
cellState,
- cells: new Proxy({} as Record, {
+ cells: new Proxy({} as GridCell[][], {
get(target, key: PropertyKey) {
- if (key === "isProxy") {
- return true;
- }
-
if (key === "length") {
return unref(grid.rows);
}
- if (typeof key !== "string") {
+ if (typeof key !== "number" && typeof key !== "string") {
return;
}
- const keyNum = parseInt(key);
- if (!Number.isFinite(keyNum) || keyNum >= unref(grid.rows)) {
- if (keyNum in target) {
- return target[keyNum];
+ const keyNum = typeof key === "number" ? key : parseInt(key);
+ if (Number.isFinite(keyNum) && keyNum < unref(grid.rows)) {
+ if (!(keyNum in target)) {
+ target[keyNum] = getCellRowHandler(grid, keyNum);
}
- return (target[keyNum] = getCellRowHandler(grid, keyNum));
+ return target[keyNum];
}
},
set(target, key, value) {
@@ -368,20 +391,20 @@ export function createGrid(optionsFunc: OptionsFunc= unref(grid.rows)) {
return false;
}
return true;
},
getOwnPropertyDescriptor(target, key) {
- if (typeof key !== "string") {
+ if (typeof key !== "number" && typeof key !== "string") {
return;
}
- const keyNum = parseInt(key);
+ const keyNum = typeof key === "number" ? key : parseInt(key);
if (
key !== "length" &&
(!Number.isFinite(keyNum) || keyNum >= unref(grid.rows))
@@ -399,15 +422,16 @@ export function createGrid(optionsFunc: OptionsFunc 0 ?
+ getStartState : processGetter(getStartState),
getStyle: convertCellMaybeRefOrGetter(getStyle),
getClasses: convertCellMaybeRefOrGetter(getClasses),
- getTitle: convertCellMaybeRefOrGetter(getTitle),
- getDisplay: convertCellMaybeRefOrGetter(getDisplay),
+ getDisplay,
getID: function (row: number, col: number): string {
return grid.id + "-" + row + "-" + col;
},
getState: function (row: number, col: number): State {
+ cellState.value[row] ??= {};
if (cellState.value[row][col] != null) {
return cellState.value[row][col];
}
@@ -421,7 +445,7 @@ export function createGrid(optionsFunc: OptionsFunc(optionsFunc: OptionsFunc = shallowReactive({});
@@ -33,34 +27,29 @@ export interface HotkeyOptions {
onPress: (e?: MouseEvent | TouchEvent) => void;
}
-/**
- * The properties that are added onto a processed {@link HotkeyOptions} to create an {@link Hotkey}.
- */
-export interface BaseHotkey {
+/** An object that represents a hotkey shortcut that performs an action upon a key sequence being pressed. */
+export interface Hotkey {
+ /** Whether or not this hotkey is currently enabled. */
+ enabled: MaybeRef;
+ /** The key tied to this hotkey */
+ key: string;
+ /** The description of this hotkey, to display in the settings. */
+ description: MaybeRef;
+ /** What to do upon pressing the key. */
+ onPress: (e?: MouseEvent | TouchEvent) => void;
/** A symbol that helps identify features of the same type. */
type: typeof HotkeyType;
}
-/** An object that represents a hotkey shortcut that performs an action upon a key sequence being pressed. */
-export type Hotkey = Replace<
- Replace,
- {
- enabled: MaybeRef;
- description: UnwrapRef;
- }
->;
-
const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
/**
* Lazily creates a hotkey with the given options.
* @param optionsFunc Hotkey options.
*/
-export function createHotkey(
- optionsFunc: OptionsFunc
-) {
- return createLazyProxy(feature => {
- const options = optionsFunc.call(feature, feature as Hotkey);
+export function createHotkey(optionsFunc: () => T) {
+ return createLazyProxy(() => {
+ const options = optionsFunc();
const { enabled, description, key, onPress, ...props } = options;
const hotkey = {
@@ -119,7 +108,7 @@ document.onkeydown = function (e) {
keysToCheck.push("ctrl+" + e.key);
}
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
- if (hotkey != null && unref(hotkey.enabled)) {
+ if (hotkey != null && unref(hotkey.enabled) !== false) {
e.preventDefault();
hotkey.onPress();
}
diff --git a/src/features/infoboxes/Infobox.vue b/src/features/infoboxes/Infobox.vue
index 8f4d018..39f875c 100644
--- a/src/features/infoboxes/Infobox.vue
+++ b/src/features/infoboxes/Infobox.vue
@@ -15,8 +15,8 @@
-
-
+
+
@@ -79,6 +79,8 @@ const stacked = computed(() => themes[settings.theme].mergeAdjacent);
width: auto;
text-align: left;
padding-left: 30px;
+ border-radius: 0;
+ margin: 00;
}
.infobox:not(.stacked) .title {
@@ -117,21 +119,15 @@ const stacked = computed(() => themes[settings.theme].mergeAdjacent);
.body {
transition-duration: 0.5s;
- border-radius: 5px;
- border-top-left-radius: 0;
+ padding: 8px;
+ width: 100%;
+ display: block;
+ box-sizing: border-box;
+ background-color: var(--background);
+ border-radius: 0 0 var(--feature-margin) var(--feature-margin);
}
.infobox:not(.stacked) .body {
padding: 4px;
}
-
-.body > * {
- padding: 8px;
- width: 100%;
- display: block;
- box-sizing: border-box;
- border-radius: 5px;
- border-top-left-radius: 0;
- background-color: var(--background);
-}
diff --git a/src/features/infoboxes/infobox.tsx b/src/features/infoboxes/infobox.tsx
index d9aec4c..ccf676c 100644
--- a/src/features/infoboxes/infobox.tsx
+++ b/src/features/infoboxes/infobox.tsx
@@ -1,11 +1,10 @@
-import type { OptionsFunc, Replace } from "features/feature";
import Infobox from "features/infoboxes/Infobox.vue";
import type { Persistent } from "game/persistence";
import { persistent } from "game/persistence";
-import { ProcessedRefOrGetter, processGetter } from "util/computed";
+import { processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
-import { CSSProperties, MaybeRefOrGetter } from "vue";
+import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "vue";
/** A symbol used to identify {@link Infobox} features. */
export const InfoboxType = Symbol("Infobox");
@@ -14,7 +13,7 @@ export const InfoboxType = Symbol("Infobox");
* An object that configures an {@link Infobox}.
*/
export interface InfoboxOptions extends VueFeatureOptions {
- /** The background color of the Infobox. */
+ /** The background color of the Infobox. Defaults to the layer color. */
color?: MaybeRefOrGetter;
/** CSS to apply to the title of the infobox. */
titleStyle?: MaybeRefOrGetter;
@@ -26,38 +25,32 @@ export interface InfoboxOptions extends VueFeatureOptions {
display: MaybeRefOrGetter;
}
-/**
- * The properties that are added onto a processed {@link InfoboxOptions} to create an {@link Infobox}.
- */
-export interface BaseInfobox extends VueFeature {
+/** An object that represents a feature that displays information in a collapsible way. */
+export interface Infobox extends VueFeature {
+ /** The background color of the Infobox. */
+ color?: MaybeRef;
+ /** CSS to apply to the title of the infobox. */
+ titleStyle?: MaybeRef;
+ /** CSS to apply to the body of the infobox. */
+ bodyStyle?: MaybeRef;
+ /** A header to appear at the top of the display. */
+ title: MaybeRef;
+ /** The main text that appears in the display. */
+ display: MaybeRef;
/** Whether or not this infobox is collapsed. */
collapsed: Persistent;
/** A symbol that helps identify features of the same type. */
type: typeof InfoboxType;
}
-/** An object that represents a feature that displays information in a collapsible way. */
-export type Infobox = Replace<
- Replace,
- {
- color: ProcessedRefOrGetter;
- titleStyle: ProcessedRefOrGetter;
- bodyStyle: ProcessedRefOrGetter;
- title: ProcessedRefOrGetter;
- display: ProcessedRefOrGetter;
- }
->;
-
/**
* Lazily creates an infobox with the given options.
* @param optionsFunc Infobox options.
*/
-export function createInfobox(
- optionsFunc: OptionsFunc
-) {
+export function createInfobox(optionsFunc: () => T) {
const collapsed = persistent(false, false);
- return createLazyProxy(feature => {
- const options = optionsFunc.call(feature, feature as Infobox);
+ return createLazyProxy(() => {
+ const options = optionsFunc();
const { color, titleStyle, bodyStyle, title, display, ...props } = options;
const infobox = {
@@ -74,7 +67,7 @@ export function createInfobox(
/>
)),
collapsed,
- color: processGetter(color),
+ color: processGetter(color) ?? "--layer-color",
titleStyle: processGetter(titleStyle),
bodyStyle: processGetter(bodyStyle),
title: processGetter(title),
diff --git a/src/features/links/Links.vue b/src/features/links/Links.vue
index 7053da4..c33b8d7 100644
--- a/src/features/links/Links.vue
+++ b/src/features/links/Links.vue
@@ -15,22 +15,30 @@
diff --git a/src/features/links/links.tsx b/src/features/links/links.tsx
index 73ae7d0..ef6bdcb 100644
--- a/src/features/links/links.tsx
+++ b/src/features/links/links.tsx
@@ -1,16 +1,15 @@
-import type { OptionsFunc, Replace } from "features/feature";
import type { Position } from "game/layers";
import { processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
-import { VueFeature, vueFeatureMixin } from "util/vue";
-import type { MaybeRef, MaybeRefOrGetter, SVGAttributes } from "vue";
+import { VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
+import { unref, type MaybeRef, type MaybeRefOrGetter, type SVGAttributes } from "vue";
import Links from "./Links.vue";
/** A symbol used to identify {@link Links} features. */
export const LinksType = Symbol("Links");
/** Represents a link between two nodes. It will be displayed as an SVG line, and can take any appropriate properties for an SVG line element. */
-export interface Link extends SVGAttributes {
+export interface Link extends /* @vue-ignore */ SVGAttributes {
startNode: { id: string };
endNode: { id: string };
offsetStart?: Position;
@@ -18,40 +17,35 @@ export interface Link extends SVGAttributes {
}
/** An object that configures a {@link Links}. */
-export interface LinksOptions {
+export interface LinksOptions extends VueFeatureOptions {
/** The list of links to display. */
links: MaybeRefOrGetter;
}
-/**
- * The properties that are added onto a processed {@link LinksOptions} to create an {@link Links}.
- */
-export interface BaseLinks extends VueFeature {
+/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
+export interface Links extends VueFeature {
+ /** The list of links to display. */
+ links: MaybeRef;
/** A symbol that helps identify features of the same type. */
type: typeof LinksType;
}
-/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
-export type Links = Replace<
- Replace,
- {
- links: MaybeRef;
- }
->;
-
/**
* Lazily creates links with the given options.
* @param optionsFunc Links options.
*/
-export function createLinks(optionsFunc: OptionsFunc) {
- return createLazyProxy(feature => {
- const options = optionsFunc?.call(feature, feature as Links);
- const { links, ...props } = options;
+export function createLinks(optionsFunc: () => T) {
+ return createLazyProxy(() => {
+ const options = optionsFunc?.();
+ const { links, style: _style, ...props } = options;
+
+ const style = processGetter(_style);
+ options.style = () => ({ position: "static", ...(unref(style) ?? {}) });
const retLinks = {
type: LinksType,
...(props as Omit),
- ...vueFeatureMixin("links", {}, () => ),
+ ...vueFeatureMixin("links", options, () => ),
links: processGetter(links)
} satisfies Links;
diff --git a/src/features/particles/Particles.vue b/src/features/particles/Particles.vue
index a4c64ad..b7426f2 100644
--- a/src/features/particles/Particles.vue
+++ b/src/features/particles/Particles.vue
@@ -24,7 +24,6 @@ const resizeObserver = new ResizeObserver(updateBounds);
const resizeListener = shallowRef(null);
onMounted(() => {
- // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
const resListener = resizeListener.value;
if (resListener != null) {
resizeObserver.observe(resListener);
diff --git a/src/features/particles/particles.tsx b/src/features/particles/particles.tsx
index be4480a..a933a20 100644
--- a/src/features/particles/particles.tsx
+++ b/src/features/particles/particles.tsx
@@ -1,8 +1,6 @@
import { Application } from "@pixi/app";
import type { EmitterConfigV3 } from "@pixi/particle-emitter";
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
-import type { OptionsFunc, Replace } from "features/feature";
-import { ProcessedRefOrGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
import { Ref, shallowRef } from "vue";
@@ -22,9 +20,14 @@ export interface ParticlesOptions extends VueFeatureOptions {
}
/**
- * The properties that are added onto a processed {@link ParticlesOptions} to create an {@link Particles}.
+ * An object that represents a feature that display particle effects on the screen.
+ * The config should typically be gotten by designing the effect using the [online particle effect editor](https://pixijs.io/pixi-particles-editor/) and passing it into the {@link upgradeConfig} from @pixi/particle-emitter.
*/
-export interface BaseParticles extends VueFeature {
+export interface Particles extends VueFeature {
+ /** A function that is called when the particles canvas is resized. */
+ onContainerResized?: (boundingRect: DOMRect) => void;
+ /** A function that is called whenever the particles element is reloaded during development. For restarting particle effects. */
+ onHotReload?: VoidFunction;
/** The Pixi.JS Application powering this particles canvas. */
app: Ref;
/**
@@ -37,27 +40,13 @@ export interface BaseParticles extends VueFeature {
type: typeof ParticlesType;
}
-/**
- * An object that represents a feature that display particle effects on the screen.
- * The config should typically be gotten by designing the effect using the [online particle effect editor](https://pixijs.io/pixi-particles-editor/) and passing it into the {@link upgradeConfig} from @pixi/particle-emitter.
- */
-export type Particles = Replace<
- Replace,
- {
- classes: ProcessedRefOrGetter;
- style: ProcessedRefOrGetter;
- }
->;
-
/**
* Lazily creates particles with the given options.
* @param optionsFunc Particles options.
*/
-export function createParticles(
- optionsFunc?: OptionsFunc
-) {
- return createLazyProxy(feature => {
- const options = optionsFunc?.call(feature, feature as Particles) ?? ({} as T);
+export function createParticles(optionsFunc?: () => T) {
+ return createLazyProxy(() => {
+ const options = optionsFunc?.() ?? ({} as T);
const { onContainerResized, onHotReload, ...props } = options;
let emittersToAdd: {
diff --git a/src/features/reset.ts b/src/features/reset.ts
index c075ec3..c9c7f06 100644
--- a/src/features/reset.ts
+++ b/src/features/reset.ts
@@ -1,4 +1,3 @@
-import type { OptionsFunc, Replace } from "features/feature";
import { globalBus } from "game/events";
import Formula from "game/formulas/formulas";
import type { BaseLayer } from "game/layers";
@@ -11,9 +10,9 @@ import {
} from "game/persistence";
import type { Unsubscribe } from "nanoevents";
import Decimal from "util/bignum";
-import { processGetter, type MaybeRefOrGetter, type UnwrapRef } from "util/computed";
+import { processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
-import { isRef, unref } from "vue";
+import { isRef, MaybeRef, MaybeRefOrGetter, unref } from "vue";
/** A symbol used to identify {@link Reset} features. */
export const ResetType = Symbol("Reset");
@@ -28,31 +27,25 @@ export interface ResetOptions {
onReset?: VoidFunction;
}
-/**
- * The properties that are added onto a processed {@link ResetOptions} to create an {@link Reset}.
- */
-export interface BaseReset {
+/** An object that represents a reset mechanic, which resets progress back to its initial state. */
+export interface Reset {
+ /** List of things to reset. Can include objects which will be recursed over for persistent values. */
+ thingsToReset: MaybeRef;
+ /** A function that is called when the reset is performed. */
+ onReset?: VoidFunction;
/** Trigger the reset. */
reset: VoidFunction;
/** A symbol that helps identify features of the same type. */
type: typeof ResetType;
}
-/** An object that represents a reset mechanic, which resets progress back to its initial state. */
-export type Reset = Replace<
- Replace,
- {
- thingsToReset: UnwrapRef;
- }
->;
-
/**
* Lazily creates a reset with the given options.
* @param optionsFunc Reset options.
*/
-export function createReset(optionsFunc: OptionsFunc) {
- return createLazyProxy(feature => {
- const options = optionsFunc.call(feature, feature as Reset);
+export function createReset(optionsFunc: () => T) {
+ return createLazyProxy(() => {
+ const options = optionsFunc();
const { thingsToReset, onReset, ...props } = options;
const reset = {
diff --git a/src/features/resources/MainDisplay.vue b/src/features/resources/MainDisplay.vue
index 2421420..d8225f7 100644
--- a/src/features/resources/MainDisplay.vue
+++ b/src/features/resources/MainDisplay.vue
@@ -3,16 +3,12 @@