Implemented dropping nodes into each other

Also improved node z-ordering and other related things
This commit is contained in:
thepaperpilot 2021-08-20 00:47:56 -05:00
parent f3b934337f
commit 02443bbb0c
8 changed files with 249 additions and 100 deletions

View file

@ -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;
}
}
}
});

View file

@ -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;
},
mouseLeave() {
this.hovering = false;
}
},
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;
watch: {
onDraggableChanged() {
if (this.dragging && !this.draggable) {
this.$emit("endDragging", this.node.id);
}
}
}

View file

@ -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
}
}
}

View file

@ -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(

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}, {})
: {};