From 424bde0cddc9b20f1d57e2ad5af94fefee1669b5 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 28 Feb 2024 23:19:11 -0600 Subject: [PATCH 1/6] WIP on rewriting board --- src/data/common.css | 9 + src/data/projEntry.tsx | 417 +++++++++++++--- src/features/boards/Board.vue | 321 +++--------- src/features/boards/BoardLink.vue | 80 --- src/features/boards/BoardNode.vue | 339 ------------- src/features/boards/BoardNodeAction.vue | 109 ---- src/features/boards/CircleProgress.vue | 29 ++ src/features/boards/SVGNode.vue | 27 + src/features/boards/SquareProgress.vue | 30 ++ src/features/boards/board.ts | 631 ------------------------ src/features/boards/board.tsx | 317 ++++++++++++ 11 files changed, 820 insertions(+), 1489 deletions(-) delete mode 100644 src/features/boards/BoardLink.vue delete mode 100644 src/features/boards/BoardNode.vue delete mode 100644 src/features/boards/BoardNodeAction.vue create mode 100644 src/features/boards/CircleProgress.vue create mode 100644 src/features/boards/SVGNode.vue create mode 100644 src/features/boards/SquareProgress.vue delete mode 100644 src/features/boards/board.ts create mode 100644 src/features/boards/board.tsx diff --git a/src/data/common.css b/src/data/common.css index 728c160..1d13f96 100644 --- a/src/data/common.css +++ b/src/data/common.css @@ -7,3 +7,12 @@ .modifier-toggle.collapsed { transform: translate(-5px, -5px) rotate(-90deg); } + +.node-text { + text-anchor: middle; + dominant-baseline: middle; + font-family: monospace; + font-size: 200%; + pointer-events: none; + filter: drop-shadow(3px 3px 2px var(--tooltip-background)); +} diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index f69ac8b..d256f4e 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -1,92 +1,371 @@ -import Node from "components/Node.vue"; -import Spacer from "components/layout/Spacer.vue"; +import Board from "features/boards/Board.vue"; +import CircleProgress from "features/boards/CircleProgress.vue"; +import SVGNode from "features/boards/SVGNode.vue"; +import SquareProgress from "features/boards/SquareProgress.vue"; +import { + NodePosition, + placeInAvailableSpace, + setupActions, + setupDraggableNode, + setupSelectable, + setupUniqueIds +} from "features/boards/board"; import { jsx } from "features/feature"; -import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource"; -import type { GenericTree } from "features/trees/tree"; -import { branchedResetPropagation, createTree } from "features/trees/tree"; -import { globalBus } from "game/events"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; +import { persistent } from "game/persistence"; import type { Player } from "game/player"; -import player from "game/player"; -import type { DecimalSource } from "util/bignum"; -import Decimal, { format, formatTime } from "util/bignum"; -import { render } from "util/vue"; -import { computed, toRaw } from "vue"; +import { ComponentPublicInstance, computed, ref, watch } from "vue"; import prestige from "./layers/prestige"; +type ANode = NodePosition & { id: number; links: number[]; type: "anode" }; +type BNode = NodePosition & { id: number; links: number[]; type: "bnode" }; +type NodeTypes = ANode | BNode; + /** * @hidden */ export const main = createLayer("main", function (this: BaseLayer) { - const points = createResource(10); - const best = trackBest(points); - const total = trackTotal(points); + const board = ref>(); - const pointGain = computed(() => { - // eslint-disable-next-line prefer-const - let gain = new Decimal(1); - return gain; - }); - globalBus.on("update", diff => { - points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff)); - }); - const oomps = trackOOMPS(points, pointGain); + const { select, deselect, selected } = setupSelectable(); + const { + select: selectAction, + deselect: deselectAction, + selected: selectedAction + } = setupSelectable(); - const tree = createTree(() => ({ - nodes: [[prestige.treeNode]], - branches: [], - onReset() { - points.value = toRaw(this.resettingNode.value) === toRaw(prestige.treeNode) ? 0 : 10; - best.value = points.value; - total.value = points.value; + watch(selected, selected => { + if (selected == null) { + deselectAction(); + } + }); + + const { + startDrag, + endDrag, + drag, + nodeBeingDragged, + hasDragged, + receivingNodes, + receivingNode, + dragDelta + } = setupDraggableNode({ + board, + isDraggable: function (node) { + return nodes.value.includes(node); + } + }); + + // 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([{ id: 0, x: 0, y: 0, links: [], type: "anode" }]); + const nodesById = computed>(() => + nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {}) + ); + function mouseDownNode(e: MouseEvent | TouchEvent, node: NodeTypes) { + if (nodeBeingDragged.value == null) { + startDrag(e, node); + } + deselect(); + } + function mouseUpNode(e: MouseEvent | TouchEvent, node: NodeTypes) { + if (!hasDragged.value) { + endDrag(); + select(node); + e.stopPropagation(); + } + } + function getTranslateString(node: NodePosition, overrideSelected?: boolean) { + const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; + const isDragging = !isSelected && nodeBeingDragged.value === node; + 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 getRotateString(rotation: number) { + return ` rotate(${rotation}deg) `; + } + function getScaleString(node: NodePosition, overrideSelected?: boolean) { + const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; + return isSelected ? " scale(1.2)" : ""; + } + function getOpacityString(node: NodePosition, overrideSelected?: boolean) { + const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; + const isDragging = !isSelected && nodeBeingDragged.value === node; + if (isDragging) { + return "; opacity: 0.5;"; + } + return ""; + } + + const renderANode = function (node: ANode) { + return ( + mouseDownNode(e, node)} + onMouseUp={e => mouseUpNode(e, node)} + > + + {receivingNodes.value.includes(node) && ( + + )} + + + + {selected.value === node && selectedAction.value === 0 && ( + + Spawn B Node + + )} + + A + + + ); + }; + const aActions = setupActions({ + node: selected, + shouldShowActions: () => selected.value?.type === "anode", + actions(node) { + return [ + p => ( + { + if (selectedAction.value === 0) { + spawnBNode(node); + } else { + selectAction(0); + } + }} + > + + + add + + + ) + ]; }, - resetPropagation: branchedResetPropagation - })) as GenericTree; + 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) && ( + + )} + + + + {selected.value === node && selectedAction.value === 0 && ( + + Spawn A Node + + )} + + B + + + ); + }; + const bActions = setupActions({ + node: selected, + shouldShowActions: () => selected.value?.type === "bnode", + actions(node) { + return [ + p => ( + { + if (selectedAction.value === 0) { + spawnANode(node); + } else { + selectAction(0); + } + }} + > + + + add + + + ) + ]; + }, + distance: 100 + }); + function spawnANode(parent: NodeTypes) { + const node: ANode = { + x: parent.x, + y: parent.y, + type: "anode", + links: [parent.id], + id: nextId.value + }; + placeInAvailableSpace(node, nodes.value); + nodes.value.push(node); + } + function spawnBNode(parent: NodeTypes) { + const node: BNode = { + x: parent.x, + y: parent.y, + type: "bnode", + links: [parent.id], + id: nextId.value + }; + placeInAvailableSpace(node, nodes.value); + nodes.value.push(node); + } + + // const cNode = createUpgrade(() => ({ + // requirements: createCostRequirement(() => ({ cost: 10, resource: points })), + // style: { + // x: "100px", + // y: "100px" + // } + // })); + // makeDraggable(cNode); // TODO make decorator + + // const dNodes; + + const links = jsx(() => ( + <> + {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 filterNodes(n: NodeTypes) { + return n !== nodeBeingDragged.value && n !== selected.value; + } + + function renderNode(node: NodeTypes | undefined) { + if (node == undefined) { + return undefined; + } else if (node.type === "anode") { + return renderANode(node); + } else if (node.type === "bnode") { + return renderBNode(node); + } + } return { name: "Tree", - links: tree.links, display: jsx(() => ( <> - {player.devSpeed === 0 ? ( -
- Game Paused - -
- ) : null} - {player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? ( -
- Dev Speed: {format(player.devSpeed)}x - -
- ) : null} - {player.offlineTime != null && player.offlineTime !== 0 ? ( -
- Offline Time: {formatTime(player.offlineTime)} - -
- ) : null} -
- {Decimal.lt(points.value, "1e1000") ? You have : null} -

