Add support for rendering VueFeatures in boards
Some checks failed
Run Tests / test (pull_request) Failing after 47s

This commit is contained in:
thepaperpilot 2024-03-03 19:59:26 -06:00
parent 424bde0cdd
commit 1acfde134b
4 changed files with 225 additions and 167 deletions

View file

@ -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;

View file

@ -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.

View file

@ -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>

View file

@ -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;
}
},
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>;