From 1acfde134b9e4923643a9ed1282797d4d040da5b Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 3 Mar 2024 19:59:26 -0600 Subject: [PATCH] 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;