2021-08-19 00:25:49 -05:00
|
|
|
<template>
|
|
|
|
<g
|
|
|
|
class="boardnode"
|
2021-08-22 01:50:03 -05:00
|
|
|
:style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
|
2021-08-19 00:25:49 -05:00
|
|
|
:transform="`translate(${position.x},${position.y})`"
|
|
|
|
>
|
2021-08-22 01:50:03 -05:00
|
|
|
<transition name="actions" appear>
|
|
|
|
<g v-if="selected && actions">
|
2021-08-22 22:57:59 -05:00
|
|
|
<!-- TODO move to separate file -->
|
2021-08-22 01:50:03 -05:00
|
|
|
<g
|
|
|
|
v-for="(action, index) in actions"
|
|
|
|
:key="action.id"
|
|
|
|
class="action"
|
2021-08-22 22:57:59 -05:00
|
|
|
:class="{ selected: selectedAction === action }"
|
2021-08-22 01:50:03 -05:00
|
|
|
:transform="
|
|
|
|
`translate(
|
|
|
|
${(-size - 30) *
|
|
|
|
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)},
|
|
|
|
${(size + 30) *
|
|
|
|
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
|
|
|
|
)`
|
|
|
|
"
|
2021-08-22 22:57:59 -05:00
|
|
|
@mousedown="e => performAction(e, action)"
|
2021-08-22 23:16:14 -05:00
|
|
|
@touchstart="e => performAction(e, action)"
|
2021-08-24 01:23:25 -05:00
|
|
|
@mouseup="e => actionMouseUp(e, action)"
|
|
|
|
@touchend.stop="e => actionMouseUp(e, action)"
|
2021-08-22 01:50:03 -05:00
|
|
|
>
|
2021-08-22 22:57:59 -05:00
|
|
|
<circle
|
|
|
|
:fill="
|
|
|
|
action.fillColor
|
|
|
|
? typeof action.fillColor === 'function'
|
|
|
|
? action.fillColor(node)
|
|
|
|
: action.fillColor
|
|
|
|
: fillColor
|
|
|
|
"
|
|
|
|
r="20"
|
|
|
|
:stroke-width="selectedAction === action ? 4 : 0"
|
|
|
|
:stroke="outlineColor"
|
|
|
|
/>
|
|
|
|
<text :fill="titleColor" class="material-icons">{{
|
|
|
|
typeof action.icon === "function" ? action.icon(node) : action.icon
|
|
|
|
}}</text>
|
2021-08-22 01:50:03 -05:00
|
|
|
</g>
|
|
|
|
</g>
|
|
|
|
</transition>
|
|
|
|
|
2021-08-26 00:28:03 -05:00
|
|
|
<g
|
|
|
|
class="node-container"
|
|
|
|
@mouseenter="mouseEnter"
|
|
|
|
@mouseleave="mouseLeave"
|
|
|
|
@mousedown="mouseDown"
|
|
|
|
@touchstart="mouseDown"
|
|
|
|
@mouseup="mouseUp"
|
|
|
|
@touchend="mouseUp"
|
|
|
|
>
|
|
|
|
<g v-if="shape === Shape.Circle">
|
|
|
|
<circle
|
|
|
|
v-if="canAccept"
|
|
|
|
:r="size + 8"
|
|
|
|
:fill="backgroundColor"
|
|
|
|
:stroke="receivingNode ? '#0F0' : '#0F03'"
|
|
|
|
:stroke-width="2"
|
|
|
|
/>
|
2021-08-19 00:25:49 -05:00
|
|
|
|
2021-08-26 00:28:03 -05:00
|
|
|
<circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
|
2021-08-19 00:25:49 -05:00
|
|
|
|
2021-08-26 00:28:03 -05:00
|
|
|
<circle
|
|
|
|
v-if="progressDisplay === ProgressDisplay.Fill"
|
|
|
|
:r="Math.max(size * progress - 2, 0)"
|
|
|
|
:fill="progressColor"
|
|
|
|
/>
|
|
|
|
<circle
|
|
|
|
v-else
|
|
|
|
:r="size + 4.5"
|
|
|
|
class="progressRing"
|
|
|
|
fill="transparent"
|
|
|
|
:stroke-dasharray="(size + 4.5) * 2 * Math.PI"
|
|
|
|
:stroke-width="5"
|
|
|
|
:stroke-dashoffset="
|
|
|
|
(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
|
|
|
|
"
|
|
|
|
:stroke="progressColor"
|
|
|
|
/>
|
|
|
|
</g>
|
|
|
|
<g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
|
|
|
|
<rect
|
|
|
|
v-if="canAccept"
|
|
|
|
:width="size * sqrtTwo + 16"
|
|
|
|
:height="size * sqrtTwo + 16"
|
|
|
|
:transform="
|
|
|
|
`translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
|
|
|
|
2})`
|
|
|
|
"
|
|
|
|
:fill="backgroundColor"
|
|
|
|
:stroke="receivingNode ? '#0F0' : '#0F03'"
|
|
|
|
:stroke-width="2"
|
|
|
|
/>
|
2021-08-20 23:21:13 -05:00
|
|
|
|
2021-08-26 00:28:03 -05:00
|
|
|
<rect
|
|
|
|
:width="size * sqrtTwo"
|
|
|
|
:height="size * sqrtTwo"
|
|
|
|
:transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
|
|
|
|
:fill="fillColor"
|
|
|
|
:stroke="outlineColor"
|
|
|
|
:stroke-width="4"
|
|
|
|
/>
|
2021-08-19 00:25:49 -05:00
|
|
|
|
2021-08-26 00:28:03 -05:00
|
|
|
<rect
|
|
|
|
v-if="progressDisplay === ProgressDisplay.Fill"
|
|
|
|
:width="Math.max(size * sqrtTwo * progress - 2, 0)"
|
|
|
|
:height="Math.max(size * sqrtTwo * progress - 2, 0)"
|
|
|
|
:transform="
|
|
|
|
`translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
|
|
|
|
2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
|
|
|
|
"
|
|
|
|
:fill="progressColor"
|
|
|
|
/>
|
|
|
|
<rect
|
|
|
|
v-else
|
|
|
|
:width="size * sqrtTwo + 9"
|
|
|
|
:height="size * sqrtTwo + 9"
|
|
|
|
:transform="
|
|
|
|
`translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`
|
|
|
|
"
|
|
|
|
fill="transparent"
|
|
|
|
:stroke-dasharray="(size * sqrtTwo + 9) * 4"
|
|
|
|
:stroke-width="5"
|
|
|
|
:stroke-dashoffset="
|
|
|
|
(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
|
|
|
|
"
|
|
|
|
:stroke="progressColor"
|
|
|
|
/>
|
2021-08-26 00:13:42 -05:00
|
|
|
</g>
|
2021-08-26 00:28:03 -05:00
|
|
|
|
|
|
|
<text :fill="titleColor" class="node-title">{{ title }}</text>
|
|
|
|
</g>
|
2021-08-22 22:57:59 -05:00
|
|
|
|
|
|
|
<transition name="fade" appear>
|
|
|
|
<text
|
|
|
|
v-if="label"
|
|
|
|
:fill="label.color || titleColor"
|
|
|
|
class="node-title"
|
|
|
|
:class="{ pulsing: label.pulsing }"
|
|
|
|
:y="-size - 20"
|
|
|
|
>{{ label.text }}</text
|
|
|
|
>
|
|
|
|
</transition>
|
|
|
|
|
|
|
|
<transition name="fade" appear>
|
|
|
|
<text
|
|
|
|
:fill="titleColor"
|
|
|
|
class="node-title"
|
|
|
|
:y="size + 75"
|
|
|
|
v-if="selected && selectedAction"
|
|
|
|
>Tap again to confirm</text
|
|
|
|
>
|
|
|
|
</transition>
|
2021-08-19 00:25:49 -05:00
|
|
|
</g>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import themes from "@/data/themes";
|
2021-08-20 23:21:13 -05:00
|
|
|
import { ProgressDisplay, Shape } from "@/game/enums";
|
2021-08-22 01:50:03 -05:00
|
|
|
import { layers } from "@/game/layers";
|
2021-08-19 00:25:49 -05:00
|
|
|
import player from "@/game/player";
|
2021-08-22 22:57:59 -05:00
|
|
|
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
|
2021-08-20 00:47:56 -05:00
|
|
|
import { getNodeTypeProperty } from "@/util/features";
|
2021-08-19 00:25:49 -05:00
|
|
|
import { defineComponent, PropType } from "vue";
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
name: "BoardNode",
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
ProgressDisplay,
|
2021-08-20 23:21:13 -05:00
|
|
|
Shape,
|
2021-08-22 01:50:03 -05:00
|
|
|
hovering: false,
|
|
|
|
sqrtTwo: Math.sqrt(2)
|
2021-08-19 00:25:49 -05:00
|
|
|
};
|
|
|
|
},
|
2021-08-22 01:50:03 -05:00
|
|
|
emits: ["mouseDown", "endDragging"],
|
2021-08-19 00:25:49 -05:00
|
|
|
props: {
|
|
|
|
node: {
|
|
|
|
type: Object as PropType<BoardNode>,
|
|
|
|
required: true
|
|
|
|
},
|
|
|
|
nodeType: {
|
|
|
|
type: Object as PropType<NodeType>,
|
|
|
|
required: true
|
2021-08-20 00:47:56 -05:00
|
|
|
},
|
|
|
|
dragging: {
|
|
|
|
type: Object as PropType<BoardNode>
|
|
|
|
},
|
|
|
|
dragged: {
|
|
|
|
type: Object as PropType<{ x: number; y: number }>,
|
|
|
|
required: true
|
|
|
|
},
|
2021-08-22 01:50:03 -05:00
|
|
|
hasDragged: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
|
|
|
},
|
2021-08-20 00:47:56 -05:00
|
|
|
receivingNode: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false
|
2021-08-19 00:25:49 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
2021-08-22 01:50:03 -05:00
|
|
|
board() {
|
|
|
|
return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
|
|
|
|
},
|
|
|
|
selected() {
|
2021-08-24 01:23:25 -05:00
|
|
|
return this.board.selectedNode === this.node;
|
2021-08-22 01:50:03 -05:00
|
|
|
},
|
2021-08-22 22:57:59 -05:00
|
|
|
selectedAction() {
|
|
|
|
return this.board.selectedAction;
|
|
|
|
},
|
2021-08-22 01:50:03 -05:00
|
|
|
actions(): BoardNodeAction[] | null | undefined {
|
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "actions");
|
|
|
|
},
|
2021-08-19 00:25:49 -05:00
|
|
|
draggable(): boolean {
|
2021-08-20 00:47:56 -05:00
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
|
|
|
position(): { x: number; y: number } {
|
2021-08-20 00:47:56 -05:00
|
|
|
return this.draggable && this.dragging?.id === this.node.id
|
2021-08-19 00:25:49 -05:00
|
|
|
? {
|
|
|
|
x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
|
|
|
|
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
|
|
|
|
}
|
|
|
|
: this.node.position;
|
|
|
|
},
|
2021-08-20 23:21:13 -05:00
|
|
|
shape(): Shape {
|
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "shape");
|
|
|
|
},
|
2021-08-19 00:25:49 -05:00
|
|
|
size(): number {
|
2021-08-20 00:47:56 -05:00
|
|
|
let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
|
|
|
|
if (this.receivingNode) {
|
|
|
|
size *= 1.25;
|
2021-08-22 01:50:03 -05:00
|
|
|
} else if (this.hovering || this.selected) {
|
2021-08-20 00:47:56 -05:00
|
|
|
size *= 1.15;
|
|
|
|
}
|
|
|
|
return size;
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
|
|
|
title(): string {
|
2021-08-20 00:47:56 -05:00
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "title");
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
2021-08-22 22:57:59 -05:00
|
|
|
label(): NodeLabel | null | undefined {
|
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "label");
|
|
|
|
},
|
2021-08-19 00:25:49 -05:00
|
|
|
progress(): number {
|
2021-08-20 00:47:56 -05:00
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
|
|
|
backgroundColor(): string {
|
|
|
|
return themes[player.theme].variables["--background"];
|
|
|
|
},
|
|
|
|
outlineColor(): string {
|
|
|
|
return (
|
2021-08-20 00:47:56 -05:00
|
|
|
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
2021-08-19 00:25:49 -05:00
|
|
|
themes[player.theme].variables["--separator"]
|
|
|
|
);
|
|
|
|
},
|
|
|
|
fillColor(): string {
|
|
|
|
return (
|
2021-08-20 00:47:56 -05:00
|
|
|
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
|
2021-08-19 00:25:49 -05:00
|
|
|
themes[player.theme].variables["--secondary-background"]
|
|
|
|
);
|
|
|
|
},
|
|
|
|
progressColor(): string {
|
2021-08-20 00:47:56 -05:00
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
|
|
|
titleColor(): string {
|
|
|
|
return (
|
2021-08-20 00:47:56 -05:00
|
|
|
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
|
2021-08-19 00:25:49 -05:00
|
|
|
themes[player.theme].variables["--color"]
|
|
|
|
);
|
|
|
|
},
|
|
|
|
progressDisplay(): ProgressDisplay {
|
|
|
|
return (
|
2021-08-20 00:47:56 -05:00
|
|
|
getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
|
2021-08-19 00:25:49 -05:00
|
|
|
ProgressDisplay.Outline
|
|
|
|
);
|
2021-08-20 00:47:56 -05:00
|
|
|
},
|
|
|
|
canAccept(): boolean {
|
2021-08-22 01:50:03 -05:00
|
|
|
if (this.dragging == null || !this.hasDragged) {
|
2021-08-20 00:47:56 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return typeof this.nodeType.canAccept === "boolean"
|
|
|
|
? this.nodeType.canAccept
|
|
|
|
: this.nodeType.canAccept(this.node, this.dragging);
|
2021-08-22 01:50:03 -05:00
|
|
|
},
|
|
|
|
actionDistance(): number {
|
|
|
|
return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
|
2021-08-19 00:25:49 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2021-08-24 19:19:55 -05:00
|
|
|
mouseDown(e: MouseEvent | TouchEvent) {
|
2021-08-22 01:50:03 -05:00
|
|
|
this.$emit("mouseDown", e, this.node.id, this.draggable);
|
|
|
|
},
|
|
|
|
mouseUp() {
|
|
|
|
if (!this.hasDragged) {
|
|
|
|
this.nodeType.onClick?.(this.node);
|
2021-08-20 23:53:40 -05:00
|
|
|
}
|
|
|
|
},
|
2021-08-20 00:47:56 -05:00
|
|
|
mouseEnter() {
|
|
|
|
this.hovering = true;
|
2021-08-19 00:25:49 -05:00
|
|
|
},
|
2021-08-20 00:47:56 -05:00
|
|
|
mouseLeave() {
|
|
|
|
this.hovering = false;
|
2021-08-22 01:50:03 -05:00
|
|
|
},
|
2021-08-24 19:19:55 -05:00
|
|
|
performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
|
2021-08-22 22:57:59 -05:00
|
|
|
// If the onClick function made this action selected,
|
|
|
|
// don't propagate the event (which will deselect everything)
|
2021-08-24 08:18:55 -05:00
|
|
|
if (action.onClick(this.node) || this.board.selectedAction === action) {
|
2021-08-22 22:57:59 -05:00
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
2021-08-24 01:23:25 -05:00
|
|
|
},
|
2021-08-24 19:19:55 -05:00
|
|
|
actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
|
2021-08-24 01:23:25 -05:00
|
|
|
if (this.board.selectedAction === action) {
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
2021-08-20 00:47:56 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
onDraggableChanged() {
|
2021-08-24 01:23:25 -05:00
|
|
|
if (this.dragging === this.node && !this.draggable) {
|
2021-08-20 00:47:56 -05:00
|
|
|
this.$emit("endDragging", this.node.id);
|
2021-08-19 00:25:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
.boardnode {
|
|
|
|
cursor: pointer;
|
|
|
|
transition-duration: 0s;
|
|
|
|
}
|
|
|
|
|
|
|
|
.node-title {
|
|
|
|
text-anchor: middle;
|
|
|
|
dominant-baseline: middle;
|
|
|
|
font-family: monospace;
|
|
|
|
font-size: 200%;
|
2021-08-22 01:50:03 -05:00
|
|
|
pointer-events: none;
|
2021-08-19 00:25:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
.progressRing {
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
}
|
2021-08-22 01:50:03 -05:00
|
|
|
|
2021-08-22 22:57:59 -05:00
|
|
|
.action:hover circle,
|
|
|
|
.action.selected circle {
|
2021-08-22 01:50:03 -05:00
|
|
|
r: 25;
|
|
|
|
}
|
|
|
|
|
2021-08-22 22:57:59 -05:00
|
|
|
.action:hover text,
|
|
|
|
.action.selected text {
|
2021-08-22 01:50:03 -05:00
|
|
|
font-size: 187.5%; /* 150% * 1.25 */
|
|
|
|
}
|
|
|
|
|
|
|
|
.action text {
|
|
|
|
text-anchor: middle;
|
|
|
|
dominant-baseline: central;
|
|
|
|
}
|
2021-08-22 22:57:59 -05:00
|
|
|
|
|
|
|
.fade-enter-from,
|
|
|
|
.fade-leave-to {
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.pulsing {
|
|
|
|
animation: pulsing 2s ease-in infinite;
|
|
|
|
}
|
|
|
|
|
|
|
|
@keyframes pulsing {
|
|
|
|
0% {
|
|
|
|
opacity: 0.25;
|
|
|
|
}
|
|
|
|
|
|
|
|
50% {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
100% {
|
|
|
|
opacity: 0.25;
|
|
|
|
}
|
|
|
|
}
|
2021-08-22 01:50:03 -05:00
|
|
|
</style>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.actions-enter-from .action,
|
|
|
|
.actions-leave-to .action {
|
|
|
|
transform: translate(0, 0);
|
|
|
|
}
|
2021-08-26 00:28:03 -05:00
|
|
|
|
|
|
|
.grow-enter-from .node-container,
|
|
|
|
.grow-leave-to .node-container {
|
|
|
|
transform: scale(0);
|
|
|
|
}
|
2021-08-19 00:25:49 -05:00
|
|
|
</style>
|