{format(points.value)}

- {Decimal.lt(points.value, "1e1e6") ? points : null} -
- {Decimal.gt(pointGain.value, 0) ? ( -
- ({oomps.value}) - -
- ) : null} - - {render(tree)} + + {links()} + {nodes.value.filter(filterNodes).map(renderNode)} + + {aActions()} + {bActions()} + + {renderNode(selected.value)} + {renderNode(nodeBeingDragged.value)} + )), - points, - best, - total, - oomps, - tree + boardNodes: nodes + // cNode }; }); diff --git a/src/features/boards/Board.vue b/src/features/boards/Board.vue index 218b302..0ded9a1 100644 --- a/src/features/boards/Board.vue +++ b/src/features/boards/Board.vue @@ -1,278 +1,75 @@ + + diff --git a/src/features/boards/BoardLink.vue b/src/features/boards/BoardLink.vue deleted file mode 100644 index 5dacc66..0000000 --- a/src/features/boards/BoardLink.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/src/features/boards/BoardNode.vue b/src/features/boards/BoardNode.vue deleted file mode 100644 index 6a32f37..0000000 --- a/src/features/boards/BoardNode.vue +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - diff --git a/src/features/boards/BoardNodeAction.vue b/src/features/boards/BoardNodeAction.vue deleted file mode 100644 index c65727a..0000000 --- a/src/features/boards/BoardNodeAction.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - diff --git a/src/features/boards/CircleProgress.vue b/src/features/boards/CircleProgress.vue new file mode 100644 index 0000000..abe748d --- /dev/null +++ b/src/features/boards/CircleProgress.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/src/features/boards/SVGNode.vue b/src/features/boards/SVGNode.vue new file mode 100644 index 0000000..d36155b --- /dev/null +++ b/src/features/boards/SVGNode.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/features/boards/SquareProgress.vue b/src/features/boards/SquareProgress.vue new file mode 100644 index 0000000..7e83c5d --- /dev/null +++ b/src/features/boards/SquareProgress.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts deleted file mode 100644 index 8a9026f..0000000 --- a/src/features/boards/board.ts +++ /dev/null @@ -1,631 +0,0 @@ -import BoardComponent from "features/boards/Board.vue"; -import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; -import { - Component, - findFeatures, - GatherProps, - getUniqueID, - setDefault, - Visibility -} from "features/feature"; -import { globalBus } from "game/events"; -import { DefaultValue, deletePersistent, Persistent, State } from "game/persistence"; -import { persistent } from "game/persistence"; -import type { Unsubscribe } from "nanoevents"; -import { Direction, isFunction } from "util/common"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { processComputable } from "util/computed"; -import { createLazyProxy } from "util/proxies"; -import { computed, isRef, ref, Ref, unref } from "vue"; -import panZoom from "vue-panzoom"; -import type { Link } from "../links/links"; - -globalBus.on("setupVue", app => panZoom.install(app)); - -/** A symbol used to identify {@link Board} features. */ -export const BoardType = Symbol("Board"); - -/** - * 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. - */ -export type NodeComputable = - | Computable - | ((node: BoardNode, ...args: S) => T); - -/** Ways to display progress of an action with a duration. */ -export enum ProgressDisplay { - Outline = "Outline", - Fill = "Fill" -} - -/** Node shapes. */ -export enum Shape { - Circle = "Circle", - Diamond = "Triangle" -} - -/** An object representing a node on the board. */ -export interface BoardNode { - id: number; - position: { - x: number; - y: number; - }; - type: string; - state?: State; - pinned?: boolean; -} - -/** An object representing a link between two nodes on the board. */ -export interface BoardNodeLink extends Omit { - startNode: BoardNode; - endNode: BoardNode; - stroke: string; - strokeWidth: number; - pulsing?: boolean; -} - -/** An object representing a label for a node. */ -export interface NodeLabel { - text: string; - color?: string; - pulsing?: boolean; -} - -/** The persistent data for a board. */ -export type BoardData = { - nodes: BoardNode[]; - selectedNode: number | null; - selectedAction: string | null; -}; - -/** - * An object that configures a {@link NodeType}. - */ -export interface NodeTypeOptions { - /** The title to display for the node. */ - title: NodeComputable; - /** An optional label for the node. */ - label?: NodeComputable; - /** The size of the node - diameter for circles, width and height for squares. */ - size: NodeComputable; - /** CSS to apply to this node. */ - style?: NodeComputable; - /** Dictionary of CSS classes to apply to this node. */ - classes?: NodeComputable>; - /** Whether the node is draggable or not. */ - draggable?: NodeComputable; - /** The shape of the node. */ - shape: NodeComputable; - /** Whether the node can accept another node being dropped upon it. */ - canAccept?: NodeComputable; - /** The progress value of the node, from 0 to 1. */ - progress?: NodeComputable; - /** How the progress should be displayed on the node. */ - progressDisplay?: NodeComputable; - /** The color of the progress indicator. */ - progressColor?: NodeComputable; - /** The fill color of the node. */ - fillColor?: NodeComputable; - /** The outline color of the node. */ - outlineColor?: NodeComputable; - /** The color of the title text. */ - titleColor?: NodeComputable; - /** The list of action options for the node. */ - actions?: BoardNodeActionOptions[]; - /** The arc between each action, in radians. */ - actionDistance?: NodeComputable; - /** A function that is called when the node is clicked. */ - onClick?: (node: BoardNode) => void; - /** A function that is called when a node is dropped onto this node. */ - onDrop?: (node: BoardNode, otherNode: BoardNode) => void; - /** A function that is called for each node of this type every tick. */ - update?: (node: BoardNode, diff: number) => void; -} - -/** - * The properties that are added onto a processed {@link NodeTypeOptions} to create a {@link NodeType}. - */ -export interface BaseNodeType { - /** The nodes currently on the board of this type. */ - nodes: Ref; -} - -/** An object that represents a type of node that can appear on a board. It will handle getting properties and callbacks for every node of that type. */ -export type NodeType = Replace< - T & BaseNodeType, - { - title: GetComputableType; - label: GetComputableType; - size: GetComputableTypeWithDefault; - style: GetComputableType; - classes: GetComputableType; - draggable: GetComputableTypeWithDefault; - shape: GetComputableTypeWithDefault; - canAccept: GetComputableTypeWithDefault; - progress: GetComputableType; - progressDisplay: GetComputableTypeWithDefault; - progressColor: GetComputableTypeWithDefault; - fillColor: GetComputableType; - outlineColor: GetComputableType; - titleColor: GetComputableType; - actions?: GenericBoardNodeAction[]; - actionDistance: GetComputableTypeWithDefault; - } ->; - -/** A type that matches any valid {@link NodeType} object. */ -export type GenericNodeType = Replace< - NodeType, - { - size: NodeComputable; - draggable: NodeComputable; - shape: NodeComputable; - canAccept: NodeComputable; - progressDisplay: NodeComputable; - progressColor: NodeComputable; - actionDistance: NodeComputable; - } ->; - -/** - * An object that configures a {@link BoardNodeAction}. - */ -export interface BoardNodeActionOptions { - /** A unique identifier for the action. */ - id: string; - /** Whether this action should be visible. */ - visibility?: NodeComputable; - /** The icon to display for the action. */ - icon: NodeComputable; - /** The fill color of the action. */ - fillColor?: NodeComputable; - /** The tooltip text to display for the action. */ - tooltip: NodeComputable; - /** The confirmation label that appears under the action. */ - confirmationLabel?: NodeComputable; - /** An array of board node links associated with the action. They appear when the action is focused. */ - links?: NodeComputable; - /** A function that is called when the action is clicked. */ - onClick: (node: BoardNode) => void; -} - -/** - * The properties that are added onto a processed {@link BoardNodeActionOptions} to create an {@link BoardNodeAction}. - */ -export interface BaseBoardNodeAction { - links?: Ref; -} - -/** An object that represents an action that can be taken upon a node. */ -export type BoardNodeAction = Replace< - T & BaseBoardNodeAction, - { - visibility: GetComputableTypeWithDefault; - icon: GetComputableType; - fillColor: GetComputableType; - tooltip: GetComputableType; - confirmationLabel: GetComputableTypeWithDefault; - links: GetComputableType; - } ->; - -/** A type that matches any valid {@link BoardNodeAction} object. */ -export type GenericBoardNodeAction = Replace< - BoardNodeAction, - { - visibility: NodeComputable; - confirmationLabel: NodeComputable; - } ->; - -/** - * An object that configures a {@link Board}. - */ -export interface BoardOptions { - /** Whether this board should be visible. */ - visibility?: Computable; - /** The height of the board. Defaults to 100% */ - height?: Computable; - /** The width of the board. Defaults to 100% */ - width?: Computable; - /** Dictionary of CSS classes to apply to this feature. */ - classes?: Computable>; - /** CSS to apply to this feature. */ - style?: Computable; - /** A function that returns an array of initial board nodes, without IDs. */ - startNodes: () => Omit[]; - /** A dictionary of node types that can appear on the board. */ - types: Record; - /** The persistent state of the board. */ - state?: Computable; - /** An array of board node links to display. */ - links?: Computable; -} - -/** - * The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}. - */ -export interface BaseBoard { - /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */ - id: string; - /** All the nodes currently on the board. */ - nodes: Ref; - /** The currently selected node, if any. */ - selectedNode: Ref; - /** The currently selected action, if any. */ - selectedAction: Ref; - /** The currently being dragged node, if any. */ - draggingNode: Ref; - /** If dragging a node, the node it's currently being hovered over, if any. */ - receivingNode: Ref; - /** The current mouse position, if over the board. */ - mousePosition: Ref<{ x: number; y: number } | null>; - /** Places a node in the nearest empty space in the given direction with the specified space around it. */ - placeInAvailableSpace: (node: BoardNode, radius?: number, direction?: Direction) => void; - /** A symbol that helps identify features of the same type. */ - type: typeof BoardType; - /** 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 zoomable, pannable board with various nodes upon it. */ -export type Board = Replace< - T & BaseBoard, - { - visibility: GetComputableTypeWithDefault; - types: Record; - height: GetComputableType; - width: GetComputableType; - classes: GetComputableType; - style: GetComputableType; - state: GetComputableTypeWithDefault>; - links: GetComputableTypeWithDefault>; - } ->; - -/** A type that matches any valid {@link Board} object. */ -export type GenericBoard = Replace< - Board, - { - visibility: ProcessedComputable; - state: ProcessedComputable; - links: ProcessedComputable; - } ->; - -/** - * Lazily creates a board with the given options. - * @param optionsFunc Board options. - */ -export function createBoard( - optionsFunc: OptionsFunc -): Board { - const state = persistent( - { - nodes: [], - selectedNode: null, - selectedAction: null - }, - false - ); - - return createLazyProxy(feature => { - const board = optionsFunc.call(feature, feature); - board.id = getUniqueID("board-"); - board.type = BoardType; - board[Component] = BoardComponent as GenericComponent; - - if (board.state) { - deletePersistent(state); - processComputable(board as T, "state"); - } else { - state[DefaultValue] = { - nodes: board.startNodes().map((n, i) => { - (n as BoardNode).id = i; - return n as BoardNode; - }), - selectedNode: null, - selectedAction: null - }; - board.state = state; - } - - board.nodes = computed(() => unref(processedBoard.state).nodes); - board.selectedNode = computed({ - get() { - return ( - processedBoard.nodes.value.find( - node => node.id === unref(processedBoard.state).selectedNode - ) || null - ); - }, - set(node) { - if (isRef(processedBoard.state)) { - processedBoard.state.value = { - ...processedBoard.state.value, - selectedNode: node?.id ?? null - }; - } else { - processedBoard.state.selectedNode = node?.id ?? null; - } - } - }); - board.selectedAction = computed({ - get() { - const selectedNode = processedBoard.selectedNode.value; - if (selectedNode == null) { - return null; - } - const type = processedBoard.types[selectedNode.type]; - if (type.actions == null) { - return null; - } - return ( - type.actions.find( - action => action.id === unref(processedBoard.state).selectedAction - ) || null - ); - }, - set(action) { - if (isRef(processedBoard.state)) { - processedBoard.state.value = { - ...processedBoard.state.value, - selectedAction: action?.id ?? null - }; - } else { - processedBoard.state.selectedAction = action?.id ?? null; - } - } - }); - board.mousePosition = ref(null); - if (board.links) { - processComputable(board as T, "links"); - } else { - board.links = computed(() => { - if (processedBoard.selectedAction.value == null) { - return null; - } - if ( - processedBoard.selectedAction.value.links && - processedBoard.selectedNode.value - ) { - return getNodeProperty( - processedBoard.selectedAction.value.links, - processedBoard.selectedNode.value - ); - } - return null; - }); - } - board.draggingNode = ref(null); - board.receivingNode = ref(null); - processComputable(board as T, "visibility"); - setDefault(board, "visibility", Visibility.Visible); - processComputable(board as T, "width"); - setDefault(board, "width", "100%"); - processComputable(board as T, "height"); - setDefault(board, "height", "100%"); - processComputable(board as T, "classes"); - processComputable(board as T, "style"); - - for (const type in board.types) { - const nodeType: NodeTypeOptions & Partial = board.types[type]; - - processComputable(nodeType as NodeTypeOptions, "title"); - processComputable(nodeType as NodeTypeOptions, "label"); - processComputable(nodeType as NodeTypeOptions, "size"); - setDefault(nodeType, "size", 50); - processComputable(nodeType as NodeTypeOptions, "style"); - processComputable(nodeType as NodeTypeOptions, "classes"); - processComputable(nodeType as NodeTypeOptions, "draggable"); - setDefault(nodeType, "draggable", false); - processComputable(nodeType as NodeTypeOptions, "shape"); - setDefault(nodeType, "shape", Shape.Circle); - processComputable(nodeType as NodeTypeOptions, "canAccept"); - setDefault(nodeType, "canAccept", false); - processComputable(nodeType as NodeTypeOptions, "progress"); - processComputable(nodeType as NodeTypeOptions, "progressDisplay"); - setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); - processComputable(nodeType as NodeTypeOptions, "progressColor"); - setDefault(nodeType, "progressColor", "none"); - processComputable(nodeType as NodeTypeOptions, "fillColor"); - processComputable(nodeType as NodeTypeOptions, "outlineColor"); - processComputable(nodeType as NodeTypeOptions, "titleColor"); - processComputable(nodeType as NodeTypeOptions, "actionDistance"); - setDefault(nodeType, "actionDistance", Math.PI / 6); - nodeType.nodes = computed(() => - unref(processedBoard.state).nodes.filter(node => node.type === type) - ); - setDefault(nodeType, "onClick", function (node: BoardNode) { - unref(processedBoard.state).selectedNode = node.id; - }); - - if (nodeType.actions) { - for (const action of nodeType.actions) { - processComputable(action, "visibility"); - setDefault(action, "visibility", Visibility.Visible); - processComputable(action, "icon"); - processComputable(action, "fillColor"); - processComputable(action, "tooltip"); - processComputable(action, "confirmationLabel"); - setDefault(action, "confirmationLabel", { text: "Tap again to confirm" }); - processComputable(action, "links"); - } - } - } - - function setDraggingNode(node: BoardNode | null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - board.draggingNode!.value = node; - } - function setReceivingNode(node: BoardNode | null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - board.receivingNode!.value = node; - } - - board.placeInAvailableSpace = function ( - node: BoardNode, - radius = 100, - direction = Direction.Right - ) { - const nodes = processedBoard.nodes.value - .slice() - .filter(n => { - // Exclude self - if (n === node) { - return false; - } - - // Exclude nodes that aren't within the corridor we'll be moving within - if ( - (direction === Direction.Down || direction === Direction.Up) && - Math.abs(n.position.x - node.position.x) > radius - ) { - return false; - } - if ( - (direction === Direction.Left || direction === Direction.Right) && - Math.abs(n.position.y - node.position.y) > radius - ) { - return false; - } - - // Exclude nodes in the wrong direction - return !( - (direction === Direction.Right && - n.position.x < node.position.x - radius) || - (direction === Direction.Left && n.position.x > node.position.x + radius) || - (direction === Direction.Up && n.position.y > node.position.y + radius) || - (direction === Direction.Down && n.position.y < node.position.y - radius) - ); - }) - .sort( - direction === Direction.Right - ? (a, b) => a.position.x - b.position.x - : direction === Direction.Left - ? (a, b) => b.position.x - a.position.x - : direction === Direction.Up - ? (a, b) => b.position.y - a.position.y - : (a, b) => a.position.y - b.position.y - ); - for (let i = 0; i < nodes.length; i++) { - const nodeToCheck = nodes[i]; - const distance = - direction === Direction.Right || direction === Direction.Left - ? Math.abs(node.position.x - nodeToCheck.position.x) - : Math.abs(node.position.y - nodeToCheck.position.y); - - // If we're too close to this node, move further - if (distance < radius) { - if (direction === Direction.Right) { - node.position.x = nodeToCheck.position.x + radius; - } else if (direction === Direction.Left) { - node.position.x = nodeToCheck.position.x - radius; - } else if (direction === Direction.Up) { - node.position.y = nodeToCheck.position.y - radius; - } else if (direction === Direction.Down) { - node.position.y = nodeToCheck.position.y + radius; - } - } else if (i > 0 && distance > radius) { - // If we're further from this node than the radius, then the nodes are past us and we can early exit - break; - } - } - }; - - board[GatherProps] = function (this: GenericBoard) { - const { - nodes, - types, - state, - visibility, - width, - height, - style, - classes, - links, - selectedAction, - selectedNode, - mousePosition, - draggingNode, - receivingNode - } = this; - return { - nodes, - types, - state, - visibility, - width, - height, - style: unref(style), - classes, - links, - selectedAction, - selectedNode, - mousePosition, - draggingNode, - receivingNode, - setDraggingNode, - setReceivingNode - }; - }; - - // This is necessary because board.types is different from T and Board - const processedBoard = board as unknown as Board; - return processedBoard; - }); -} - -/** - * Gets the value of a property for a specified node. - * @param property The property to find the value of - * @param node The node to get the property of - */ -export function getNodeProperty( - property: NodeComputable, - node: BoardNode, - ...args: S -): T { - return isFunction>(property) - ? property(node, ...args) - : unref(property); -} - -/** - * Utility to get an ID for a node that is guaranteed unique. - * @param board The board feature to generate an ID for - */ -export function getUniqueNodeID(board: GenericBoard): number { - let id = 0; - board.nodes.value.forEach(node => { - if (node.id >= id) { - id = node.id + 1; - } - }); - return id; -} - -const listeners: Record = {}; -globalBus.on("addLayer", layer => { - const boards: GenericBoard[] = findFeatures(layer, BoardType) as GenericBoard[]; - listeners[layer.id] = layer.on("postUpdate", diff => { - boards.forEach(board => { - Object.values(board.types).forEach(type => - type.nodes.value.forEach(node => type.update?.(node, diff)) - ); - }); - }); -}); -globalBus.on("removeLayer", layer => { - // unsubscribe from postUpdate - listeners[layer.id]?.(); - listeners[layer.id] = undefined; -}); diff --git a/src/features/boards/board.tsx b/src/features/boards/board.tsx new file mode 100644 index 0000000..ac61615 --- /dev/null +++ b/src/features/boards/board.tsx @@ -0,0 +1,317 @@ +import Board from "features/boards/Board.vue"; +import { jsx } from "features/feature"; +import { globalBus } from "game/events"; +import type { PanZoom } from "panzoom"; +import { Direction, isFunction } from "util/common"; +import type { Computable, ProcessedComputable } from "util/computed"; +import { convertComputable } from "util/computed"; +import type { ComponentPublicInstance, Ref } from "vue"; +import { computed, ref, unref, watchEffect } from "vue"; +import panZoom from "vue-panzoom"; + +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. + */ +export type NodeComputable = + | Computable + | ((node: T, ...args: S) => R); + +/** + * Gets the value of a property for a specified node. + * @param property The property to find the value of + * @param node The node to get the property of + */ +export function unwrapNodeRef( + property: NodeComputable, + node: T, + ...args: S +): R { + return isFunction>(property) + ? property(node, ...args) + : unref(property); +} + +export function setupUniqueIds(nodes: Computable<{ id: number }[]>) { + const processedNodes = convertComputable(nodes); + return computed(() => Math.max(-1, ...unref(processedNodes).map(node => node.id)) + 1); +} + +export function setupSelectable() { + const selected = ref(); + return { + select: function (node: T) { + selected.value = node; + }, + deselect: function () { + selected.value = undefined; + }, + selected + }; +} + +export function setupDraggableNode(options: { + board: Ref | undefined>; + receivingNodes?: NodeComputable; + dropAreaRadius?: NodeComputable; + isDraggable?: NodeComputable; + onDrop?: (acceptingNode: S, draggingNode: T) => void; +}) { + const nodeBeingDragged = ref(); + const receivingNode = ref(); + const hasDragged = ref(false); + const mousePosition = ref(); + const lastMousePosition = ref({ x: 0, y: 0 }); + const dragDelta = ref({ x: 0, y: 0 }); + const isDraggable = options.isDraggable ?? true; + const receivingNodes = computed(() => + nodeBeingDragged.value == null + ? [] + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + unwrapNodeRef(options.receivingNodes ?? [], nodeBeingDragged.value!) + ); + const dropAreaRadius = options.dropAreaRadius ?? 50; + + watchEffect(() => { + if (nodeBeingDragged.value != null && !unwrapNodeRef(isDraggable, nodeBeingDragged.value)) { + result.endDrag(); + } + }); + + watchEffect(() => { + const node = nodeBeingDragged.value; + if (node == null) { + return null; + } + + const position = { + x: node.x + dragDelta.value.x, + y: node.y + dragDelta.value.y + }; + let smallestDistance = Number.MAX_VALUE; + + receivingNode.value = unref(receivingNodes).reduce((smallest: S | undefined, curr: S) => { + if ((curr as S | T) === node) { + return smallest; + } + + const distanceSquared = + Math.pow(position.x - curr.x, 2) + Math.pow(position.y - curr.y, 2); + const size = unwrapNodeRef(dropAreaRadius, curr); + if (distanceSquared > smallestDistance || distanceSquared > size * size) { + return smallest; + } + + smallestDistance = distanceSquared; + return curr; + }, undefined); + }); + + const result = { + nodeBeingDragged, + receivingNode, + hasDragged, + mousePosition, + lastMousePosition, + dragDelta, + receivingNodes, + startDrag: function (e: MouseEvent | TouchEvent, node?: T) { + e.preventDefault(); + e.stopPropagation(); + + let clientX, clientY; + if ("touches" in e) { + if (e.touches.length === 1) { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { + return; + } + } else { + clientX = e.clientX; + clientY = e.clientY; + } + lastMousePosition.value = { + x: clientX, + y: clientY + }; + dragDelta.value = { x: 0, y: 0 }; + hasDragged.value = false; + + if (node != null && unwrapNodeRef(isDraggable, node)) { + nodeBeingDragged.value = node; + } + }, + endDrag: function () { + if (nodeBeingDragged.value == null) { + return; + } + if (receivingNode.value == null) { + nodeBeingDragged.value.x += Math.round(dragDelta.value.x / 25) * 25; + nodeBeingDragged.value.y += Math.round(dragDelta.value.y / 25) * 25; + } + + if (receivingNode.value != null) { + options.onDrop?.(receivingNode.value, nodeBeingDragged.value); + } + + nodeBeingDragged.value = undefined; + }, + drag: function (e: MouseEvent | TouchEvent) { + const panZoomInstance = options.board.value?.panZoomInstance as PanZoom | undefined; + if (panZoomInstance == null) { + return; + } + + const { x, y, scale } = panZoomInstance.getTransform(); + + let clientX, clientY; + if ("touches" in e) { + if (e.touches.length === 1) { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { + result.endDrag(); + mousePosition.value = undefined; + return; + } + } else { + clientX = e.clientX; + clientY = e.clientY; + } + + mousePosition.value = { + x: (clientX - x) / scale, + y: (clientY - y) / scale + }; + + dragDelta.value = { + x: dragDelta.value.x + (clientX - lastMousePosition.value.x) / scale, + y: dragDelta.value.y + (clientY - lastMousePosition.value.y) / scale + }; + lastMousePosition.value = { + x: clientX, + y: clientY + }; + + if (Math.abs(dragDelta.value.x) > 10 || Math.abs(dragDelta.value.y) > 10) { + hasDragged.value = true; + } + + if (nodeBeingDragged.value != null) { + e.preventDefault(); + e.stopPropagation(); + } + } + }; + return result; +} + +export function setupActions(options: { + node: Computable; + shouldShowActions?: NodeComputable; + actions: NodeComputable JSX.Element)[]>; + distance: NodeComputable; + arcLength?: NodeComputable; +}) { + const node = convertComputable(options.node); + return jsx(() => { + const currNode = unref(node); + if (currNode == null) { + return ""; + } + + const actions = unwrapNodeRef(options.actions, currNode); + const shouldShow = unwrapNodeRef(options.shouldShowActions, currNode) ?? true; + if (!shouldShow) { + return <>{actions.map(f => f(currNode))}; + } + + const distance = unwrapNodeRef(options.distance, currNode); + const arcLength = unwrapNodeRef(options.arcLength, currNode) ?? Math.PI / 6; + const firstAngle = Math.PI / 2 - ((actions.length - 1) / 2) * arcLength; + return ( + <> + {actions.map((f, index) => + f({ + x: currNode.x + Math.cos(firstAngle + index * arcLength) * distance, + y: currNode.y + Math.sin(firstAngle + index * arcLength) * distance + }) + )} + + ); + }); +} + +export function placeInAvailableSpace( + nodeToPlace: T, + nodes: T[], + radius = 100, + direction = Direction.Right +) { + nodes = nodes + .filter(n => { + // Exclude self + if (n === nodeToPlace) { + return false; + } + + // Exclude nodes that aren't within the corridor we'll be moving within + if ( + (direction === Direction.Down || direction === Direction.Up) && + Math.abs(n.x - nodeToPlace.x) > radius + ) { + return false; + } + if ( + (direction === Direction.Left || direction === Direction.Right) && + Math.abs(n.y - nodeToPlace.y) > radius + ) { + return false; + } + + // Exclude nodes in the wrong direction + return !( + (direction === Direction.Right && n.x < nodeToPlace.x - radius) || + (direction === Direction.Left && n.x > nodeToPlace.x + radius) || + (direction === Direction.Up && n.y > nodeToPlace.y + radius) || + (direction === Direction.Down && n.y < nodeToPlace.y - radius) + ); + }) + .sort( + direction === Direction.Right + ? (a, b) => a.x - b.x + : direction === Direction.Left + ? (a, b) => b.x - a.x + : direction === Direction.Up + ? (a, b) => b.y - a.y + : (a, b) => a.y - b.y + ); + + for (let i = 0; i < nodes.length; i++) { + const nodeToCheck = nodes[i]; + const distance = + direction === Direction.Right || direction === Direction.Left + ? Math.abs(nodeToPlace.x - nodeToCheck.x) + : Math.abs(nodeToPlace.y - nodeToCheck.y); + + // If we're too close to this node, move further + if (distance < radius) { + if (direction === Direction.Right) { + nodeToPlace.x = nodeToCheck.x + radius; + } else if (direction === Direction.Left) { + nodeToPlace.x = nodeToCheck.x - radius; + } else if (direction === Direction.Up) { + nodeToPlace.y = nodeToCheck.y - radius; + } else if (direction === Direction.Down) { + nodeToPlace.y = nodeToCheck.y + radius; + } + } else if (i > 0 && distance > radius) { + // If we're further from this node than the radius, then the nodes are past us and we can early exit + break; + } + } +} -- 2.45.2 From 1acfde134b9e4923643a9ed1282797d4d040da5b Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 19:59:26 -0600 Subject: [PATCH 2/6] Add support for rendering VueFeatures in boards --- src/data/layers/prestige.tsx | 73 ------------- src/data/projEntry.tsx | 172 ++++++++++++++++++------------ src/features/boards/Draggable.vue | 37 +++++++ src/features/boards/board.tsx | 110 ++++++++++++++----- 4 files changed, 225 insertions(+), 167 deletions(-) delete mode 100644 src/data/layers/prestige.tsx create mode 100644 src/features/boards/Draggable.vue diff --git a/src/data/layers/prestige.tsx b/src/data/layers/prestige.tsx deleted file mode 100644 index 6e3cb69..0000000 --- a/src/data/layers/prestige.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @module - * @hidden - */ -import { main } from "data/projEntry"; -import { createCumulativeConversion } from "features/conversion"; -import { jsx } from "features/feature"; -import { createHotkey } from "features/hotkey"; -import { createReset } from "features/reset"; -import MainDisplay from "features/resources/MainDisplay.vue"; -import { createResource } from "features/resources/resource"; -import { addTooltip } from "features/tooltips/tooltip"; -import { createResourceTooltip } from "features/trees/tree"; -import { BaseLayer, createLayer } from "game/layers"; -import type { DecimalSource } from "util/bignum"; -import { render } from "util/vue"; -import { createLayerTreeNode, createResetButton } from "../common"; - -const id = "p"; -const layer = createLayer(id, function (this: BaseLayer) { - const name = "Prestige"; - const color = "#4BDC13"; - const points = createResource(0, "prestige points"); - - const conversion = createCumulativeConversion(() => ({ - formula: x => x.div(10).sqrt(), - baseResource: main.points, - gainResource: points - })); - - const reset = createReset(() => ({ - thingsToReset: (): Record[] => [layer] - })); - - const treeNode = createLayerTreeNode(() => ({ - layerID: id, - color, - reset - })); - const tooltip = addTooltip(treeNode, { - display: createResourceTooltip(points), - pinnable: true - }); - - const resetButton = createResetButton(() => ({ - conversion, - tree: main.tree, - treeNode - })); - - const hotkey = createHotkey(() => ({ - description: "Reset for prestige points", - key: "p", - onPress: resetButton.onClick - })); - - return { - name, - color, - points, - tooltip, - display: jsx(() => ( - <> - - {render(resetButton)} - - )), - treeNode, - hotkey - }; -}); - -export default layer; diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index d256f4e..6762d45 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -4,6 +4,7 @@ import SVGNode from "features/boards/SVGNode.vue"; import SquareProgress from "features/boards/SquareProgress.vue"; import { NodePosition, + makeDraggable, placeInAvailableSpace, setupActions, setupDraggableNode, @@ -11,24 +12,29 @@ import { setupUniqueIds } from "features/boards/board"; import { jsx } from "features/feature"; +import { createResource } from "features/resources/resource"; +import { createUpgrade } from "features/upgrades/upgrade"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; -import { persistent } from "game/persistence"; +import { Persistent, persistent } from "game/persistence"; import type { Player } from "game/player"; +import { createCostRequirement } from "game/requirements"; +import { render } from "util/vue"; import { ComponentPublicInstance, computed, ref, watch } from "vue"; -import prestige from "./layers/prestige"; - -type ANode = NodePosition & { id: number; links: number[]; type: "anode" }; -type BNode = NodePosition & { id: number; links: number[]; type: "bnode" }; -type NodeTypes = ANode | BNode; +import "./common.css"; /** * @hidden */ export const main = createLayer("main", function (this: BaseLayer) { + type ANode = NodePosition & { id: number; links: number[]; type: "anode" }; + type BNode = NodePosition & { id: number; links: number[]; type: "bnode" }; + type CNode = typeof cNode & { position: Persistent }; + type NodeTypes = ANode | BNode; + const board = ref>(); - const { select, deselect, selected } = setupSelectable(); + const { select, deselect, selected } = setupSelectable(); const { select: selectAction, deselect: deselectAction, @@ -50,10 +56,15 @@ export const main = createLayer("main", function (this: BaseLayer) { receivingNodes, receivingNode, dragDelta - } = setupDraggableNode({ + } = setupDraggableNode({ board, - isDraggable: function (node) { - return nodes.value.includes(node); + getPosition(id) { + return nodesById.value[id] ?? (cNode as CNode).position.value; + }, + setPosition(id, position) { + const node = nodesById.value[id] ?? (cNode as CNode).position.value; + node.x = position.x; + node.y = position.y; } }); @@ -64,26 +75,26 @@ export const main = createLayer("main", function (this: BaseLayer) { // 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([{ id: 0, x: 0, y: 0, links: [], type: "anode" }]); + const nodes = persistent<(ANode | BNode)[]>([{ id: 0, x: 0, y: 0, links: [], type: "anode" }]); const nodesById = computed>(() => nodes.value.reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {}) ); function mouseDownNode(e: MouseEvent | TouchEvent, node: NodeTypes) { if (nodeBeingDragged.value == null) { - startDrag(e, node); + startDrag(e, node.id); } deselect(); } function mouseUpNode(e: MouseEvent | TouchEvent, node: NodeTypes) { if (!hasDragged.value) { endDrag(); - select(node); + if (typeof node.id === "number") { + select(node.id); + } e.stopPropagation(); } } - function getTranslateString(node: NodePosition, overrideSelected?: boolean) { - const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; - const isDragging = !isSelected && nodeBeingDragged.value === node; + function getTranslateString(node: NodePosition, isDragging: boolean) { let x = node.x; let y = node.y; if (isDragging) { @@ -95,13 +106,13 @@ export const main = createLayer("main", function (this: BaseLayer) { function getRotateString(rotation: number) { return ` rotate(${rotation}deg) `; } - function getScaleString(node: NodePosition, overrideSelected?: boolean) { - const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; + function getScaleString(nodeOrBool: NodeTypes | boolean) { + const isSelected = + typeof nodeOrBool === "boolean" ? nodeOrBool : selected.value === nodeOrBool.id; return isSelected ? " scale(1.2)" : ""; } - function getOpacityString(node: NodePosition, overrideSelected?: boolean) { - const isSelected = overrideSelected == null ? selected.value === node : overrideSelected; - const isDragging = !isSelected && nodeBeingDragged.value === node; + function getOpacityString(node: NodeTypes) { + const isDragging = selected.value !== node.id && nodeBeingDragged.value === node.id; if (isDragging) { return "; opacity: 0.5;"; } @@ -111,16 +122,19 @@ export const main = createLayer("main", function (this: BaseLayer) { const renderANode = function (node: ANode) { return ( mouseDownNode(e, node)} onMouseUp={e => mouseUpNode(e, node)} > - - {receivingNodes.value.includes(node) && ( + + {receivingNodes.value.includes(node.id) && ( )} @@ -132,7 +146,7 @@ export const main = createLayer("main", function (this: BaseLayer) { stroke-width="4" /> - {selected.value === node && selectedAction.value === 0 && ( + {selected.value === node.id && selectedAction.value === 0 && ( Spawn B Node @@ -144,8 +158,8 @@ export const main = createLayer("main", function (this: BaseLayer) { ); }; const aActions = setupActions({ - node: selected, - shouldShowActions: () => selected.value?.type === "anode", + node: () => nodesById.value[selected.value ?? ""], + shouldShowActions: node => node.type === "anode", actions(node) { return [ p => ( @@ -153,10 +167,10 @@ export const main = createLayer("main", function (this: BaseLayer) { style={`transform: ${getTranslateString( p, selectedAction.value === 0 - )}${getScaleString(p, selectedAction.value === 0)}`} + )}${getScaleString(selectedAction.value === 0)}`} onClick={() => { if (selectedAction.value === 0) { - spawnBNode(node); + spawnBNode(node as ANode); } else { selectAction(0); } @@ -176,16 +190,15 @@ export const main = createLayer("main", function (this: BaseLayer) { const renderBNode = function (node: BNode) { return ( mouseDownNode(e, node)} onMouseUp={e => mouseUpNode(e, node)} > - - {receivingNodes.value.includes(node) && ( + + {receivingNodes.value.includes(node.id) && ( )} @@ -213,7 +226,7 @@ export const main = createLayer("main", function (this: BaseLayer) { stroke-width="4" /> - {selected.value === node && selectedAction.value === 0 && ( + {selected.value === node.id && selectedAction.value === 0 && ( Spawn A Node @@ -225,8 +238,8 @@ export const main = createLayer("main", function (this: BaseLayer) { ); }; const bActions = setupActions({ - node: selected, - shouldShowActions: () => selected.value?.type === "bnode", + node: () => nodesById.value[selected.value ?? ""], + shouldShowActions: node => node.type === "bnode", actions(node) { return [ p => ( @@ -234,10 +247,10 @@ export const main = createLayer("main", function (this: BaseLayer) { style={`transform: ${getTranslateString( p, selectedAction.value === 0 - )}${getScaleString(p, selectedAction.value === 0)}`} + )}${getScaleString(selectedAction.value === 0)}`} onClick={() => { if (selectedAction.value === 0) { - spawnANode(node); + spawnANode(node as BNode); } else { selectAction(0); } @@ -253,7 +266,7 @@ export const main = createLayer("main", function (this: BaseLayer) { }, distance: 100 }); - function spawnANode(parent: NodeTypes) { + function spawnANode(parent: ANode | BNode) { const node: ANode = { x: parent.x, y: parent.y, @@ -264,7 +277,7 @@ export const main = createLayer("main", function (this: BaseLayer) { placeInAvailableSpace(node, nodes.value); nodes.value.push(node); } - function spawnBNode(parent: NodeTypes) { + function spawnBNode(parent: ANode | BNode) { const node: BNode = { x: parent.x, y: parent.y, @@ -276,14 +289,29 @@ export const main = createLayer("main", function (this: BaseLayer) { nodes.value.push(node); } - // const cNode = createUpgrade(() => ({ - // requirements: createCostRequirement(() => ({ cost: 10, resource: points })), - // style: { - // x: "100px", - // y: "100px" - // } - // })); - // makeDraggable(cNode); // TODO make decorator + const points = createResource(10); + const cNode = createUpgrade(() => ({ + display: "

