<template> <g class="boardnode" :style="{ opacity: dragging?.id === node.id ? 0.5 : 1 }" :transform="`translate(${position.x},${position.y})`" @mouseenter="mouseEnter" @mouseleave="mouseLeave" @mousedown="e => $emit('startDragging', e, node.id)" > <circle v-if="canAccept" :r="size + 8" :fill="backgroundColor" :stroke="receivingNode ? '#0F0' : '#0F03'" :stroke-width="2" /> <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" /> <circle v-if="progressDisplay === ProgressDisplay.Fill" :r="size * progress" :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" /> <text :fill="titleColor" class="node-title">{{ title }}</text> </g> </template> <script lang="ts"> import themes from "@/data/themes"; import { ProgressDisplay } from "@/game/enums"; import player from "@/game/player"; import { BoardNode, NodeType } from "@/typings/features/board"; import { getNodeTypeProperty } from "@/util/features"; import { InjectLayerMixin } from "@/util/vue"; import { defineComponent, PropType } from "vue"; export default defineComponent({ name: "BoardNode", mixins: [InjectLayerMixin], data() { return { ProgressDisplay, lastMousePosition: { x: 0, y: 0 }, hovering: false }; }, emits: ["startDragging", "endDragging"], props: { node: { type: Object as PropType<BoardNode>, required: true }, nodeType: { type: Object as PropType<NodeType>, required: true }, dragging: { type: Object as PropType<BoardNode> }, dragged: { type: Object as PropType<{ x: number; y: number }>, required: true }, receivingNode: { type: Boolean, default: false } }, computed: { draggable(): boolean { return getNodeTypeProperty(this.nodeType, this.node, "draggable"); }, position(): { x: number; y: number } { return this.draggable && this.dragging?.id === this.node.id ? { 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; }, size(): number { let size: number = getNodeTypeProperty(this.nodeType, this.node, "size"); if (this.receivingNode) { size *= 1.25; } else if (this.hovering) { size *= 1.15; } return size; }, title(): string { return getNodeTypeProperty(this.nodeType, this.node, "title"); }, progress(): number { return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0; }, backgroundColor(): string { return themes[player.theme].variables["--background"]; }, outlineColor(): string { return ( getNodeTypeProperty(this.nodeType, this.node, "outlineColor") || themes[player.theme].variables["--separator"] ); }, fillColor(): string { return ( getNodeTypeProperty(this.nodeType, this.node, "fillColor") || themes[player.theme].variables["--secondary-background"] ); }, progressColor(): string { return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none"; }, titleColor(): string { return ( getNodeTypeProperty(this.nodeType, this.node, "titleColor") || themes[player.theme].variables["--color"] ); }, progressDisplay(): ProgressDisplay { return ( getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") || ProgressDisplay.Outline ); }, canAccept(): boolean { if (this.dragging == null) { return false; } return typeof this.nodeType.canAccept === "boolean" ? this.nodeType.canAccept : this.nodeType.canAccept(this.node, this.dragging); } }, methods: { mouseEnter() { this.hovering = true; }, mouseLeave() { this.hovering = false; } }, watch: { onDraggableChanged() { if (this.dragging && !this.draggable) { this.$emit("endDragging", this.node.id); } } } }); </script> <style scoped> .boardnode { cursor: pointer; transition-duration: 0s; } .node-title { text-anchor: middle; dominant-baseline: middle; font-family: monospace; font-size: 200%; } .progressRing { transform: rotate(-90deg); } </style>