Implemented selecting nodes and running immediate actions

This commit is contained in:
thepaperpilot 2021-08-22 01:50:03 -05:00
parent 35b3226995
commit 11df9853d0
12 changed files with 346 additions and 103 deletions

View file

@ -6,6 +6,7 @@
ref="stage" ref="stage"
@init="onInit" @init="onInit"
@mousemove="drag" @mousemove="drag"
@mousedown="deselect"
@mouseup="() => endDragging(dragging)" @mouseup="() => endDragging(dragging)"
@mouseleave="() => endDragging(dragging)" @mouseleave="() => endDragging(dragging)"
> >
@ -18,18 +19,9 @@
:nodeType="board.types[node.type]" :nodeType="board.types[node.type]"
:dragging="draggingNode" :dragging="draggingNode"
:dragged="dragged" :dragged="dragged"
:hasDragged="hasDragged"
:receivingNode="receivingNode?.id === node.id" :receivingNode="receivingNode?.id === node.id"
@startDragging="startDragging" @mouseDown="mouseDown"
@endDragging="endDragging"
/>
<BoardNode
v-if="draggingNode"
:node="draggingNode"
:nodeType="board.types[draggingNode.type]"
:dragging="draggingNode"
:dragged="dragged"
:receivingNode="receivingNode?.id === draggingNode.id"
@startDragging="startDragging"
@endDragging="endDragging" @endDragging="endDragging"
/> />
</g> </g>
@ -51,11 +43,13 @@ export default defineComponent({
return { return {
lastMousePosition: { x: 0, y: 0 }, lastMousePosition: { x: 0, y: 0 },
dragged: { x: 0, y: 0 }, dragged: { x: 0, y: 0 },
dragging: null dragging: null,
hasDragged: false
} as { } as {
lastMousePosition: { x: number; y: number }; lastMousePosition: { x: number; y: number };
dragged: { x: number; y: number }; dragged: { x: number; y: number };
dragging: string | null; dragging: string | null;
hasDragged: boolean;
}; };
}, },
props: { props: {
@ -79,14 +73,15 @@ export default defineComponent({
]; ];
}, },
draggingNode() { draggingNode() {
return this.dragging return this.dragging ? this.board.nodes.find(node => node.id === this.dragging) : null;
? player.layers[this.layer].boards[this.id].find(node => node.id === this.dragging)
: null;
}, },
nodes() { nodes() {
return player.layers[this.layer].boards[this.id].filter( const nodes = this.board.nodes.slice();
node => node !== this.draggingNode if (this.draggingNode) {
); const draggingNode = nodes.splice(nodes.indexOf(this.draggingNode), 1)[0];
nodes.push(draggingNode);
}
return nodes;
}, },
receivingNode(): BoardNode | null { receivingNode(): BoardNode | null {
if (this.draggingNode == null) { if (this.draggingNode == null) {
@ -99,6 +94,9 @@ export default defineComponent({
}; };
let smallestDistance = Number.MAX_VALUE; let smallestDistance = Number.MAX_VALUE;
return this.nodes.reduce((smallest: BoardNode | null, curr: BoardNode) => { return this.nodes.reduce((smallest: BoardNode | null, curr: BoardNode) => {
if (curr.id === this.draggingNode!.id) {
return smallest;
}
const nodeType = this.board.types[curr.type]; const nodeType = this.board.types[curr.type];
const canAccept = const canAccept =
typeof nodeType.canAccept === "boolean" typeof nodeType.canAccept === "boolean"
@ -126,10 +124,14 @@ export default defineComponent({
getZoomLevel(): number { getZoomLevel(): number {
return (this.$refs.stage as any).$panZoomInstance.getTransform().scale; return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
}, },
onInit: function(panzoomInstance: any) { onInit(panzoomInstance: any) {
panzoomInstance.setTransformOrigin(null); panzoomInstance.setTransformOrigin(null);
}, },
startDragging(e: MouseEvent, nodeID: string) { deselect() {
player.layers[this.layer].boards[this.id].selectedNode = null;
player.layers[this.layer].boards[this.id].selectedAction = null;
},
mouseDown(e: MouseEvent, nodeID: string, draggable: boolean) {
if (this.dragging == null) { if (this.dragging == null) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -139,33 +141,43 @@ export default defineComponent({
y: e.clientY y: e.clientY
}; };
this.dragged = { x: 0, y: 0 }; this.dragged = { x: 0, y: 0 };
this.hasDragged = false;
if (draggable) {
this.dragging = nodeID; this.dragging = nodeID;
} }
}
player.layers[this.layer].boards[this.id].selectedNode = null;
player.layers[this.layer].boards[this.id].selectedAction = null;
}, },
drag(e: MouseEvent) { drag(e: MouseEvent) {
if (this.dragging) {
e.preventDefault();
e.stopPropagation();
const zoom = (this.getZoomLevel as () => number)(); const zoom = (this.getZoomLevel as () => number)();
this.dragged = { this.dragged = {
x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom, x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom,
y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom
} };
this.lastMousePosition = { this.lastMousePosition = {
x: e.clientX, x: e.clientX,
y: e.clientY y: e.clientY
}; };
if (Math.abs(this.dragged.x) > 10 || Math.abs(this.dragged.y) > 10) {
this.hasDragged = true;
}
if (this.dragging) {
e.preventDefault();
e.stopPropagation();
} }
}, },
endDragging(nodeID: string | null) { endDragging(nodeID: string | null) {
if (this.dragging != null && this.dragging === nodeID) { if (this.dragging != null && this.dragging === nodeID) {
const nodes = player.layers[this.layer].boards[this.id];
const draggingNode = this.draggingNode!; const draggingNode = this.draggingNode!;
const receivingNode = this.receivingNode; const receivingNode = this.receivingNode;
draggingNode.position.x += Math.round(this.dragged.x / 25) * 25; draggingNode.position.x += Math.round(this.dragged.x / 25) * 25;
draggingNode.position.y += Math.round(this.dragged.y / 25) * 25; draggingNode.position.y += Math.round(this.dragged.y / 25) * 25;
const nodes = this.board.nodes;
nodes.splice(nodes.indexOf(draggingNode), 1); nodes.splice(nodes.indexOf(draggingNode), 1);
nodes.push(draggingNode); nodes.push(draggingNode);

View file

@ -1,13 +1,38 @@
<template> <template>
<g <g
class="boardnode" class="boardnode"
:style="{ opacity: dragging?.id === node.id ? 0.5 : 1 }" :style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
:transform="`translate(${position.x},${position.y})`" :transform="`translate(${position.x},${position.y})`"
>
<transition name="actions" appear>
<g v-if="selected && actions">
<g
v-for="(action, index) in actions"
:key="action.id"
class="action"
:transform="
`translate(
${(-size - 30) *
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)},
${(size + 30) *
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
)`
"
@click="performAction(action)"
>
<circle :fill="fillColor" r="20" />
<text :fill="titleColor" class="material-icons">{{ action.icon }}</text>
</g>
</g>
</transition>
<g
v-if="shape === Shape.Circle"
@mouseenter="mouseEnter" @mouseenter="mouseEnter"
@mouseleave="mouseLeave" @mouseleave="mouseLeave"
@mousedown="mouseDown" @mousedown="mouseDown"
@mouseup="mouseUp"
> >
<g v-if="shape === Shape.Circle">
<circle <circle
v-if="canAccept" v-if="canAccept"
:r="size + 8" :r="size + 8"
@ -36,21 +61,30 @@
:stroke="progressColor" :stroke="progressColor"
/> />
</g> </g>
<g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)"> <g
v-else-if="shape === Shape.Diamond"
transform="rotate(45, 0, 0)"
@mouseenter="mouseEnter"
@mouseleave="mouseLeave"
@mousedown="mouseDown"
@mouseup="mouseUp"
>
<rect <rect
v-if="canAccept" v-if="canAccept"
:width="size + 16" :width="size * sqrtTwo + 16"
:height="size + 16" :height="size * sqrtTwo + 16"
:transform="`translate(${-(size + 16) / 2}, ${-(size + 16) / 2})`" :transform="
`translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) / 2})`
"
:fill="backgroundColor" :fill="backgroundColor"
:stroke="receivingNode ? '#0F0' : '#0F03'" :stroke="receivingNode ? '#0F0' : '#0F03'"
:stroke-width="2" :stroke-width="2"
/> />
<rect <rect
:width="size" :width="size * sqrtTwo"
:height="size" :height="size * sqrtTwo"
:transform="`translate(${-size / 2}, ${-size / 2})`" :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
:fill="fillColor" :fill="fillColor"
:stroke="outlineColor" :stroke="outlineColor"
:stroke-width="4" :stroke-width="4"
@ -58,11 +92,11 @@
<rect <rect
v-if="progressDisplay === ProgressDisplay.Fill" v-if="progressDisplay === ProgressDisplay.Fill"
:width="Math.max(size * progress - 2, 0)" :width="Math.max(size * sqrtTwo * progress - 2, 0)"
:height="Math.max(size * progress - 2, 0)" :height="Math.max(size * sqrtTwo * progress - 2, 0)"
:transform=" :transform="
`translate(${-Math.max(size * progress - 2, 0) / 2}, ${-Math.max( `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${-Math.max(
size * progress - 2, size * sqrtTwo * progress - 2,
0 0
) / 2})` ) / 2})`
" "
@ -70,13 +104,13 @@
/> />
<rect <rect
v-else v-else
:width="size + 9" :width="size * sqrtTwo + 9"
:height="size + 9" :height="size * sqrtTwo + 9"
:transform="`translate(${-(size + 9) / 2}, ${-(size + 9) / 2})`" :transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`"
fill="transparent" fill="transparent"
:stroke-dasharray="(size + 9) * 4" :stroke-dasharray="(size * sqrtTwo + 9) * 4"
:stroke-width="5" :stroke-width="5"
:stroke-dashoffset="(size + 9) * 4 - progress * (size + 9) * 4" :stroke-dashoffset="(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4"
:stroke="progressColor" :stroke="progressColor"
/> />
</g> </g>
@ -88,8 +122,9 @@
<script lang="ts"> <script lang="ts">
import themes from "@/data/themes"; import themes from "@/data/themes";
import { ProgressDisplay, Shape } from "@/game/enums"; import { ProgressDisplay, Shape } from "@/game/enums";
import { layers } from "@/game/layers";
import player from "@/game/player"; import player from "@/game/player";
import { BoardNode, NodeType } from "@/typings/features/board"; import { BoardNode, BoardNodeAction, NodeType } from "@/typings/features/board";
import { getNodeTypeProperty } from "@/util/features"; import { getNodeTypeProperty } from "@/util/features";
import { InjectLayerMixin } from "@/util/vue"; import { InjectLayerMixin } from "@/util/vue";
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
@ -102,10 +137,11 @@ export default defineComponent({
ProgressDisplay, ProgressDisplay,
Shape, Shape,
lastMousePosition: { x: 0, y: 0 }, lastMousePosition: { x: 0, y: 0 },
hovering: false hovering: false,
sqrtTwo: Math.sqrt(2)
}; };
}, },
emits: ["startDragging", "endDragging"], emits: ["mouseDown", "endDragging"],
props: { props: {
node: { node: {
type: Object as PropType<BoardNode>, type: Object as PropType<BoardNode>,
@ -122,12 +158,25 @@ export default defineComponent({
type: Object as PropType<{ x: number; y: number }>, type: Object as PropType<{ x: number; y: number }>,
required: true required: true
}, },
hasDragged: {
type: Boolean,
default: false
},
receivingNode: { receivingNode: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, },
computed: { computed: {
board() {
return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
},
selected() {
return this.board.selectedNode?.id === this.node.id;
},
actions(): BoardNodeAction[] | null | undefined {
return getNodeTypeProperty(this.nodeType, this.node, "actions");
},
draggable(): boolean { draggable(): boolean {
return getNodeTypeProperty(this.nodeType, this.node, "draggable"); return getNodeTypeProperty(this.nodeType, this.node, "draggable");
}, },
@ -146,7 +195,7 @@ export default defineComponent({
let size: number = getNodeTypeProperty(this.nodeType, this.node, "size"); let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
if (this.receivingNode) { if (this.receivingNode) {
size *= 1.25; size *= 1.25;
} else if (this.hovering) { } else if (this.hovering || this.selected) {
size *= 1.15; size *= 1.15;
} }
return size; return size;
@ -188,18 +237,24 @@ export default defineComponent({
); );
}, },
canAccept(): boolean { canAccept(): boolean {
if (this.dragging == null) { if (this.dragging == null || !this.hasDragged) {
return false; return false;
} }
return typeof this.nodeType.canAccept === "boolean" return typeof this.nodeType.canAccept === "boolean"
? this.nodeType.canAccept ? this.nodeType.canAccept
: this.nodeType.canAccept(this.node, this.dragging); : this.nodeType.canAccept(this.node, this.dragging);
},
actionDistance(): number {
return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
} }
}, },
methods: { methods: {
mouseDown(e: MouseEvent) { mouseDown(e: MouseEvent) {
if (this.draggable) { this.$emit("mouseDown", e, this.node.id, this.draggable);
this.$emit('startDragging', e, this.node.id); },
mouseUp() {
if (!this.hasDragged) {
this.nodeType.onClick?.(this.node);
} }
}, },
mouseEnter() { mouseEnter() {
@ -207,11 +262,14 @@ export default defineComponent({
}, },
mouseLeave() { mouseLeave() {
this.hovering = false; this.hovering = false;
},
performAction(action: BoardNodeAction) {
action.onClick(this.node);
} }
}, },
watch: { watch: {
onDraggableChanged() { onDraggableChanged() {
if (this.dragging && !this.draggable) { if (this.dragging?.id === this.node.id && !this.draggable) {
this.$emit("endDragging", this.node.id); this.$emit("endDragging", this.node.id);
} }
} }
@ -230,9 +288,30 @@ export default defineComponent({
dominant-baseline: middle; dominant-baseline: middle;
font-family: monospace; font-family: monospace;
font-size: 200%; font-size: 200%;
pointer-events: none;
} }
.progressRing { .progressRing {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
.action:hover circle {
r: 25;
}
.action:hover text {
font-size: 187.5%; /* 150% * 1.25 */
}
.action text {
text-anchor: middle;
dominant-baseline: central;
}
</style>
<style>
.actions-enter-from .action,
.actions-leave-to .action {
transform: translate(0, 0);
}
</style> </style>

80
src/data/layers/Main.vue Normal file
View file

@ -0,0 +1,80 @@
<template>
<div v-if="devSpeed === 0">Game Paused</div>
<div v-else-if="devSpeed && devSpeed !== 1">Dev Speed: {{ formattedDevSpeed }}x</div>
<Board id="main" />
<Modal :show="showModal" @close="closeModal">
<template v-slot:header v-if="title">
<component :is="title" />
</template>
<template v-slot:body v-if="body">
<component :is="body" />
</template>
<template v-slot:footer v-if="footer">
<component :is="footer" />
</template>
</Modal>
</template>
<script lang="ts">
import player from "@/game/player";
import { CoercableComponent } from "@/typings/component";
import { format } from "@/util/break_eternity";
import { camelToTitle } from "@/util/common";
import { coerceComponent } from "@/util/vue";
import { computed, defineComponent, shallowRef, watchEffect } from "vue";
import { ActionNodeData, ResourceNodeData } from "./main";
export default defineComponent(function Main() {
const title = shallowRef<CoercableComponent | null>(null);
const body = shallowRef<CoercableComponent | null>(null);
const footer = shallowRef<CoercableComponent | null>(null);
watchEffect(() => {
const node = player.layers.main.boards.main.nodes.find(
node => node.id === player.layers.main.openNode
);
if (node == null) {
player.layers.main.showModal = false;
return;
}
switch (node.type) {
default:
player.layers.main.showModal = false;
break;
case "resource":
switch ((node.data as ResourceNodeData).resourceType) {
default:
player.layers.main.showModal = false;
break;
case "time":
title.value = coerceComponent("<h2>Time</h2>");
body.value = coerceComponent(
"The ultimate resource, that you'll never have enough of."
);
break;
}
break;
case "action":
title.value = coerceComponent(
camelToTitle((node.data as ActionNodeData).actionType)
);
body.value = coerceComponent(
"<div><div>" +
(node.data as ActionNodeData).log.join("</div><div>") +
"</div></div>"
);
break;
}
});
const showModal = computed(() => player.layers.main.showModal);
const closeModal = () => {
player.layers.main.showModal = false;
};
const devSpeed = computed(() => player.devSpeed);
const formattedDevSpeed = computed(() => player.devSpeed && format(player.devSpeed));
return { title, body, footer, showModal, closeModal, devSpeed, formattedDevSpeed };
});
</script>

View file

@ -4,36 +4,34 @@ import Decimal, { DecimalSource } from "@/lib/break_eternity";
import { RawLayer } from "@/typings/layer"; import { RawLayer } from "@/typings/layer";
import { camelToTitle } from "@/util/common"; import { camelToTitle } from "@/util/common";
import themes from "../themes"; import themes from "../themes";
import Main from "./Main.vue";
type ResourceNodeData = { export type ResourceNodeData = {
resourceType: string; resourceType: string;
amount: DecimalSource; amount: DecimalSource;
maxAmount: DecimalSource; maxAmount: DecimalSource;
}; };
type ItemNodeData = { export type ItemNodeData = {
itemType: string; itemType: string;
amount: DecimalSource; amount: DecimalSource;
}; };
type ActionNodeData = { export type ActionNodeData = {
actionType: string; actionType: string;
log: string[];
}; };
export default { export default {
id: "main", id: "main",
display: ` display: Main,
<div v-if="player.devSpeed === 0">Game Paused</div>
<div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
<div>TODO: Board</div>
<Board id="main" />
`,
startData() { startData() {
return { return {
openNode: null openNode: null,
showModal: false
} as { } as {
openNode: string | null; openNode: string | null;
showModal: boolean;
}; };
}, },
minimizable: false, minimizable: false,
@ -71,7 +69,8 @@ export default {
position: { x: -150, y: 150 }, position: { x: -150, y: 150 },
type: "action", type: "action",
data: { data: {
actionType: "browse" actionType: "web",
log: []
} }
} }
]; ];
@ -101,31 +100,55 @@ export default {
canAccept(node, otherNode) { canAccept(node, otherNode) {
return otherNode.type === "item"; return otherNode.type === "item";
}, },
onClick(node) {
player.layers.main.openNode = node.id;
player.layers.main.showModal = true;
},
onDrop(node, otherNode) { onDrop(node, otherNode) {
const index = player.layers[this.layer].boards[this.id].indexOf( const index = player.layers[this.layer].boards[this.id].nodes.indexOf(
otherNode otherNode
); );
player.layers[this.layer].boards[this.id].splice(index, 1); player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
} }
}, },
item: { item: {
title(node) { title(node) {
return (node.data as ItemNodeData).itemType; return (node.data as ItemNodeData).itemType;
}, },
onClick(node) {
player.layers.main.openNode = node.id;
player.layers.main.showModal = true;
},
draggable: true draggable: true
}, },
action: { action: {
title(node) { title(node) {
return camelToTitle((node.data as ActionNodeData).actionType); return camelToTitle((node.data as ActionNodeData).actionType);
}, },
fillColor() { fillColor: "#000",
return themes[player.theme].variables["--background-tooltip"];
},
draggable: true, draggable: true,
shape: Shape.Diamond, shape: Shape.Diamond,
size: 100,
progressColor: "#0FF3", progressColor: "#0FF3",
progressDisplay: ProgressDisplay.Outline progressDisplay: ProgressDisplay.Outline,
actions: [
{
id: "info",
icon: "history_edu",
tooltip: "Log",
onClick(node) {
player.layers.main.openNode = node.id;
player.layers.main.showModal = true;
}
},
{
id: "reddit",
icon: "reddit",
tooltip: "Browse Reddit",
onClick(node) {
// TODO
}
}
]
} }
} }
} }

View file

@ -68,10 +68,10 @@ function updateLayers(diff: DecimalSource) {
); );
} }
layers[layer].update?.(diff); layers[layer].update?.(diff);
if (layers[layer].boards) { if (layers[layer].boards && layers[layer].boards?.data) {
Reflect.ownKeys(player.layers[layer].boards).forEach(board => { Object.values(layers[layer].boards!.data!).forEach(board => {
player.layers[layer].boards[board.toString()].forEach(node => { board.nodes.forEach(node => {
const nodeType = layers[layer].boards!.data[board.toString()].types[node.type]; const nodeType = board.types[node.type];
nodeType.update?.(node, diff); nodeType.update?.(node, diff);
}); });
}); });

View file

@ -432,6 +432,29 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
for (const id in layer.boards.data) { for (const id in layer.boards.data) {
setDefault(layer.boards.data[id], "width", "100%"); setDefault(layer.boards.data[id], "width", "100%");
setDefault(layer.boards.data[id], "height", "400px"); setDefault(layer.boards.data[id], "height", "400px");
setDefault(layer.boards.data[id], "nodes", function() {
return playerProxy.layers[this.layer].boards[this.id].nodes;
});
setDefault(layer.boards.data[id], "selectedNode", function() {
return playerProxy.layers[this.layer].boards[this.id].nodes.find(
node => node.id === playerProxy.layers[this.layer].boards[this.id].selectedNode
);
});
setDefault(layer.boards.data[id], "selectedAction", function() {
if (this.selectedNode == null) {
return null;
}
const nodeType = layers[this.layer].boards!.data[this.id].types[
this.selectedNode.type
];
if (nodeType.actions === null) {
return null;
}
if (typeof nodeType.actions === "function") {
return nodeType.actions(this.selectedNode);
}
return nodeType.actions;
});
for (const nodeType in layer.boards.data[id].types) { for (const nodeType in layer.boards.data[id].types) {
layer.boards.data[id].types[nodeType].layer = layer.id; layer.boards.data[id].types[nodeType].layer = layer.id;
layer.boards.data[id].types[nodeType].id = id; layer.boards.data[id].types[nodeType].id = id;
@ -440,16 +463,20 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
setDefault(layer.boards.data[id].types[nodeType], "draggable", false); setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
setDefault(layer.boards.data[id].types[nodeType], "shape", Shape.Circle); setDefault(layer.boards.data[id].types[nodeType], "shape", Shape.Circle);
setDefault(layer.boards.data[id].types[nodeType], "canAccept", false); setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
setDefault(layer.boards.data[id].types[nodeType], "actionDistance", Math.PI / 6);
setDefault( setDefault(
layer.boards.data[id].types[nodeType], layer.boards.data[id].types[nodeType],
"progressDisplay", "progressDisplay",
ProgressDisplay.Fill ProgressDisplay.Fill
); );
setDefault(layer.boards.data[id].types[nodeType], "nodes", function() { setDefault(layer.boards.data[id].types[nodeType], "nodes", function() {
return playerProxy.layers[this.layer].boards[this.id].filter( return playerProxy.layers[this.layer].boards[this.id].nodes.filter(
node => node.type === this.type node => node.type === this.type
); );
}); });
setDefault(layer.boards.data[id].types[nodeType], "onClick", function(node) {
playerProxy.layers[this.layer].boards[this.id].selectedNode = node.id;
});
} }
} }
} }

View file

@ -1,3 +1,3 @@
import { ComponentOptions } from "vue"; import { Component, ComponentOptions } from "vue";
export type CoercableComponent = string | ComponentOptions; export type CoercableComponent = string | ComponentOptions | Component;

View file

@ -13,9 +13,10 @@ export interface BoardNode {
data?: State; data?: State;
} }
export interface CardOption { export interface BoardData {
text: string; nodes: BoardNode[];
selected: (node: BoardNode) => void; selectedNode: string | null;
selectedAction: string | null;
} }
export interface Board extends Feature { export interface Board extends Feature {
@ -24,6 +25,9 @@ export interface Board extends Feature {
height: string; height: string;
width: string; width: string;
types: Record<string, NodeType>; types: Record<string, NodeType>;
nodes: BoardNode[];
selectedNode: BoardNode | null;
selectedAction: BoardNodeAction | null;
} }
export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & { export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
@ -43,8 +47,17 @@ export interface NodeType extends Feature {
fillColor?: string | ((node: BoardNode) => string); fillColor?: string | ((node: BoardNode) => string);
outlineColor?: string | ((node: BoardNode) => string); outlineColor?: string | ((node: BoardNode) => string);
titleColor?: string | ((node: BoardNode) => string); titleColor?: string | ((node: BoardNode) => string);
actions?: BoardNodeAction[] | ((node: BoardNode) => BoardNodeAction[]);
actionDistance: number | ((node: BoardNode) => number);
onClick?: (node: BoardNode) => void; onClick?: (node: BoardNode) => void;
onDrop?: (node: BoardNode, otherNode: BoardNode) => void; onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
update?: (node: BoardNode, diff: DecimalSource) => void; update?: (node: BoardNode, diff: DecimalSource) => void;
nodes: BoardNode[]; nodes: BoardNode[];
} }
export interface BoardNodeAction {
id: string;
icon: string;
tooltip: string;
onClick: (node: BoardNode) => void;
}

View file

@ -1,7 +1,7 @@
import { Themes } from "@/data/themes"; import { Themes } from "@/data/themes";
import { DecimalSource } from "@/lib/break_eternity"; import { DecimalSource } from "@/lib/break_eternity";
import Decimal from "@/util/bignum"; import Decimal from "@/util/bignum";
import { BoardNode } from "./features/board"; import { BoardData, BoardNode } from "./features/board";
import { MilestoneDisplay } from "./features/milestone"; import { MilestoneDisplay } from "./features/milestone";
import { State } from "./state"; import { State } from "./state";
@ -62,7 +62,7 @@ export interface LayerSaveData {
clickables: Record<string, State>; clickables: Record<string, State>;
challenges: Record<string, Decimal>; challenges: Record<string, Decimal>;
grids: Record<string, Record<string, State>>; grids: Record<string, Record<string, State>>;
boards: Record<string, BoardNode[]>; boards: Record<string, BoardData>;
confirmRespecBuyables: boolean; confirmRespecBuyables: boolean;
[index: string]: unknown; [index: string]: unknown;
} }

View file

@ -1,7 +1,7 @@
import { hotkeys, layers } from "@/game/layers"; import { hotkeys, layers } from "@/game/layers";
import player from "@/game/player"; import player from "@/game/player";
import { CacheableFunction } from "@/typings/cacheableFunction"; import { CacheableFunction } from "@/typings/cacheableFunction";
import { Board, BoardNode, RawBoard } from "@/typings/features/board"; import { Board, BoardData, BoardNode, RawBoard } from "@/typings/features/board";
import { Buyable } from "@/typings/features/buyable"; import { Buyable } from "@/typings/features/buyable";
import { Challenge } from "@/typings/features/challenge"; import { Challenge } from "@/typings/features/challenge";
import { Clickable } from "@/typings/features/clickable"; import { Clickable } from "@/typings/features/clickable";
@ -73,17 +73,21 @@ export function getStartingChallenges(
export function getStartingBoards( export function getStartingBoards(
boards?: Record<string, Board> | Record<string, RawBoard> | undefined boards?: Record<string, Board> | Record<string, RawBoard> | undefined
): Record<string, BoardNode[]> { ): Record<string, BoardData> {
return boards return boards
? Object.keys(boards).reduce((acc: Record<string, BoardNode[]>, curr: string): Record< ? Object.keys(boards).reduce((acc: Record<string, BoardData>, curr: string): Record<
string, string,
BoardNode[] BoardData
> => { > => {
const nodes = boards[curr].startNodes?.() || []; const nodes = boards[curr].startNodes?.() || [];
acc[curr] = nodes.map((node, index) => ({ acc[curr] = {
nodes: nodes.map((node, index) => ({
id: index.toString(), id: index.toString(),
...node ...node
})) as BoardNode[]; })),
selectedNode: null,
selectedAction: null
} as BoardData;
return acc; return acc;
}, {}) }, {})
: {}; : {};

View file

@ -43,7 +43,8 @@ function travel(
object[key] = computed(object[key].bind(objectProxy)); object[key] = computed(object[key].bind(objectProxy));
} else if ( } else if (
(isPlainObject(object[key]) || Array.isArray(object[key])) && (isPlainObject(object[key]) || Array.isArray(object[key])) &&
!(object[key] instanceof Decimal) !(object[key] instanceof Decimal) &&
typeof object[key].render !== "function"
) { ) {
object[key] = callback(object[key]); object[key] = callback(object[key]);
} }
@ -62,7 +63,11 @@ const layerHandler: ProxyHandler<Record<string, any>> = {
if (isRef(target[key])) { if (isRef(target[key])) {
return target[key].value; return target[key].value;
} else if (target[key].isProxy || target[key] instanceof Decimal) { } else if (
target[key].isProxy ||
target[key] instanceof Decimal ||
typeof target[key].render === "function"
) {
return target[key]; return target[key];
} else if ( } else if (
(isPlainObject(target[key]) || Array.isArray(target[key])) && (isPlainObject(target[key]) || Array.isArray(target[key])) &&

View file

@ -35,7 +35,7 @@ const data = function(): Record<string, unknown> {
return { Decimal, player, layers, hasWon, pointGain, ...numberUtils }; return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
}; };
export function coerceComponent( export function coerceComponent(
component: string | ComponentOptions, component: string | ComponentOptions | Component,
defaultWrapper = "span" defaultWrapper = "span"
): Component | string { ): Component | string {
if (typeof component === "string") { if (typeof component === "string") {