forked from profectus/Profectus
Implemented dropping nodes into each other
Also improved node z-ordering and other related things
This commit is contained in:
parent
f3b934337f
commit
02443bbb0c
8 changed files with 249 additions and 100 deletions
|
@ -2,18 +2,35 @@
|
|||
<panZoom
|
||||
:style="style"
|
||||
selector="#g1"
|
||||
@init="onInit"
|
||||
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10 }"
|
||||
ref="stage"
|
||||
@init="onInit"
|
||||
@mousemove="drag"
|
||||
@mouseup="() => endDragging(dragging)"
|
||||
@mouseleave="() => endDragging(dragging)"
|
||||
>
|
||||
<svg class="stage" width="100%" height="100%">
|
||||
<g id="g1">
|
||||
<BoardNode
|
||||
v-for="(node, nodeIndex) in nodes"
|
||||
:key="nodeIndex"
|
||||
:index="nodeIndex"
|
||||
v-for="node in nodes"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
:nodeType="board.types[node.type]"
|
||||
:dragging="draggingNode"
|
||||
:dragged="dragged"
|
||||
:receivingNode="receivingNode?.id === node.id"
|
||||
@startDragging="startDragging"
|
||||
@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"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
@ -23,24 +40,30 @@
|
|||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import player from "@/game/player";
|
||||
import { Board } from "@/typings/features/board";
|
||||
import { Board, BoardNode } from "@/typings/features/board";
|
||||
import { InjectLayerMixin } from "@/util/vue";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Board",
|
||||
mixins: [InjectLayerMixin],
|
||||
data() {
|
||||
return {
|
||||
lastMousePosition: { x: 0, y: 0 },
|
||||
dragged: { x: 0, y: 0 },
|
||||
dragging: null
|
||||
} as {
|
||||
lastMousePosition: { x: number; y: number };
|
||||
dragged: { x: number; y: number };
|
||||
dragging: string | null;
|
||||
};
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getZoomLevel: () => (this.$refs.stage as any).$panZoomInstance.getTransform().scale
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
board(): Board {
|
||||
return layers[this.layer].boards!.data[this.id];
|
||||
|
@ -55,13 +78,101 @@ export default defineComponent({
|
|||
this.board.style
|
||||
];
|
||||
},
|
||||
draggingNode() {
|
||||
return this.dragging
|
||||
? player.layers[this.layer].boards[this.id].find(node => node.id === this.dragging)
|
||||
: null;
|
||||
},
|
||||
nodes() {
|
||||
return player.layers[this.layer].boards[this.id];
|
||||
return player.layers[this.layer].boards[this.id].filter(
|
||||
node => node !== this.draggingNode
|
||||
);
|
||||
},
|
||||
receivingNode(): BoardNode | null {
|
||||
if (this.draggingNode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const position = {
|
||||
x: this.draggingNode.position.x + this.dragged.x,
|
||||
y: this.draggingNode.position.y + this.dragged.y
|
||||
};
|
||||
let smallestDistance = Number.MAX_VALUE;
|
||||
return this.nodes.reduce((smallest: BoardNode | null, curr: BoardNode) => {
|
||||
const nodeType = this.board.types[curr.type];
|
||||
const canAccept =
|
||||
typeof nodeType.canAccept === "boolean"
|
||||
? nodeType.canAccept
|
||||
: nodeType.canAccept(curr, this.draggingNode!);
|
||||
if (!canAccept) {
|
||||
return smallest;
|
||||
}
|
||||
|
||||
const distanceSquared =
|
||||
Math.pow(position.x - curr.position.x, 2) +
|
||||
Math.pow(position.y - curr.position.y, 2);
|
||||
const size =
|
||||
typeof nodeType.size === "number" ? nodeType.size : nodeType.size(curr);
|
||||
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
||||
return smallest;
|
||||
}
|
||||
|
||||
smallestDistance = distanceSquared;
|
||||
return curr;
|
||||
}, null);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getZoomLevel(): number {
|
||||
return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
|
||||
},
|
||||
onInit: function(panzoomInstance) {
|
||||
panzoomInstance.setTransformOrigin(null);
|
||||
},
|
||||
startDragging(e: MouseEvent, nodeID: string) {
|
||||
if (this.dragging == null) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.lastMousePosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
this.dragged = { x: 0, y: 0 };
|
||||
|
||||
this.dragging = nodeID;
|
||||
}
|
||||
},
|
||||
drag(e: MouseEvent) {
|
||||
if (this.dragging) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const zoom = (this.getZoomLevel as () => number)();
|
||||
this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
|
||||
this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
|
||||
this.lastMousePosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
},
|
||||
endDragging(nodeID: string | null) {
|
||||
if (this.dragging != null && this.dragging === nodeID) {
|
||||
const nodes = player.layers[this.layer].boards[this.id];
|
||||
const draggingNode = this.draggingNode!;
|
||||
const receivingNode = this.receivingNode;
|
||||
draggingNode.position.x += Math.round(this.dragged.x / 25) * 25;
|
||||
draggingNode.position.y += Math.round(this.dragged.y / 25) * 25;
|
||||
nodes.splice(nodes.indexOf(draggingNode), 1);
|
||||
nodes.push(draggingNode);
|
||||
|
||||
if (receivingNode) {
|
||||
this.board.types[receivingNode.type].onDrop(receivingNode, draggingNode);
|
||||
}
|
||||
|
||||
this.dragging = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
<template>
|
||||
<g
|
||||
class="boardnode"
|
||||
:style="{ opacity: dragging ? 0.5 : 1 }"
|
||||
:style="{ opacity: dragging?.id === node.id ? 0.5 : 1 }"
|
||||
:transform="`translate(${position.x},${position.y})`"
|
||||
@mousedown="mouseDown"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
@mousedown="e => $emit('startDragging', e, node.id)"
|
||||
>
|
||||
<circle :r="size + 8" :fill="backgroundColor" stroke="#0F03" :stroke-width="2" />
|
||||
<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" />
|
||||
|
||||
|
@ -34,45 +42,22 @@ 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";
|
||||
|
||||
// TODO will blindly use any T given (can't restrict it to S[R] because I can't figure out how
|
||||
// to make it support narrowing the return type)
|
||||
function getTypeProperty<T, S extends NodeType, R extends keyof S>(
|
||||
nodeType: S,
|
||||
node: BoardNode,
|
||||
property: R
|
||||
): S[R] extends Pick<
|
||||
S,
|
||||
{
|
||||
[K in keyof S]-?: undefined extends S[K] ? never : K;
|
||||
}[keyof S]
|
||||
>
|
||||
? T
|
||||
: T | undefined {
|
||||
return typeof nodeType[property] === "function"
|
||||
? (nodeType[property] as (node: BoardNode) => T)(node)
|
||||
: (nodeType[property] as T);
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: "BoardNode",
|
||||
mixins: [InjectLayerMixin],
|
||||
inject: ["getZoomLevel"],
|
||||
data() {
|
||||
return {
|
||||
ProgressDisplay,
|
||||
lastMousePosition: { x: 0, y: 0 },
|
||||
dragged: { x: 0, y: 0 },
|
||||
dragging: false
|
||||
hovering: false
|
||||
};
|
||||
},
|
||||
emits: ["startDragging", "endDragging"],
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
node: {
|
||||
type: Object as PropType<BoardNode>,
|
||||
required: true
|
||||
|
@ -80,14 +65,25 @@ export default defineComponent({
|
|||
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 getTypeProperty(this.nodeType, this.node, "draggable");
|
||||
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
|
||||
},
|
||||
position(): { x: number; y: number } {
|
||||
return this.draggable && this.dragging
|
||||
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
|
||||
|
@ -95,89 +91,71 @@ export default defineComponent({
|
|||
: this.node.position;
|
||||
},
|
||||
size(): number {
|
||||
return getTypeProperty(this.nodeType, this.node, "size");
|
||||
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 getTypeProperty(this.nodeType, this.node, "title");
|
||||
return getNodeTypeProperty(this.nodeType, this.node, "title");
|
||||
},
|
||||
progress(): number {
|
||||
return getTypeProperty(this.nodeType, this.node, "progress") || 0;
|
||||
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
|
||||
},
|
||||
backgroundColor(): string {
|
||||
return themes[player.theme].variables["--background"];
|
||||
},
|
||||
outlineColor(): string {
|
||||
return (
|
||||
getTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
||||
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
||||
themes[player.theme].variables["--separator"]
|
||||
);
|
||||
},
|
||||
fillColor(): string {
|
||||
return (
|
||||
getTypeProperty(this.nodeType, this.node, "fillColor") ||
|
||||
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
|
||||
themes[player.theme].variables["--secondary-background"]
|
||||
);
|
||||
},
|
||||
progressColor(): string {
|
||||
return getTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
||||
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
||||
},
|
||||
titleColor(): string {
|
||||
return (
|
||||
getTypeProperty(this.nodeType, this.node, "titleColor") ||
|
||||
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
|
||||
themes[player.theme].variables["--color"]
|
||||
);
|
||||
},
|
||||
progressDisplay(): ProgressDisplay {
|
||||
return (
|
||||
getTypeProperty(this.nodeType, this.node, "progressDisplay") ||
|
||||
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: {
|
||||
mouseDown(e: MouseEvent) {
|
||||
if (this.draggable) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.lastMousePosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
this.dragged = { x: 0, y: 0 };
|
||||
|
||||
this.dragging = true;
|
||||
document.onmouseup = this.mouseUp;
|
||||
document.onmousemove = this.mouseMove;
|
||||
}
|
||||
mouseEnter() {
|
||||
this.hovering = true;
|
||||
},
|
||||
mouseMove(e: MouseEvent) {
|
||||
if (this.draggable && this.dragging) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const zoom = (this.getZoomLevel as () => number)();
|
||||
console.log(zoom);
|
||||
this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
|
||||
this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
|
||||
this.lastMousePosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
},
|
||||
mouseUp(e: MouseEvent) {
|
||||
if (this.draggable && this.dragging) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let node = player.layers[this.nodeType.layer].boards[this.nodeType.id][this.index];
|
||||
node.position.x += Math.round(this.dragged.x / 25) * 25;
|
||||
node.position.y += Math.round(this.dragged.y / 25) * 25;
|
||||
|
||||
this.dragging = false;
|
||||
document.onmouseup = null;
|
||||
document.onmousemove = null;
|
||||
mouseLeave() {
|
||||
this.hovering = false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
onDraggableChanged() {
|
||||
if (this.dragging && !this.draggable) {
|
||||
this.$emit("endDragging", this.node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,11 @@ type ResourceNodeData = {
|
|||
maxAmount: DecimalSource;
|
||||
};
|
||||
|
||||
type ItemNodeData = {
|
||||
itemType: string;
|
||||
amount: DecimalSource;
|
||||
};
|
||||
|
||||
export default {
|
||||
id: "main",
|
||||
display: `
|
||||
|
@ -39,6 +44,14 @@ export default {
|
|||
amount: new Decimal(24 * 60 * 60),
|
||||
maxAmount: new Decimal(24 * 60 * 60)
|
||||
}
|
||||
},
|
||||
{
|
||||
position: { x: 0, y: 150 },
|
||||
type: "item",
|
||||
data: {
|
||||
itemType: "speed",
|
||||
amount: new Decimal(5 * 60 * 60)
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
|
@ -63,7 +76,22 @@ export default {
|
|||
default:
|
||||
return "none";
|
||||
}
|
||||
},
|
||||
canAccept(node, otherNode) {
|
||||
return otherNode.type === "item";
|
||||
},
|
||||
onDrop(node, otherNode) {
|
||||
const index = player.layers[this.layer].boards[this.id].indexOf(
|
||||
otherNode
|
||||
);
|
||||
player.layers[this.layer].boards[this.id].splice(index, 1);
|
||||
}
|
||||
},
|
||||
item: {
|
||||
title(node) {
|
||||
return (node.data as ItemNodeData).itemType;
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,6 +104,12 @@ const playerHandler: ProxyHandler<Record<string, any>> = {
|
|||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
ownKeys(target: Record<string, any>) {
|
||||
return Reflect.ownKeys(target.__state);
|
||||
},
|
||||
has(target: Record<string, any>, key: string) {
|
||||
return Reflect.has(target.__state, key);
|
||||
}
|
||||
};
|
||||
export default window.player = new Proxy(
|
||||
|
|
8
src/typings/features/board.d.ts
vendored
8
src/typings/features/board.d.ts
vendored
|
@ -2,6 +2,7 @@ import { State } from "../state";
|
|||
import { Feature, RawFeature } from "./feature";
|
||||
|
||||
export interface BoardNode {
|
||||
id: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
|
@ -16,15 +17,15 @@ export interface CardOption {
|
|||
}
|
||||
|
||||
export interface Board extends Feature {
|
||||
startNodes: () => BoardNode[];
|
||||
startNodes: () => Omit<BoardNode, "id">[];
|
||||
style?: Partial<CSSStyleDeclaration>;
|
||||
height: string;
|
||||
width: string;
|
||||
types: Record<string, NodeType>;
|
||||
}
|
||||
|
||||
export type RawBoard = Omit<RawFeature<Board>, "types"> & {
|
||||
startNodes: () => BoardNode[];
|
||||
export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
|
||||
startNodes: () => Omit<BoardNode, "id">[];
|
||||
types: Record<string, RawFeature<NodeType>>;
|
||||
};
|
||||
|
||||
|
@ -41,5 +42,6 @@ export interface NodeType extends Feature {
|
|||
outlineColor?: string | ((node: BoardNode) => string);
|
||||
titleColor?: string | ((node: BoardNode) => string);
|
||||
onClick: (node: BoardNode) => void;
|
||||
onDrop: (node: BoardNode, otherNode: BoardNode) => void;
|
||||
nodes: BoardNode[];
|
||||
}
|
||||
|
|
2
src/typings/player.d.ts
vendored
2
src/typings/player.d.ts
vendored
|
@ -62,7 +62,7 @@ export interface LayerSaveData {
|
|||
clickables: Record<string, State>;
|
||||
challenges: Record<string, Decimal>;
|
||||
grids: Record<string, Record<string, State>>;
|
||||
boards: Record<string, Array<BoardNode>>;
|
||||
boards: Record<string, BoardNode[]>;
|
||||
confirmRespecBuyables: boolean;
|
||||
[index: string]: unknown;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { layers } from "@/game/layers";
|
||||
import { NodeType, BoardNode } from "@/typings/features/board";
|
||||
import { GridCell } from "@/typings/features/grid";
|
||||
import { State } from "@/typings/state";
|
||||
import Decimal, { DecimalSource } from "@/util/bignum";
|
||||
|
@ -87,3 +88,22 @@ export function achievementEffect(layer: string, id: string | number): State | u
|
|||
export function gridEffect(layer: string, id: string, cell: string | number): State | undefined {
|
||||
return (layers[layer].grids?.data[id][cell] as GridCell).effect;
|
||||
}
|
||||
|
||||
// TODO will blindly use any T given (can't restrict it to S[R] because I can't figure out how
|
||||
// to make it support narrowing the return type)
|
||||
export function getNodeTypeProperty<T, S extends NodeType, R extends keyof S>(
|
||||
nodeType: S,
|
||||
node: BoardNode,
|
||||
property: R
|
||||
): S[R] extends Pick<
|
||||
S,
|
||||
{
|
||||
[K in keyof S]-?: undefined extends S[K] ? never : K;
|
||||
}[keyof S]
|
||||
>
|
||||
? T
|
||||
: T | undefined {
|
||||
return typeof nodeType[property] === "function"
|
||||
? (nodeType[property] as (node: BoardNode) => T)(node)
|
||||
: (nodeType[property] as T);
|
||||
}
|
||||
|
|
|
@ -73,13 +73,17 @@ export function getStartingChallenges(
|
|||
|
||||
export function getStartingBoards(
|
||||
boards?: Record<string, Board> | Record<string, RawBoard> | undefined
|
||||
): Record<string, Array<BoardNode>> {
|
||||
): Record<string, BoardNode[]> {
|
||||
return boards
|
||||
? Object.keys(boards).reduce((acc: Record<string, Array<BoardNode>>, curr: string): Record<
|
||||
? Object.keys(boards).reduce((acc: Record<string, BoardNode[]>, curr: string): Record<
|
||||
string,
|
||||
Array<BoardNode>
|
||||
BoardNode[]
|
||||
> => {
|
||||
acc[curr] = boards[curr].startNodes?.() || [];
|
||||
const nodes = boards[curr].startNodes?.() || [];
|
||||
acc[curr] = nodes.map((node, index) => ({
|
||||
id: index.toString(),
|
||||
...node
|
||||
})) as BoardNode[];
|
||||
return acc;
|
||||
}, {})
|
||||
: {};
|
||||
|
|
Loading…
Reference in a new issue