C

", + // Purposefully not using noPersist + requirements: createCostRequirement(() => ({ cost: 10, resource: points })), + style: { + x: "100px", + y: "100px" + } + })); + makeDraggable(cNode, { + id: "cnode", + endDrag, + startDrag, + hasDragged, + nodeBeingDragged, + dragDelta, + onMouseUp() { + if (!hasDragged.value) { + cNode.purchase(); + } + } + }); // const dNodes; @@ -302,22 +330,22 @@ export const main = createLayer("main", function (this: BaseLayer) { stroke="white" stroke-width={4} x1={ - nodeBeingDragged.value === link.from + nodeBeingDragged.value === link.from.id ? dragDelta.value.x + link.from.x : link.from.x } y1={ - nodeBeingDragged.value === link.from + nodeBeingDragged.value === link.from.id ? dragDelta.value.y + link.from.y : link.from.y } x2={ - nodeBeingDragged.value === link.to + nodeBeingDragged.value === link.to.id ? dragDelta.value.x + link.to.x : link.to.x } y2={ - nodeBeingDragged.value === link.to + nodeBeingDragged.value === link.to.id ? dragDelta.value.y + link.to.y : link.to.y } @@ -328,22 +356,30 @@ export const main = createLayer("main", function (this: BaseLayer) { const nextId = setupUniqueIds(() => nodes.value); - function filterNodes(n: NodeTypes) { + function filterNodes(n: number | "cnode") { return n !== nodeBeingDragged.value && n !== selected.value; } - function renderNode(node: NodeTypes | undefined) { - if (node == undefined) { + function renderNodeById(id: number | "cnode" | undefined) { + if (id == null) { return undefined; - } else if (node.type === "anode") { + } + return renderNode(nodesById.value[id] ?? cNode); + } + + 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: "Tree", + color: "var(--accent1)", display: jsx(() => ( <> {links()} - {nodes.value.filter(filterNodes).map(renderNode)} + {nodes.value.filter(n => filterNodes(n.id)).map(renderNode)} + {filterNodes("cnode") && render(cNode)} {aActions()} {bActions()} - {renderNode(selected.value)} - {renderNode(nodeBeingDragged.value)} + {renderNodeById(selected.value)} + {renderNodeById(nodeBeingDragged.value)} )), - boardNodes: nodes - // cNode + boardNodes: nodes, + cNode, + selected: persistent(selected) }; }); @@ -376,7 +414,7 @@ export const main = createLayer("main", function (this: BaseLayer) { export const getInitialLayers = ( /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ player: Partial -): Array => [main, prestige]; +): Array => [main]; /** * A computed ref whose value is true whenever the game is over. diff --git a/src/features/boards/Draggable.vue b/src/features/boards/Draggable.vue new file mode 100644 index 0000000..9d59db6 --- /dev/null +++ b/src/features/boards/Draggable.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/features/boards/board.tsx b/src/features/boards/board.tsx index ac61615..20501d8 100644 --- a/src/features/boards/board.tsx +++ b/src/features/boards/board.tsx @@ -1,12 +1,15 @@ import Board from "features/boards/Board.vue"; -import { jsx } from "features/feature"; +import Draggable from "features/boards/Draggable.vue"; +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 { VueFeature } from "util/vue"; import type { ComponentPublicInstance, Ref } from "vue"; -import { computed, ref, unref, watchEffect } from "vue"; +import { computed, nextTick, ref, unref, watchEffect } from "vue"; import panZoom from "vue-panzoom"; globalBus.on("setupVue", app => panZoom.install(app)); @@ -53,20 +56,20 @@ export function setupSelectable() { }; } -export function setupDraggableNode(options: { +export function setupDraggableNode(options: { board: Ref | undefined>; - receivingNodes?: NodeComputable; - dropAreaRadius?: NodeComputable; - isDraggable?: NodeComputable; - onDrop?: (acceptingNode: S, draggingNode: T) => void; + getPosition: (node: T) => NodePosition; + setPosition: (node: T, position: NodePosition) => void; + receivingNodes?: NodeComputable; + dropAreaRadius?: NodeComputable; + onDrop?: (acceptingNode: T, draggingNode: T) => void; }) { const nodeBeingDragged = ref(); - const receivingNode = ref(); + const receivingNode = ref(); const hasDragged = ref(false); const mousePosition = ref(); const lastMousePosition = ref({ x: 0, y: 0 }); const dragDelta = ref({ x: 0, y: 0 }); - const isDraggable = options.isDraggable ?? true; const receivingNodes = computed(() => nodeBeingDragged.value == null ? [] @@ -75,31 +78,26 @@ export function setupDraggableNode { - if (nodeBeingDragged.value != null && !unwrapNodeRef(isDraggable, nodeBeingDragged.value)) { - result.endDrag(); - } - }); - watchEffect(() => { const node = nodeBeingDragged.value; if (node == null) { return null; } + const originalPosition = options.getPosition(node); const position = { - x: node.x + dragDelta.value.x, - y: node.y + dragDelta.value.y + x: originalPosition.x + dragDelta.value.x, + y: originalPosition.y + dragDelta.value.y }; let smallestDistance = Number.MAX_VALUE; - receivingNode.value = unref(receivingNodes).reduce((smallest: S | undefined, curr: S) => { - if ((curr as S | T) === node) { + receivingNode.value = unref(receivingNodes).reduce((smallest: T | undefined, curr: T) => { + if ((curr as T) === node) { return smallest; } - const distanceSquared = - Math.pow(position.x - curr.x, 2) + Math.pow(position.y - curr.y, 2); + const { x, y } = options.getPosition(curr); + const distanceSquared = Math.pow(position.x - x, 2) + Math.pow(position.y - y, 2); const size = unwrapNodeRef(dropAreaRadius, curr); if (distanceSquared > smallestDistance || distanceSquared > size * size) { return smallest; @@ -118,7 +116,7 @@ export function setupDraggableNode( + element: T, + options: { + id: S; + nodeBeingDragged: Ref; + hasDragged: Ref; + dragDelta: Ref; + startDrag: (e: MouseEvent | TouchEvent, id: S) => void; + endDrag: VoidFunction; + onMouseDown?: (e: MouseEvent | TouchEvent) => boolean | void; + onMouseUp?: (e: MouseEvent | TouchEvent) => boolean | void; + initialPosition?: NodePosition; + } +): asserts element is T & { position: Persistent } { + const position = persistent(options.initialPosition ?? { x: 0, y: 0 }); + (element as T & { position: Persistent }).position = position; + const computedPosition = computed(() => { + if (options.nodeBeingDragged.value === options.id) { + return { + x: position.value.x + options.dragDelta.value.x, + y: position.value.y + options.dragDelta.value.y + }; + } + return position.value; + }); + + function handleMouseDown(e: MouseEvent | TouchEvent) { + if (options.onMouseDown?.(e) === false) { + return; + } + + if (options.nodeBeingDragged.value == null) { + options.startDrag(e, options.id); + } + } + + function handleMouseUp(e: MouseEvent | TouchEvent) { + 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); + }); +} + export function setupActions(options: { node: Computable; shouldShowActions?: NodeComputable; -- 2.45.2 From f0e831ee8fea2aee319f37b722593dda259b95c3 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 20:26:00 -0600 Subject: [PATCH 3/6] Add cnodes --- src/data/projEntry.tsx | 20 +++++++++++++++++++- src/features/boards/SVGNode.vue | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 6762d45..140fb55 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -313,6 +313,21 @@ export const main = createLayer("main", function (this: BaseLayer) { } }); + const dNodesPerAxis = 50; + const dNodes = jsx(() => + 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 dNodes; const links = jsx(() => ( @@ -389,7 +404,10 @@ export const main = createLayer("main", function (this: BaseLayer) { onMouseLeave={endDrag} ref={board} > - {links()} + + {dNodes()} + {links()} + {nodes.value.filter(n => filterNodes(n.id)).map(renderNode)} {filterNodes("cnode") && render(cNode)} diff --git a/src/features/boards/SVGNode.vue b/src/features/boards/SVGNode.vue index d36155b..a0d1b14 100644 --- a/src/features/boards/SVGNode.vue +++ b/src/features/boards/SVGNode.vue @@ -23,5 +23,6 @@ svg { cursor: pointer; transition-duration: 0s; overflow: visible; + position: absolute; } -- 2.45.2 From aca56f6af6e8a05e019806d7d82c5e736c590dbd Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 22:17:06 -0600 Subject: [PATCH 4/6] Use z-index to avoid changing render order --- src/data/projEntry.tsx | 106 ++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 140fb55..8a3f858 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -27,8 +27,8 @@ import "./common.css"; * @hidden */ export const main = createLayer("main", function (this: BaseLayer) { - type ANode = NodePosition & { id: number; links: number[]; type: "anode" }; - type BNode = NodePosition & { id: number; links: number[]; type: "bnode" }; + 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 & { position: Persistent }; type NodeTypes = ANode | BNode; @@ -75,11 +75,20 @@ export const main = createLayer("main", function (this: BaseLayer) { // 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, links: [], type: "anode" }]); + 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); } @@ -94,7 +103,7 @@ export const main = createLayer("main", function (this: BaseLayer) { e.stopPropagation(); } } - function getTranslateString(node: NodePosition, isDragging: boolean) { + function translate(node: NodePosition, isDragging: boolean) { let x = node.x; let y = node.y; if (isDragging) { @@ -103,33 +112,38 @@ export const main = createLayer("main", function (this: BaseLayer) { } return ` translate(${x}px,${y}px)`; } - function getRotateString(rotation: number) { + function rotate(rotation: number) { return ` rotate(${rotation}deg) `; } - function getScaleString(nodeOrBool: NodeTypes | boolean) { + function scale(nodeOrBool: NodeTypes | boolean) { const isSelected = typeof nodeOrBool === "boolean" ? nodeOrBool : selected.value === nodeOrBool.id; return isSelected ? " scale(1.2)" : ""; } - function getOpacityString(node: NodeTypes) { + 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) && ( ( { if (selectedAction.value === 0) { spawnBNode(node as ANode); @@ -190,14 +203,13 @@ export const main = createLayer("main", function (this: BaseLayer) { const renderBNode = function (node: BNode) { return ( mouseDownNode(e, node)} onMouseUp={e => mouseUpNode(e, node)} > - + {receivingNodes.value.includes(node.id) && ( ( { if (selectedAction.value === 0) { spawnANode(node as BNode); @@ -270,6 +281,7 @@ export const main = createLayer("main", function (this: BaseLayer) { const node: ANode = { x: parent.x, y: parent.y, + z: nextId.value, type: "anode", links: [parent.id], id: nextId.value @@ -281,6 +293,7 @@ export const main = createLayer("main", function (this: BaseLayer) { const node: BNode = { x: parent.x, y: parent.y, + z: nextId.value, type: "bnode", links: [parent.id], id: nextId.value @@ -314,21 +327,21 @@ export const main = createLayer("main", function (this: BaseLayer) { }); const dNodesPerAxis = 50; - const dNodes = jsx(() => - 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 dNodes; + const dNodes = jsx(() => ( + <> + {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 = jsx(() => ( <> @@ -371,17 +384,6 @@ export const main = createLayer("main", function (this: BaseLayer) { const nextId = setupUniqueIds(() => nodes.value); - function filterNodes(n: number | "cnode") { - return n !== nodeBeingDragged.value && n !== selected.value; - } - - function renderNodeById(id: number | "cnode" | undefined) { - if (id == null) { - return undefined; - } - return renderNode(nodesById.value[id] ?? cNode); - } - function renderNode(node: NodeTypes | typeof cNode) { if (node.type === "anode") { return renderANode(node); @@ -408,14 +410,12 @@ export const main = createLayer("main", function (this: BaseLayer) { {dNodes()} {links()} - {nodes.value.filter(n => filterNodes(n.id)).map(renderNode)} - {filterNodes("cnode") && render(cNode)} + {nodes.value.map(renderNode)} + {render(cNode)} {aActions()} {bActions()} - {renderNodeById(selected.value)} - {renderNodeById(nodeBeingDragged.value)} )), -- 2.45.2 From 3fd8375031de265f8cf6f307190c52986ec74fb9 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 22:17:16 -0600 Subject: [PATCH 5/6] Perf optimization --- src/features/boards/board.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/features/boards/board.tsx b/src/features/boards/board.tsx index 20501d8..b687998 100644 --- a/src/features/boards/board.tsx +++ b/src/features/boards/board.tsx @@ -160,7 +160,7 @@ export function setupDraggableNode(options: { }, drag: function (e: MouseEvent | TouchEvent) { const panZoomInstance = options.board.value?.panZoomInstance as PanZoom | undefined; - if (panZoomInstance == null) { + if (panZoomInstance == null || nodeBeingDragged.value == null) { return; } @@ -199,10 +199,8 @@ export function setupDraggableNode(options: { hasDragged.value = true; } - if (nodeBeingDragged.value != null) { - e.preventDefault(); - e.stopPropagation(); - } + e.preventDefault(); + e.stopPropagation(); } }; return result; -- 2.45.2 From 17b878e3be553d491cfa3194be4f220d054c913b Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 22:31:20 -0600 Subject: [PATCH 6/6] Fix upgrade purchasing on drag --- src/data/projEntry.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 8a3f858..2fa532d 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -309,7 +309,8 @@ export const main = createLayer("main", function (this: BaseLayer) { requirements: createCostRequirement(() => ({ cost: 10, resource: points })), style: { x: "100px", - y: "100px" + y: "100px", + pointerEvents: "none" } })); makeDraggable(cNode, { -- 2.45.2