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<DecimalSource>(0, "prestige points"); - - const conversion = createCumulativeConversion(() => ({ - formula: x => x.div(10).sqrt(), - baseResource: main.points, - gainResource: points - })); - - const reset = createReset(() => ({ - thingsToReset: (): Record<string, unknown>[] => [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(() => ( - <> - <MainDisplay resource={points} color={color} /> - {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<NodePosition> }; + type NodeTypes = ANode | BNode; + const board = ref<ComponentPublicInstance<typeof Board>>(); - const { select, deselect, selected } = setupSelectable<NodeTypes>(); + const { select, deselect, selected } = setupSelectable<number>(); const { select: selectAction, deselect: deselectAction, @@ -50,10 +56,15 @@ export const main = createLayer("main", function (this: BaseLayer) { receivingNodes, receivingNode, dragDelta - } = setupDraggableNode<NodeTypes /* | typeof cNode*/>({ + } = setupDraggableNode<number | "cnode">({ 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<NodeTypes[]>([{ 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<Record<string, NodeTypes>>(() => 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 ( <SVGNode - style={`transform: ${getTranslateString(node)}${getOpacityString(node)}`} + style={`transform: ${getTranslateString( + node, + selected.value === node.id && nodeBeingDragged.value === node.id + )}${getOpacityString(node)}`} onMouseDown={e => mouseDownNode(e, node)} onMouseUp={e => mouseUpNode(e, node)} > - <g style={`transform: ${getScaleString(node)}; transition-duration: 0s`}> - {receivingNodes.value.includes(node) && ( + <g style={`transform: ${getScaleString(node)}`}> + {receivingNodes.value.includes(node.id) && ( <circle r="58" fill="var(--background)" - stroke={receivingNode.value === node ? "#0F0" : "#0F03"} + stroke={receivingNode.value === node.id ? "#0F0" : "#0F03"} stroke-width="2" /> )} @@ -132,7 +146,7 @@ export const main = createLayer("main", function (this: BaseLayer) { stroke-width="4" /> </g> - {selected.value === node && selectedAction.value === 0 && ( + {selected.value === node.id && selectedAction.value === 0 && ( <text y="140" fill="var(--foreground)" class="node-text"> Spawn B Node </text> @@ -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 ( <SVGNode - style={`transform: ${getTranslateString(node)}${getOpacityString(node)}`} + style={`transform: ${getTranslateString( + node, + selected.value === node.id && nodeBeingDragged.value === node.id + )}${getOpacityString(node)}`} onMouseDown={e => mouseDownNode(e, node)} onMouseUp={e => mouseUpNode(e, node)} > - <g - style={`transform: ${getScaleString(node)}${getRotateString( - 45 - )}; transition-duration: 0s`} - > - {receivingNodes.value.includes(node) && ( + <g style={`transform: ${getScaleString(node)}${getRotateString(45)}`}> + {receivingNodes.value.includes(node.id) && ( <rect width={50 * sqrtTwo + 16} height={50 * sqrtTwo + 16} @@ -193,7 +206,7 @@ export const main = createLayer("main", function (this: BaseLayer) { (-50 * sqrtTwo + 16) / 2 })`} fill="var(--background)" - stroke={receivingNode.value === node ? "#0F0" : "#0F03"} + stroke={receivingNode.value === node.id ? "#0F0" : "#0F03"} stroke-width="2" /> )} @@ -213,7 +226,7 @@ export const main = createLayer("main", function (this: BaseLayer) { stroke-width="4" /> </g> - {selected.value === node && selectedAction.value === 0 && ( + {selected.value === node.id && selectedAction.value === 0 && ( <text y="140" fill="var(--foreground)" class="node-text"> Spawn A Node </text> @@ -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: "<h1>C</h1>", + // 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(() => ( <> <Board @@ -354,18 +390,20 @@ export const main = createLayer("main", function (this: BaseLayer) { ref={board} > <SVGNode>{links()}</SVGNode> - {nodes.value.filter(filterNodes).map(renderNode)} + {nodes.value.filter(n => filterNodes(n.id)).map(renderNode)} + {filterNodes("cnode") && render(cNode)} <SVGNode> {aActions()} {bActions()} </SVGNode> - {renderNode(selected.value)} - {renderNode(nodeBeingDragged.value)} + {renderNodeById(selected.value)} + {renderNodeById(nodeBeingDragged.value)} </Board> </> )), - 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<Player> -): Array<GenericLayer> => [main, prestige]; +): Array<GenericLayer> => [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 @@ +<template> + <div + :style="`transform: translate(calc(${unref(position).x}px - 50%), ${unref(position).y}px);`" + @mousedown="e => mouseDown(e)" + @touchstart.passive="e => mouseDown(e)" + @mouseup="e => mouseUp(e)" + @touchend.passive="e => mouseUp(e)" + > + <component v-if="comp" :is="comp" /> + </div> +</template> + +<script setup lang="tsx"> +import { jsx } from "features/feature"; +import { VueFeature, coerceComponent, renderJSX } from "util/vue"; +import { Ref, shallowRef, unref } from "vue"; +import { NodePosition } from "./board"; +unref; + +const props = defineProps<{ + element: VueFeature; + mouseDown: (e: MouseEvent | TouchEvent) => void; + mouseUp: (e: MouseEvent | TouchEvent) => void; + position: Ref<NodePosition>; +}>(); + +const comp = shallowRef(coerceComponent(jsx(() => renderJSX(props.element)))); +</script> + +<style scoped> +div { + position: absolute; + top: 0; + left: 50%; + transition-duration: 0s; +} +</style> 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<T>() { }; } -export function setupDraggableNode<T extends NodePosition, S extends NodePosition = T>(options: { +export function setupDraggableNode<T>(options: { board: Ref<ComponentPublicInstance<typeof Board> | undefined>; - receivingNodes?: NodeComputable<T, S[]>; - dropAreaRadius?: NodeComputable<S, number>; - isDraggable?: NodeComputable<T, boolean>; - onDrop?: (acceptingNode: S, draggingNode: T) => void; + getPosition: (node: T) => NodePosition; + setPosition: (node: T, position: NodePosition) => void; + receivingNodes?: NodeComputable<T, T[]>; + dropAreaRadius?: NodeComputable<T, number>; + onDrop?: (acceptingNode: T, draggingNode: T) => void; }) { const nodeBeingDragged = ref<T>(); - const receivingNode = ref<S>(); + const receivingNode = ref<T>(); const hasDragged = ref(false); const mousePosition = ref<NodePosition>(); 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<T extends NodePosition, S extends NodePositio ); 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 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<T extends NodePosition, S extends NodePositio lastMousePosition, dragDelta, receivingNodes, - startDrag: function (e: MouseEvent | TouchEvent, node?: T) { + startDrag: function (e: MouseEvent | TouchEvent, node: T) { e.preventDefault(); e.stopPropagation(); @@ -141,17 +139,17 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio dragDelta.value = { x: 0, y: 0 }; hasDragged.value = false; - if (node != null && unwrapNodeRef(isDraggable, node)) { - nodeBeingDragged.value = 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; + const { x, y } = options.getPosition(nodeBeingDragged.value); + const newX = x + Math.round(dragDelta.value.x / 25) * 25; + const newY = y + Math.round(dragDelta.value.y / 25) * 25; + options.setPosition(nodeBeingDragged.value, { x: newX, y: newY }); } if (receivingNode.value != null) { @@ -210,6 +208,64 @@ export function setupDraggableNode<T extends NodePosition, S extends NodePositio return result; } +export function makeDraggable<T extends VueFeature, S>( + element: T, + options: { + id: S; + nodeBeingDragged: Ref<S | undefined>; + hasDragged: Ref<boolean>; + dragDelta: Ref<NodePosition>; + 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<NodePosition> } { + const position = persistent(options.initialPosition ?? { x: 0, y: 0 }); + (element as T & { position: Persistent<NodePosition> }).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<T extends NodePosition>(options: { node: Computable<T | undefined>; shouldShowActions?: NodeComputable<T, boolean>;