Profectus/src/features/boards/BoardNode.vue

320 lines
9.7 KiB
Vue

<template>
<g
class="boardnode"
:class="{ [node.type]: true, isSelected, isDraggable }"
:style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
:transform="`translate(${position.x},${position.y})`"
>
<BoardNodeAction
:actions="actions ?? []"
:is-selected="isSelected"
:node="node"
:node-type="nodeType"
/>
<g
class="node-container"
@mousedown="mouseDown"
@touchstart.passive="mouseDown"
@mouseup="mouseUp"
@touchend.passive="mouseUp"
>
<g v-if="shape === Shape.Circle">
<circle
v-if="canAccept"
class="receiver"
:r="size + 8"
:fill="backgroundColor"
:stroke="receivingNode ? '#0F0' : '#0F03'"
:stroke-width="2"
/>
<circle
class="body"
:r="size"
:fill="fillColor"
:stroke="outlineColor"
:stroke-width="4"
/>
<circle
class="progress progressFill"
v-if="progressDisplay === ProgressDisplay.Fill"
:r="Math.max(size * progress - 2, 0)"
:fill="progressColor"
/>
<circle
v-else
:r="size + 4.5"
class="progress 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"
class="receiver"
: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"
/>
<rect
class="body"
:width="size * sqrtTwo"
:height="size * sqrtTwo"
:transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
:fill="fillColor"
:stroke="outlineColor"
:stroke-width="4"
/>
<rect
v-if="progressDisplay === ProgressDisplay.Fill"
class="progress progressFill"
: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
class="progress progressDiamond"
: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"
/>
</g>
<text :fill="titleColor" class="node-title">{{ title }}</text>
</g>
<transition name="fade" appear>
<g v-if="label">
<text
:fill="label.color || titleColor"
class="node-title"
:class="{ pulsing: label.pulsing }"
:y="-size - 20"
>{{ label.text }}
</text>
</g>
</transition>
<transition name="fade" appear>
<text
v-if="isSelected && selectedAction"
:fill="titleColor"
class="node-title"
:y="size + 75"
>Tap again to confirm</text
>
</transition>
</g>
</template>
<script setup lang="ts">
import themes from "data/themes";
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
import { ProgressDisplay, getNodeProperty, Shape } from "features/boards/board";
import { isVisible } from "features/feature";
import settings from "game/settings";
import { computed, ref, toRefs, unref, watch } from "vue";
import BoardNodeAction from "./BoardNodeAction.vue";
const sqrtTwo = Math.sqrt(2);
const _props = defineProps<{
node: BoardNode;
nodeType: GenericNodeType;
dragging?: BoardNode;
dragged?: {
x: number;
y: number;
};
hasDragged?: boolean;
receivingNode?: boolean;
selectedNode?: BoardNode | null;
selectedAction?: GenericBoardNodeAction | null;
}>();
const props = toRefs(_props);
const emit = defineEmits<{
(e: "mouseDown", event: MouseEvent | TouchEvent, node: number, isDraggable: boolean): void;
(e: "endDragging", node: number): void;
}>();
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
const isDraggable = computed(() =>
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
);
watch(isDraggable, value => {
const node = unref(props.node);
if (unref(props.dragging) === node && !value) {
emit("endDragging", node.id);
}
});
const actions = computed(() => {
const node = unref(props.node);
return getNodeProperty(props.nodeType.value.actions, node)?.filter(action =>
isVisible(getNodeProperty(action.visibility, node))
);
});
const position = computed(() => {
const node = unref(props.node);
if (
getNodeProperty(props.nodeType.value.draggable, node) &&
unref(props.dragging)?.id === node.id &&
unref(props.dragged) != null
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { x, y } = unref(props.dragged)!;
return {
x: node.position.x + Math.round(x / 25) * 25,
y: node.position.y + Math.round(y / 25) * 25
};
}
return node.position;
});
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
const label = computed(() => getNodeProperty(props.nodeType.value.label, unref(props.node)));
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
const progress = computed(
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) ?? 0
);
const backgroundColor = computed(() => themes[settings.theme].variables["--background"]);
const outlineColor = computed(
() =>
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ??
themes[settings.theme].variables["--outline"]
);
const fillColor = computed(
() =>
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ??
themes[settings.theme].variables["--raised-background"]
);
const progressColor = computed(() =>
getNodeProperty(props.nodeType.value.progressColor, unref(props.node))
);
const titleColor = computed(
() =>
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ??
themes[settings.theme].variables["--foreground"]
);
const progressDisplay = computed(() =>
getNodeProperty(props.nodeType.value.progressDisplay, unref(props.node))
);
const canAccept = computed(
() =>
unref(props.dragging) != null &&
unref(props.hasDragged) &&
getNodeProperty(props.nodeType.value.canAccept, unref(props.node))
);
function mouseDown(e: MouseEvent | TouchEvent) {
emit("mouseDown", e, props.node.value.id, isDraggable.value);
}
function mouseUp(e: MouseEvent | TouchEvent) {
if (!props.hasDragged?.value) {
props.nodeType.value.onClick?.(props.node.value);
e.stopPropagation();
}
}
</script>
<style scoped>
.boardnode {
cursor: pointer;
transition-duration: 0s;
}
.boardnode:hover .body {
fill: var(--highlighted);
}
.boardnode.isSelected {
transform: scale(1.2);
}
.boardnode.isSelected .body {
fill: var(--accent1) !important;
}
.boardnode:not(.isDraggable) .body {
fill: var(--locked);
}
.node-title {
text-anchor: middle;
dominant-baseline: middle;
font-family: monospace;
font-size: 200%;
pointer-events: none;
}
.progress {
transition-duration: 0.05s;
}
.progressRing {
transform: rotate(-90deg);
}
.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;
}
}
</style>
<style>
.grow-enter-from .node-container,
.grow-leave-to .node-container {
transform: scale(0);
}
</style>