Merge branch 'side-project'
This commit is contained in:
commit
6b501dd240
31 changed files with 868 additions and 199 deletions
2
.replit
Normal file
2
.replit
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
language = "nodejs"
|
||||||
|
run = "npm run serve"
|
26
package-lock.json
generated
26
package-lock.json
generated
|
@ -16,6 +16,7 @@
|
||||||
"vue-panzoom": "^1.1.6",
|
"vue-panzoom": "^1.1.6",
|
||||||
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
||||||
"vue-textarea-autosize": "^1.1.1",
|
"vue-textarea-autosize": "^1.1.1",
|
||||||
|
"vue-toastification": "^2.0.0-rc.1",
|
||||||
"vue-transition-expand": "^0.1.0"
|
"vue-transition-expand": "^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"eslint-plugin-vue": "^7.0.0-alpha.0",
|
"eslint-plugin-vue": "^7.0.0-alpha.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
|
@ -18245,7 +18247,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
|
||||||
"integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
|
"integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prettier-linter-helpers": "^1.0.0"
|
"prettier-linter-helpers": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -18894,8 +18895,7 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "2.2.7",
|
"version": "2.2.7",
|
||||||
|
@ -23775,7 +23775,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-diff": "^1.1.2"
|
"fast-diff": "^1.1.2"
|
||||||
},
|
},
|
||||||
|
@ -27327,6 +27326,14 @@
|
||||||
"deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.",
|
"deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.",
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-toastification": {
|
||||||
|
"version": "2.0.0-rc.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.1.tgz",
|
||||||
|
"integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-transition-expand": {
|
"node_modules/vue-transition-expand": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
|
||||||
|
@ -33899,7 +33906,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
|
||||||
"integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
|
"integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"prettier-linter-helpers": "^1.0.0"
|
"prettier-linter-helpers": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
@ -34361,8 +34367,7 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "2.2.7",
|
"version": "2.2.7",
|
||||||
|
@ -38256,7 +38261,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-diff": "^1.1.2"
|
"fast-diff": "^1.1.2"
|
||||||
}
|
}
|
||||||
|
@ -41126,6 +41130,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vue-toastification": {
|
||||||
|
"version": "2.0.0-rc.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.1.tgz",
|
||||||
|
"integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"vue-transition-expand": {
|
"vue-transition-expand": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"vue-panzoom": "^1.1.6",
|
"vue-panzoom": "^1.1.6",
|
||||||
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
||||||
"vue-textarea-autosize": "^1.1.1",
|
"vue-textarea-autosize": "^1.1.1",
|
||||||
|
"vue-toastification": "^2.0.0-rc.1",
|
||||||
"vue-transition-expand": "^0.1.0"
|
"vue-transition-expand": "^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"eslint-plugin-vue": "^7.0.0-alpha.0",
|
"eslint-plugin-vue": "^7.0.0-alpha.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
|
|
|
@ -2,19 +2,38 @@
|
||||||
<panZoom
|
<panZoom
|
||||||
:style="style"
|
:style="style"
|
||||||
selector="#g1"
|
selector="#g1"
|
||||||
@init="onInit"
|
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
|
||||||
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10 }"
|
|
||||||
ref="stage"
|
ref="stage"
|
||||||
|
@init="onInit"
|
||||||
|
@mousemove="drag"
|
||||||
|
@touchmove="drag"
|
||||||
|
@mousedown="e => mouseDown(e)"
|
||||||
|
@touchstart="e => mouseDown(e)"
|
||||||
|
@mouseup="() => endDragging(dragging)"
|
||||||
|
@touchend="() => endDragging(dragging)"
|
||||||
|
@mouseleave="() => endDragging(dragging)"
|
||||||
>
|
>
|
||||||
<svg class="stage" width="100%" height="100%">
|
<svg class="stage" width="100%" height="100%">
|
||||||
<g id="g1">
|
<g id="g1">
|
||||||
<BoardNode
|
<transition-group name="link" appear>
|
||||||
v-for="(node, nodeIndex) in nodes"
|
<g v-for="link in board.links || []" :key="link">
|
||||||
:key="nodeIndex"
|
<BoardLink :link="link" />
|
||||||
:index="nodeIndex"
|
</g>
|
||||||
:node="node"
|
</transition-group>
|
||||||
:nodeType="board.types[node.type]"
|
<transition-group name="grow" :duration="500" appear>
|
||||||
/>
|
<g v-for="node in nodes" :key="node.id" style="transition-duration: 0s">
|
||||||
|
<BoardNode
|
||||||
|
:node="node"
|
||||||
|
:nodeType="board.types[node.type]"
|
||||||
|
:dragging="draggingNode"
|
||||||
|
:dragged="dragged"
|
||||||
|
:hasDragged="hasDragged"
|
||||||
|
:receivingNode="receivingNode?.id === node.id"
|
||||||
|
@mouseDown="mouseDown"
|
||||||
|
@endDragging="endDragging"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</transition-group>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</panZoom>
|
</panZoom>
|
||||||
|
@ -23,24 +42,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { layers } from "@/game/layers";
|
import { layers } from "@/game/layers";
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { Board } from "@/typings/features/board";
|
import { Board, BoardNode } from "@/typings/features/board";
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
import { InjectLayerMixin } from "@/util/vue";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Board",
|
name: "Board",
|
||||||
mixins: [InjectLayerMixin],
|
mixins: [InjectLayerMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastMousePosition: { x: 0, y: 0 },
|
||||||
|
dragged: { x: 0, y: 0 },
|
||||||
|
dragging: null,
|
||||||
|
hasDragged: false
|
||||||
|
} as {
|
||||||
|
lastMousePosition: { x: number; y: number };
|
||||||
|
dragged: { x: number; y: number };
|
||||||
|
dragging: number | null;
|
||||||
|
hasDragged: boolean;
|
||||||
|
};
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
getZoomLevel: () => (this.$refs.stage as any).$panZoomInstance.getTransform().scale
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
board(): Board {
|
board(): Board {
|
||||||
return layers[this.layer].boards!.data[this.id];
|
return layers[this.layer].boards!.data[this.id];
|
||||||
|
@ -55,13 +82,151 @@ export default defineComponent({
|
||||||
this.board.style
|
this.board.style
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
draggingNode() {
|
||||||
|
return this.dragging == null
|
||||||
|
? null
|
||||||
|
: this.board.nodes.find(node => node.id === this.dragging);
|
||||||
|
},
|
||||||
nodes() {
|
nodes() {
|
||||||
return player.layers[this.layer].boards[this.id];
|
const nodes = this.board.nodes.slice();
|
||||||
|
if (this.draggingNode) {
|
||||||
|
const draggingNode = nodes.splice(nodes.indexOf(this.draggingNode), 1)[0];
|
||||||
|
nodes.push(draggingNode);
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
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) => {
|
||||||
|
if (curr.id === this.draggingNode!.id) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
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: {
|
methods: {
|
||||||
onInit: function(panzoomInstance) {
|
getZoomLevel(): number {
|
||||||
|
return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
|
||||||
|
},
|
||||||
|
onInit(panzoomInstance: any) {
|
||||||
panzoomInstance.setTransformOrigin(null);
|
panzoomInstance.setTransformOrigin(null);
|
||||||
|
},
|
||||||
|
mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, draggable = false) {
|
||||||
|
if (this.dragging == null) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if ("touches" in e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
this.lastMousePosition = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
this.dragged = { x: 0, y: 0 };
|
||||||
|
this.hasDragged = false;
|
||||||
|
|
||||||
|
if (draggable) {
|
||||||
|
this.dragging = nodeID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeID != null) {
|
||||||
|
player.layers[this.layer].boards[this.id].selectedNode = null;
|
||||||
|
player.layers[this.layer].boards[this.id].selectedAction = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drag(e: MouseEvent | TouchEvent) {
|
||||||
|
const zoom = (this.getZoomLevel as () => number)();
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if ("touches" in e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
this.endDragging(this.dragging);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragged = {
|
||||||
|
x: this.dragged.x + (clientX - this.lastMousePosition.x) / zoom,
|
||||||
|
y: this.dragged.y + (clientY - this.lastMousePosition.y) / zoom
|
||||||
|
};
|
||||||
|
this.lastMousePosition = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Math.abs(this.dragged.x) > 10 || Math.abs(this.dragged.y) > 10) {
|
||||||
|
this.hasDragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dragging) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
endDragging(nodeID: number | null) {
|
||||||
|
if (this.dragging != null && this.dragging === nodeID) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const nodes = this.board.nodes;
|
||||||
|
nodes.splice(nodes.indexOf(draggingNode), 1);
|
||||||
|
nodes.push(draggingNode);
|
||||||
|
|
||||||
|
if (receivingNode) {
|
||||||
|
this.board.types[receivingNode.type].onDrop?.(receivingNode, draggingNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dragging = null;
|
||||||
|
} else if (!this.hasDragged) {
|
||||||
|
player.layers[this.layer].boards[this.id].selectedNode = null;
|
||||||
|
player.layers[this.layer].boards[this.id].selectedAction = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -77,4 +242,9 @@ export default defineComponent({
|
||||||
#g1 {
|
#g1 {
|
||||||
transition-duration: 0s;
|
transition-duration: 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-enter-from,
|
||||||
|
.link-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
55
src/components/board/BoardLink.vue
Normal file
55
src/components/board/BoardLink.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<line
|
||||||
|
v-bind="link"
|
||||||
|
class="link"
|
||||||
|
:class="{ pulsing: link.pulsing }"
|
||||||
|
:x1="startPosition.x"
|
||||||
|
:y1="startPosition.y"
|
||||||
|
:x2="endPosition.x"
|
||||||
|
:y2="endPosition.y"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Position } from "@/typings/branches";
|
||||||
|
import { BoardNodeLink } from "@/typings/features/board";
|
||||||
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "BoardLink",
|
||||||
|
props: {
|
||||||
|
link: {
|
||||||
|
type: Object as PropType<BoardNodeLink>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
startPosition(): Position {
|
||||||
|
return this.link.from.position;
|
||||||
|
},
|
||||||
|
endPosition(): Position {
|
||||||
|
return this.link.to.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.link.pulsing {
|
||||||
|
animation: pulsing 2s ease-in infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulsing {
|
||||||
|
0% {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,78 +1,199 @@
|
||||||
<template>
|
<template>
|
||||||
<g
|
<g
|
||||||
class="boardnode"
|
class="boardnode"
|
||||||
:style="{ opacity: dragging ? 0.5 : 1 }"
|
:class="node.type"
|
||||||
|
:style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
|
||||||
:transform="`translate(${position.x},${position.y})`"
|
:transform="`translate(${position.x},${position.y})`"
|
||||||
@mousedown="mouseDown"
|
|
||||||
>
|
>
|
||||||
<circle :r="size + 8" :fill="backgroundColor" stroke="#0F03" :stroke-width="2" />
|
<transition name="actions" appear>
|
||||||
|
<g v-if="selected && actions">
|
||||||
|
<!-- TODO move to separate file -->
|
||||||
|
<g
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="action.id"
|
||||||
|
class="action"
|
||||||
|
:class="{ selected: selectedAction?.id === action.id }"
|
||||||
|
:transform="
|
||||||
|
`translate(
|
||||||
|
${(-size - 30) *
|
||||||
|
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)},
|
||||||
|
${(size + 30) *
|
||||||
|
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
|
||||||
|
)`
|
||||||
|
"
|
||||||
|
@mousedown="e => performAction(e, action)"
|
||||||
|
@touchstart="e => performAction(e, action)"
|
||||||
|
@mouseup="e => actionMouseUp(e, action)"
|
||||||
|
@touchend.stop="e => actionMouseUp(e, action)"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
:fill="
|
||||||
|
action.fillColor
|
||||||
|
? typeof action.fillColor === 'function'
|
||||||
|
? action.fillColor(node)
|
||||||
|
: action.fillColor
|
||||||
|
: fillColor
|
||||||
|
"
|
||||||
|
r="20"
|
||||||
|
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
||||||
|
:stroke="outlineColor"
|
||||||
|
/>
|
||||||
|
<text :fill="titleColor" class="material-icons">{{
|
||||||
|
typeof action.icon === "function" ? action.icon(node) : action.icon
|
||||||
|
}}</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
|
<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"
|
||||||
|
class="receiver"
|
||||||
|
:r="size + 8"
|
||||||
|
:fill="backgroundColor"
|
||||||
|
:stroke="receivingNode ? '#0F0' : '#0F03'"
|
||||||
|
:stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
<circle
|
<circle
|
||||||
v-if="progressDisplay === ProgressDisplay.Fill"
|
class="body"
|
||||||
:r="size * progress"
|
:r="size"
|
||||||
:fill="progressColor"
|
:fill="fillColor"
|
||||||
/>
|
:stroke="outlineColor"
|
||||||
<circle
|
:stroke-width="4"
|
||||||
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>
|
<circle
|
||||||
|
class="progressFill"
|
||||||
|
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"
|
||||||
|
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="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="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="selected && selectedAction"
|
||||||
|
:fill="titleColor"
|
||||||
|
class="node-title"
|
||||||
|
:y="size + 75"
|
||||||
|
>Tap again to confirm</text
|
||||||
|
>
|
||||||
|
</transition>
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import themes from "@/data/themes";
|
import themes from "@/data/themes";
|
||||||
import { ProgressDisplay } from "@/game/enums";
|
import { ProgressDisplay, Shape } from "@/game/enums";
|
||||||
|
import { layers } from "@/game/layers";
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { BoardNode, NodeType } from "@/typings/features/board";
|
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
import { getNodeTypeProperty } from "@/util/features";
|
||||||
import { defineComponent, PropType } from "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({
|
export default defineComponent({
|
||||||
name: "BoardNode",
|
name: "BoardNode",
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
inject: ["getZoomLevel"],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ProgressDisplay,
|
ProgressDisplay,
|
||||||
lastMousePosition: { x: 0, y: 0 },
|
Shape,
|
||||||
dragged: { x: 0, y: 0 },
|
hovering: false,
|
||||||
dragging: false
|
sqrtTwo: Math.sqrt(2)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
emits: ["mouseDown", "endDragging"],
|
||||||
props: {
|
props: {
|
||||||
index: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
node: {
|
node: {
|
||||||
type: Object as PropType<BoardNode>,
|
type: Object as PropType<BoardNode>,
|
||||||
required: true
|
required: true
|
||||||
|
@ -80,104 +201,159 @@ export default defineComponent({
|
||||||
nodeType: {
|
nodeType: {
|
||||||
type: Object as PropType<NodeType>,
|
type: Object as PropType<NodeType>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
dragging: {
|
||||||
|
type: Object as PropType<BoardNode>
|
||||||
|
},
|
||||||
|
dragged: {
|
||||||
|
type: Object as PropType<{ x: number; y: number }>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasDragged: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
receivingNode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
board() {
|
||||||
|
return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
|
||||||
|
},
|
||||||
|
selected() {
|
||||||
|
return this.board.selectedNode === this.node;
|
||||||
|
},
|
||||||
|
selectedAction() {
|
||||||
|
return this.board.selectedAction;
|
||||||
|
},
|
||||||
|
actions(): BoardNodeAction[] | null | undefined {
|
||||||
|
const actions = getNodeTypeProperty(this.nodeType, this.node, "actions") as
|
||||||
|
| BoardNodeAction[]
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
|
if (actions) {
|
||||||
|
return actions.filter(action => {
|
||||||
|
if (action.enabled == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof action.enabled === "function") {
|
||||||
|
return action.enabled();
|
||||||
|
}
|
||||||
|
return action.enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
draggable(): boolean {
|
draggable(): boolean {
|
||||||
return getTypeProperty(this.nodeType, this.node, "draggable");
|
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
|
||||||
},
|
},
|
||||||
position(): { x: number; y: number } {
|
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,
|
x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
|
||||||
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
|
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
|
||||||
}
|
}
|
||||||
: this.node.position;
|
: this.node.position;
|
||||||
},
|
},
|
||||||
|
shape(): Shape {
|
||||||
|
return getNodeTypeProperty(this.nodeType, this.node, "shape");
|
||||||
|
},
|
||||||
size(): number {
|
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 || this.selected) {
|
||||||
|
size *= 1.15;
|
||||||
|
}
|
||||||
|
return size;
|
||||||
},
|
},
|
||||||
title(): string {
|
title(): string {
|
||||||
return getTypeProperty(this.nodeType, this.node, "title");
|
return getNodeTypeProperty(this.nodeType, this.node, "title");
|
||||||
|
},
|
||||||
|
label(): NodeLabel | null | undefined {
|
||||||
|
return getNodeTypeProperty(this.nodeType, this.node, "label");
|
||||||
},
|
},
|
||||||
progress(): number {
|
progress(): number {
|
||||||
return getTypeProperty(this.nodeType, this.node, "progress") || 0;
|
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
|
||||||
},
|
},
|
||||||
backgroundColor(): string {
|
backgroundColor(): string {
|
||||||
return themes[player.theme].variables["--background"];
|
return themes[player.theme].variables["--background"];
|
||||||
},
|
},
|
||||||
outlineColor(): string {
|
outlineColor(): string {
|
||||||
return (
|
return (
|
||||||
getTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
||||||
themes[player.theme].variables["--separator"]
|
themes[player.theme].variables["--separator"]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
fillColor(): string {
|
fillColor(): string {
|
||||||
return (
|
return (
|
||||||
getTypeProperty(this.nodeType, this.node, "fillColor") ||
|
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
|
||||||
themes[player.theme].variables["--secondary-background"]
|
themes[player.theme].variables["--secondary-background"]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
progressColor(): string {
|
progressColor(): string {
|
||||||
return getTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
||||||
},
|
},
|
||||||
titleColor(): string {
|
titleColor(): string {
|
||||||
return (
|
return (
|
||||||
getTypeProperty(this.nodeType, this.node, "titleColor") ||
|
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
|
||||||
themes[player.theme].variables["--color"]
|
themes[player.theme].variables["--color"]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
progressDisplay(): ProgressDisplay {
|
progressDisplay(): ProgressDisplay {
|
||||||
return (
|
return (
|
||||||
getTypeProperty(this.nodeType, this.node, "progressDisplay") ||
|
getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
|
||||||
ProgressDisplay.Outline
|
ProgressDisplay.Outline
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
canAccept(): boolean {
|
||||||
|
if (this.dragging == null || !this.hasDragged) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return typeof this.nodeType.canAccept === "boolean"
|
||||||
|
? this.nodeType.canAccept
|
||||||
|
: this.nodeType.canAccept(this.node, this.dragging);
|
||||||
|
},
|
||||||
|
actionDistance(): number {
|
||||||
|
return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mouseDown(e: MouseEvent) {
|
mouseDown(e: MouseEvent | TouchEvent) {
|
||||||
if (this.draggable) {
|
this.$emit("mouseDown", e, this.node.id, this.draggable);
|
||||||
e.preventDefault();
|
},
|
||||||
e.stopPropagation();
|
mouseUp() {
|
||||||
|
if (!this.hasDragged) {
|
||||||
this.lastMousePosition = {
|
this.nodeType.onClick?.(this.node);
|
||||||
x: e.clientX,
|
|
||||||
y: e.clientY
|
|
||||||
};
|
|
||||||
this.dragged = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
this.dragging = true;
|
|
||||||
document.onmouseup = this.mouseUp;
|
|
||||||
document.onmousemove = this.mouseMove;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mouseMove(e: MouseEvent) {
|
mouseEnter() {
|
||||||
if (this.draggable && this.dragging) {
|
this.hovering = true;
|
||||||
|
},
|
||||||
|
mouseLeave() {
|
||||||
|
this.hovering = false;
|
||||||
|
},
|
||||||
|
performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
|
||||||
|
// If the onClick function made this action selected,
|
||||||
|
// don't propagate the event (which will deselect everything)
|
||||||
|
if (action.onClick(this.node) || this.board.selectedAction?.id === action.id) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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) {
|
actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
|
||||||
if (this.draggable && this.dragging) {
|
if (this.board.selectedAction?.id === action.id) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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;
|
watch: {
|
||||||
|
onDraggableChanged() {
|
||||||
this.dragging = false;
|
if (this.dragging === this.node && !this.draggable) {
|
||||||
document.onmouseup = null;
|
this.$emit("endDragging", this.node.id);
|
||||||
document.onmousemove = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,9 +371,60 @@ export default defineComponent({
|
||||||
dominant-baseline: middle;
|
dominant-baseline: middle;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressRing {
|
.progressRing {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action:not(.boardnode):hover circle,
|
||||||
|
.action:not(.boardnode).selected circle {
|
||||||
|
r: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:not(.boardnode):hover text,
|
||||||
|
.action:not(.boardnode).selected text {
|
||||||
|
font-size: 187.5%; /* 150% * 1.25 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:not(.boardnode) text {
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: central;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
|
.actions-enter-from .action,
|
||||||
|
.actions-leave-to .action {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow-enter-from .node-container,
|
||||||
|
.grow-leave-to .node-container {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row > * {
|
.row > :not(.feature) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
// which will allow us to use them in any template strings anywhere in the project
|
// which will allow us to use them in any template strings anywhere in the project
|
||||||
|
|
||||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||||
import VueTextareaAutosize from "vue-textarea-autosize";
|
import { App } from "vue";
|
||||||
import Sortable from "vue-sortable";
|
|
||||||
import VueNextSelect from "vue-next-select";
|
import VueNextSelect from "vue-next-select";
|
||||||
import "vue-next-select/dist/index.css";
|
import "vue-next-select/dist/index.css";
|
||||||
import panZoom from "vue-panzoom";
|
import panZoom from "vue-panzoom";
|
||||||
import { App } from "vue";
|
import Sortable from "vue-sortable";
|
||||||
|
import VueTextareaAutosize from "vue-textarea-autosize";
|
||||||
|
import Toast from "vue-toastification";
|
||||||
|
import "vue-toastification/dist/index.css";
|
||||||
|
|
||||||
export function registerComponents(vue: App): void {
|
export function registerComponents(vue: App): void {
|
||||||
/* from files */
|
/* from files */
|
||||||
|
@ -25,4 +27,5 @@ export function registerComponents(vue: App): void {
|
||||||
vue.use(Sortable);
|
vue.use(Sortable);
|
||||||
vue.component("vue-select", VueNextSelect);
|
vue.component("vue-select", VueNextSelect);
|
||||||
vue.use(panZoom);
|
vue.use(panZoom);
|
||||||
|
vue.use(Toast);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
Aarex
|
Aarex
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="link" @click="$emit('openDialog', 'Changelog')">
|
<div v-if="false" class="link" @click="$emit('openDialog', 'Changelog')">
|
||||||
Changelog
|
Changelog
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>Time Played: {{ timePlayed }}</div>
|
<div>Time Played: {{ timePlayed }}</div>
|
||||||
<div v-if="hotkeys">
|
<div v-if="hotkeys.length > 0">
|
||||||
<br />
|
<br />
|
||||||
<h4>Hotkeys</h4>
|
<h4>Hotkeys</h4>
|
||||||
<div v-for="key in hotkeys" :key="key.key">
|
<div v-for="key in hotkeys" :key="key.key">
|
||||||
|
|
|
@ -2,14 +2,18 @@
|
||||||
<div class="nav" v-if="useHeader" v-bind="$attrs">
|
<div class="nav" v-if="useHeader" v-bind="$attrs">
|
||||||
<img v-if="banner" :src="banner" height="100%" :alt="title" />
|
<img v-if="banner" :src="banner" height="100%" :alt="title" />
|
||||||
<div v-else class="title">{{ title }}</div>
|
<div v-else class="title">{{ title }}</div>
|
||||||
<div @click="openDialog('Changelog')" class="version-container">
|
<div
|
||||||
|
@click="openDialog('Changelog')"
|
||||||
|
class="version-container"
|
||||||
|
style="pointer-events: none"
|
||||||
|
>
|
||||||
<tooltip display="Changelog" bottom class="version"
|
<tooltip display="Changelog" bottom class="version"
|
||||||
><span>v{{ version }}</span></tooltip
|
><span>v{{ version }}</span></tooltip
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow: 1; cursor: unset;"></div>
|
<div style="flex-grow: 1; cursor: unset;"></div>
|
||||||
<div class="discord">
|
<div class="discord">
|
||||||
<img src="images/discord.png" @click="window.open(discordLink, 'mywindow')" />
|
<img src="images/discord.png" @click="openDiscord" />
|
||||||
<ul class="discord-links">
|
<ul class="discord-links">
|
||||||
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
|
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
|
||||||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||||
|
@ -139,6 +143,7 @@ export default defineComponent({
|
||||||
width: 46px;
|
width: 46px;
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-nav {
|
.overlay-nav {
|
||||||
|
@ -169,6 +174,9 @@ export default defineComponent({
|
||||||
|
|
||||||
.nav > .title {
|
.nav > .title {
|
||||||
width: unset;
|
width: unset;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav .saves,
|
.nav .saves,
|
||||||
|
|
|
@ -181,6 +181,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
openSave(id: string) {
|
openSave(id: string) {
|
||||||
this.saves[player.id].time = player.time;
|
this.saves[player.id].time = player.time;
|
||||||
|
save();
|
||||||
loadSave(this.saves[id]);
|
loadSave(this.saves[id]);
|
||||||
const modData = JSON.parse(
|
const modData = JSON.parse(
|
||||||
decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
|
decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
:layer="tab"
|
:layer="tab"
|
||||||
:index="index"
|
:index="index"
|
||||||
v-else-if="tab in components"
|
v-else-if="tab in components"
|
||||||
:minimizable="true"
|
:minimizable="minimizable[tab]"
|
||||||
:tab="() => $refs[`tab-${index}`]"
|
:tab="() => $refs[`tab-${index}`]"
|
||||||
/>
|
/>
|
||||||
<component :is="tab" :index="index" v-else />
|
<component :is="tab" :index="index" v-else />
|
||||||
|
@ -47,6 +47,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
minimizable() {
|
||||||
|
return Object.keys(layers).reduce((acc: Record<string, boolean>, curr) => {
|
||||||
|
acc[curr] = layers[curr].minimizable !== false;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -95,6 +95,7 @@ export default defineComponent({
|
||||||
background-color: var(--background-tooltip);
|
background-color: var(--background-tooltip);
|
||||||
color: var(--points);
|
color: var(--points);
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shown {
|
.shown {
|
||||||
|
|
|
@ -37,11 +37,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
return this.options.stroke!;
|
return this.options.stroke!;
|
||||||
},
|
},
|
||||||
strokeWidth(): string {
|
strokeWidth(): number | string {
|
||||||
if (typeof this.options === "string" || !("stroke-width" in this.options)) {
|
if (typeof this.options === "string" || !("strokeWidth" in this.options)) {
|
||||||
return "15px";
|
return "15";
|
||||||
}
|
}
|
||||||
return this.options["stroke-width"]!;
|
return this.options["strokeWidth"]!;
|
||||||
},
|
},
|
||||||
startPosition(): Position {
|
startPosition(): Position {
|
||||||
const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
|
const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
|
||||||
|
|
|
@ -23,6 +23,7 @@ const defaultTheme: Theme = {
|
||||||
export enum Themes {
|
export enum Themes {
|
||||||
Classic = "classic",
|
Classic = "classic",
|
||||||
Paper = "paper",
|
Paper = "paper",
|
||||||
|
Nordic = "nordic",
|
||||||
Aqua = "aqua"
|
Aqua = "aqua"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +45,27 @@ export default {
|
||||||
stackedInfoboxes: true,
|
stackedInfoboxes: true,
|
||||||
floatingTabs: false
|
floatingTabs: false
|
||||||
} as Theme,
|
} as Theme,
|
||||||
|
// Based on https://www.nordtheme.com
|
||||||
|
nordic: {
|
||||||
|
...defaultTheme,
|
||||||
|
variables: {
|
||||||
|
...defaultTheme.variables,
|
||||||
|
"--color": "#D8DEE9",
|
||||||
|
"--points": "#E5E9F0",
|
||||||
|
"--background": "#2E3440",
|
||||||
|
"--secondary-background": "#3B4252",
|
||||||
|
"--locked": "#3B4252",
|
||||||
|
"--bought": "#8FBCBB",
|
||||||
|
"--link": "#88C0D0",
|
||||||
|
"--separator": "#3B4252",
|
||||||
|
"--border-radius": "4px",
|
||||||
|
"--danger": "#D08770",
|
||||||
|
"--modal-border": "solid 2px #3B4252",
|
||||||
|
"--feature-margin": "5px"
|
||||||
|
},
|
||||||
|
stackedInfoboxes: true,
|
||||||
|
floatingTabs: false
|
||||||
|
} as Theme,
|
||||||
aqua: {
|
aqua: {
|
||||||
...defaultTheme,
|
...defaultTheme,
|
||||||
variables: {
|
variables: {
|
||||||
|
|
|
@ -33,3 +33,8 @@ export enum ProgressDisplay {
|
||||||
Outline = "Outline",
|
Outline = "Outline",
|
||||||
Fill = "Fill"
|
Fill = "Fill"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Shape {
|
||||||
|
Circle = "Circle",
|
||||||
|
Diamond = "Triangle"
|
||||||
|
}
|
||||||
|
|
|
@ -68,6 +68,14 @@ function updateLayers(diff: DecimalSource) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
layers[layer].update?.(diff);
|
layers[layer].update?.(diff);
|
||||||
|
if (layers[layer].boards && layers[layer].boards?.data) {
|
||||||
|
Object.values(layers[layer].boards!.data!).forEach(board => {
|
||||||
|
board.nodes.forEach(node => {
|
||||||
|
const nodeType = board.types[node.type];
|
||||||
|
nodeType.update?.(node, diff);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Automate each active layer
|
// Automate each active layer
|
||||||
activeLayers.forEach(layer => {
|
activeLayers.forEach(layer => {
|
||||||
|
@ -164,6 +172,8 @@ function update() {
|
||||||
modUpdate(diff);
|
modUpdate(diff);
|
||||||
updateOOMPS(trueDiff);
|
updateOOMPS(trueDiff);
|
||||||
updateLayers(diff);
|
updateLayers(diff);
|
||||||
|
|
||||||
|
player.justLoaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function startGameLoop(): void {
|
export default function startGameLoop(): void {
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { createGridProxy, createLayerProxy } from "@/util/proxies";
|
||||||
import { applyPlayerData } from "@/util/save";
|
import { applyPlayerData } from "@/util/save";
|
||||||
import clone from "lodash.clonedeep";
|
import clone from "lodash.clonedeep";
|
||||||
import { isRef } from "vue";
|
import { isRef } from "vue";
|
||||||
import { ProgressDisplay } from "./enums";
|
import { ProgressDisplay, Shape } from "./enums";
|
||||||
import { default as playerProxy } from "./player";
|
import { default as playerProxy } from "./player";
|
||||||
|
|
||||||
export const layers: Record<string, Readonly<Layer>> = {};
|
export const layers: Record<string, Readonly<Layer>> = {};
|
||||||
|
@ -73,7 +73,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
|
||||||
buyables: getStartingBuyables(layer.buyables?.data),
|
buyables: getStartingBuyables(layer.buyables?.data),
|
||||||
clickables: getStartingClickables(layer.clickables?.data),
|
clickables: getStartingClickables(layer.clickables?.data),
|
||||||
challenges: getStartingChallenges(layer.challenges?.data),
|
challenges: getStartingChallenges(layer.challenges?.data),
|
||||||
boards: getStartingBoards(layer.boards?.data),
|
boards: player.layers[layer.id]?.boards || getStartingBoards(layer.boards?.data),
|
||||||
grids: {},
|
grids: {},
|
||||||
confirmRespecBuyables: false,
|
confirmRespecBuyables: false,
|
||||||
...(layer.startData?.() || {})
|
...(layer.startData?.() || {})
|
||||||
|
@ -432,23 +432,67 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
|
||||||
for (const id in layer.boards.data) {
|
for (const id in layer.boards.data) {
|
||||||
setDefault(layer.boards.data[id], "width", "100%");
|
setDefault(layer.boards.data[id], "width", "100%");
|
||||||
setDefault(layer.boards.data[id], "height", "400px");
|
setDefault(layer.boards.data[id], "height", "400px");
|
||||||
|
setDefault(layer.boards.data[id], "nodes", function() {
|
||||||
|
return playerProxy.layers[this.layer].boards[this.id].nodes;
|
||||||
|
});
|
||||||
|
setDefault(layer.boards.data[id], "selectedNode", function() {
|
||||||
|
return playerProxy.layers[this.layer].boards[this.id].nodes.find(
|
||||||
|
node => node.id === playerProxy.layers[this.layer].boards[this.id].selectedNode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setDefault(layer.boards.data[id], "selectedAction", function() {
|
||||||
|
if (this.selectedNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nodeType = layers[this.layer].boards!.data[this.id].types[
|
||||||
|
this.selectedNode.type
|
||||||
|
];
|
||||||
|
if (nodeType.actions === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const actions =
|
||||||
|
typeof nodeType.actions === "function"
|
||||||
|
? nodeType.actions(this.selectedNode)
|
||||||
|
: nodeType.actions;
|
||||||
|
return actions?.find(
|
||||||
|
action =>
|
||||||
|
action.id === playerProxy.layers[this.layer].boards[this.id].selectedAction
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setDefault(layer.boards.data[id], "links", function() {
|
||||||
|
if (this.selectedAction == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.selectedAction.links) {
|
||||||
|
if (typeof this.selectedAction.links === "function") {
|
||||||
|
return this.selectedAction.links(this.selectedNode);
|
||||||
|
}
|
||||||
|
return this.selectedAction.links;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
for (const nodeType in layer.boards.data[id].types) {
|
for (const nodeType in layer.boards.data[id].types) {
|
||||||
layer.boards.data[id].types[nodeType].layer = layer.id;
|
layer.boards.data[id].types[nodeType].layer = layer.id;
|
||||||
layer.boards.data[id].types[nodeType].id = id;
|
layer.boards.data[id].types[nodeType].id = id;
|
||||||
layer.boards.data[id].types[nodeType].type = nodeType;
|
layer.boards.data[id].types[nodeType].type = nodeType;
|
||||||
setDefault(layer.boards.data[id].types[nodeType], "size", 50);
|
setDefault(layer.boards.data[id].types[nodeType], "size", 50);
|
||||||
setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
|
setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
|
||||||
|
setDefault(layer.boards.data[id].types[nodeType], "shape", Shape.Circle);
|
||||||
setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
|
setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
|
||||||
|
setDefault(layer.boards.data[id].types[nodeType], "actionDistance", Math.PI / 6);
|
||||||
setDefault(
|
setDefault(
|
||||||
layer.boards.data[id].types[nodeType],
|
layer.boards.data[id].types[nodeType],
|
||||||
"progressDisplay",
|
"progressDisplay",
|
||||||
ProgressDisplay.Fill
|
ProgressDisplay.Fill
|
||||||
);
|
);
|
||||||
setDefault(layer.boards.data[id].types[nodeType], "nodes", function() {
|
setDefault(layer.boards.data[id].types[nodeType], "nodes", function() {
|
||||||
return playerProxy.layers[this.layer].boards[this.id].filter(
|
return playerProxy.layers[this.layer].boards[this.id].nodes.filter(
|
||||||
node => node.type === this.type
|
node => node.type === this.type
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
setDefault(layer.boards.data[id].types[nodeType], "onClick", function(node) {
|
||||||
|
playerProxy.layers[this.layer].boards[this.id].selectedNode = node.id;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,12 @@ const state = reactive<PlayerData>({
|
||||||
showTPS: true,
|
showTPS: true,
|
||||||
msDisplay: MilestoneDisplay.All,
|
msDisplay: MilestoneDisplay.All,
|
||||||
hideChallenges: false,
|
hideChallenges: false,
|
||||||
theme: Themes.Paper,
|
theme: Themes.Nordic,
|
||||||
subtabs: {},
|
subtabs: {},
|
||||||
minimized: {},
|
minimized: {},
|
||||||
modID: "",
|
modID: "",
|
||||||
modVersion: "",
|
modVersion: "",
|
||||||
|
justLoaded: false,
|
||||||
hasNaN: false,
|
hasNaN: false,
|
||||||
NaNPath: [],
|
NaNPath: [],
|
||||||
NaNReceiver: null,
|
NaNReceiver: null,
|
||||||
|
@ -104,6 +105,12 @@ const playerHandler: ProxyHandler<Record<string, any>> = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
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(
|
export default window.player = new Proxy(
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 640px;
|
|
||||||
transition: none;
|
transition: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -63,3 +62,7 @@ a:hover,
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Vue-Toastification__toast {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
3
src/typings/branches.d.ts
vendored
3
src/typings/branches.d.ts
vendored
|
@ -17,9 +17,10 @@ export interface BranchOptions {
|
||||||
target?: string;
|
target?: string;
|
||||||
featureType?: string;
|
featureType?: string;
|
||||||
stroke?: string;
|
stroke?: string;
|
||||||
"stroke-width"?: string;
|
strokeWidth?: number | string;
|
||||||
startOffset?: Position;
|
startOffset?: Position;
|
||||||
endOffset?: Position;
|
endOffset?: Position;
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Position {
|
export interface Position {
|
||||||
|
|
4
src/typings/component.d.ts
vendored
4
src/typings/component.d.ts
vendored
|
@ -1,3 +1,3 @@
|
||||||
import { ComponentOptions } from "vue";
|
import { Component, ComponentOptions } from "vue";
|
||||||
|
|
||||||
export type CoercableComponent = string | ComponentOptions;
|
export type CoercableComponent = string | ComponentOptions | Component;
|
||||||
|
|
56
src/typings/features/board.d.ts
vendored
56
src/typings/features/board.d.ts
vendored
|
@ -1,38 +1,48 @@
|
||||||
|
import { Shape } from "@/game/enums";
|
||||||
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
import { State } from "../state";
|
import { State } from "../state";
|
||||||
import { Feature, RawFeature } from "./feature";
|
import { Feature, RawFeature } from "./feature";
|
||||||
|
|
||||||
export interface BoardNode {
|
export interface BoardNode {
|
||||||
|
id: number;
|
||||||
position: {
|
position: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
type: string;
|
type: string;
|
||||||
data?: State;
|
data?: State;
|
||||||
|
pinned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardOption {
|
export interface BoardData {
|
||||||
text: string;
|
nodes: BoardNode[];
|
||||||
selected: (node: BoardNode) => void;
|
selectedNode: number | null;
|
||||||
|
selectedAction: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Board extends Feature {
|
export interface Board extends Feature {
|
||||||
startNodes: () => BoardNode[];
|
startNodes: () => Omit<BoardNode, "id">[];
|
||||||
style?: Partial<CSSStyleDeclaration>;
|
style?: Partial<CSSStyleDeclaration>;
|
||||||
height: string;
|
height: string;
|
||||||
width: string;
|
width: string;
|
||||||
types: Record<string, NodeType>;
|
types: Record<string, NodeType>;
|
||||||
|
nodes: BoardNode[];
|
||||||
|
selectedNode: BoardNode | null;
|
||||||
|
selectedAction: BoardNodeAction | null;
|
||||||
|
links: BoardNodeLink[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RawBoard = Omit<RawFeature<Board>, "types"> & {
|
export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
|
||||||
startNodes: () => BoardNode[];
|
startNodes: () => Omit<BoardNode, "id">[];
|
||||||
types: Record<string, RawFeature<NodeType>>;
|
types: Record<string, RawFeature<NodeType>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface NodeType extends Feature {
|
export interface NodeType extends Feature {
|
||||||
tooltip?: string | ((node: BoardNode) => string);
|
|
||||||
title: string | ((node: BoardNode) => string);
|
title: string | ((node: BoardNode) => string);
|
||||||
size: number | ((node: BoardNode) => number);
|
label?: NodeLabel | null | ((node: BoardNode) => NodeLabel | null);
|
||||||
|
size: number | string | ((node: BoardNode) => number | string);
|
||||||
draggable: boolean | ((node: BoardNode) => boolean);
|
draggable: boolean | ((node: BoardNode) => boolean);
|
||||||
|
shape: Shape | ((node: BoardNode) => Shape);
|
||||||
canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean);
|
canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean);
|
||||||
progress?: number | ((node: BoardNode) => number);
|
progress?: number | ((node: BoardNode) => number);
|
||||||
progressDisplay: ProgressDisplay | ((node: BoardNode) => ProgressDisplay);
|
progressDisplay: ProgressDisplay | ((node: BoardNode) => ProgressDisplay);
|
||||||
|
@ -40,6 +50,34 @@ export interface NodeType extends Feature {
|
||||||
fillColor?: string | ((node: BoardNode) => string);
|
fillColor?: string | ((node: BoardNode) => string);
|
||||||
outlineColor?: string | ((node: BoardNode) => string);
|
outlineColor?: string | ((node: BoardNode) => string);
|
||||||
titleColor?: string | ((node: BoardNode) => string);
|
titleColor?: string | ((node: BoardNode) => string);
|
||||||
onClick: (node: BoardNode) => void;
|
actions?: BoardNodeAction[] | ((node: BoardNode) => BoardNodeAction[]);
|
||||||
|
actionDistance: number | ((node: BoardNode) => number);
|
||||||
|
onClick?: (node: BoardNode) => void;
|
||||||
|
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
|
||||||
|
update?: (node: BoardNode, diff: DecimalSource) => void;
|
||||||
nodes: BoardNode[];
|
nodes: BoardNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BoardNodeAction {
|
||||||
|
id: string;
|
||||||
|
icon: string | ((node: BoardNode) => string);
|
||||||
|
fillColor?: string | ((node: BoardNode) => string);
|
||||||
|
tooltip: string | ((node: BoardNode) => string);
|
||||||
|
onClick: (node: BoardNode) => boolean | undefined;
|
||||||
|
links?: BoardNodeLink[] | ((node: BoardNode) => BoardNodeLink[]);
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoardNodeLink {
|
||||||
|
from: BoardNode;
|
||||||
|
to: BoardNode;
|
||||||
|
stroke: string;
|
||||||
|
pulsing?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeLabel {
|
||||||
|
text: string;
|
||||||
|
color?: string;
|
||||||
|
pulsing?: boolean;
|
||||||
|
}
|
||||||
|
|
5
src/typings/player.d.ts
vendored
5
src/typings/player.d.ts
vendored
|
@ -1,7 +1,7 @@
|
||||||
import { Themes } from "@/data/themes";
|
import { Themes } from "@/data/themes";
|
||||||
import { DecimalSource } from "@/lib/break_eternity";
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
import Decimal from "@/util/bignum";
|
import Decimal from "@/util/bignum";
|
||||||
import { BoardNode } from "./features/board";
|
import { BoardData } from "./features/board";
|
||||||
import { MilestoneDisplay } from "./features/milestone";
|
import { MilestoneDisplay } from "./features/milestone";
|
||||||
import { State } from "./state";
|
import { State } from "./state";
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ export interface PlayerData {
|
||||||
minimized: Record<string, boolean>;
|
minimized: Record<string, boolean>;
|
||||||
modID: string;
|
modID: string;
|
||||||
modVersion: string;
|
modVersion: string;
|
||||||
|
justLoaded: boolean;
|
||||||
hasNaN: boolean;
|
hasNaN: boolean;
|
||||||
NaNPath?: Array<string>;
|
NaNPath?: Array<string>;
|
||||||
NaNReceiver?: Record<string, unknown> | null;
|
NaNReceiver?: Record<string, unknown> | null;
|
||||||
|
@ -62,7 +63,7 @@ export interface LayerSaveData {
|
||||||
clickables: Record<string, State>;
|
clickables: Record<string, State>;
|
||||||
challenges: Record<string, Decimal>;
|
challenges: Record<string, Decimal>;
|
||||||
grids: Record<string, Record<string, State>>;
|
grids: Record<string, Record<string, State>>;
|
||||||
boards: Record<string, Array<BoardNode>>;
|
boards: Record<string, BoardData>;
|
||||||
confirmRespecBuyables: boolean;
|
confirmRespecBuyables: boolean;
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,48 +114,45 @@ export function formatWhole(num: DecimalSource): string {
|
||||||
return format(num, 0);
|
return format(num, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTime(s: DecimalSource): string {
|
export function formatTime(seconds: DecimalSource): string {
|
||||||
if (Decimal.gt(s, 2 ^ 51)) {
|
if (Decimal.lt(seconds, 0)) {
|
||||||
// integer precision limit
|
return "-" + formatTime(Decimal.neg(seconds));
|
||||||
return format(Decimal.div(s, 31536000)) + "y";
|
|
||||||
}
|
}
|
||||||
s = new Decimal(s).toNumber();
|
if (Decimal.gt(seconds, 2 ** 51)) {
|
||||||
if (s < 60) {
|
// integer precision limit
|
||||||
return format(s) + "s";
|
return format(Decimal.div(seconds, 31536000)) + "y";
|
||||||
} else if (s < 3600) {
|
}
|
||||||
return formatWhole(Math.floor(s / 60)) + "m " + format(s % 60) + "s";
|
seconds = new Decimal(seconds).toNumber();
|
||||||
} else if (s < 86400) {
|
if (seconds < 60) {
|
||||||
|
return format(seconds) + "s";
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
return formatWhole(Math.floor(seconds / 60)) + "m " + format(seconds % 60) + "s";
|
||||||
|
} else if (seconds < 86400) {
|
||||||
return (
|
return (
|
||||||
formatWhole(Math.floor(s / 3600)) +
|
formatWhole(Math.floor(seconds / 3600)) +
|
||||||
"h " +
|
"h " +
|
||||||
formatWhole(Math.floor(s / 60) % 60) +
|
formatWhole(Math.floor(seconds / 60) % 60) +
|
||||||
"m " +
|
"m " +
|
||||||
format(s % 60) +
|
formatWhole(seconds % 60) +
|
||||||
"s"
|
"s"
|
||||||
);
|
);
|
||||||
} else if (s < 31536000) {
|
} else if (seconds < 31536000) {
|
||||||
return (
|
return (
|
||||||
formatWhole(Math.floor(s / 84600) % 365) +
|
formatWhole(Math.floor(seconds / 84600) % 365) +
|
||||||
"d " +
|
"d " +
|
||||||
formatWhole(Math.floor(s / 3600) % 24) +
|
formatWhole(Math.floor(seconds / 3600) % 24) +
|
||||||
"h " +
|
"h " +
|
||||||
formatWhole(Math.floor(s / 60) % 60) +
|
formatWhole(Math.floor(seconds / 60) % 60) +
|
||||||
"m " +
|
"m"
|
||||||
format(s % 60) +
|
|
||||||
"s"
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
formatWhole(Math.floor(s / 31536000)) +
|
formatWhole(Math.floor(seconds / 31536000)) +
|
||||||
"y " +
|
"y " +
|
||||||
formatWhole(Math.floor(s / 84600) % 365) +
|
formatWhole(Math.floor(seconds / 84600) % 365) +
|
||||||
"d " +
|
"d " +
|
||||||
formatWhole(Math.floor(s / 3600) % 24) +
|
formatWhole(Math.floor(seconds / 3600) % 24) +
|
||||||
"h " +
|
"h"
|
||||||
formatWhole(Math.floor(s / 60) % 60) +
|
|
||||||
"m " +
|
|
||||||
format(s % 60) +
|
|
||||||
"s"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { layers } from "@/game/layers";
|
import { layers } from "@/game/layers";
|
||||||
|
import { NodeType, BoardNode, Board } from "@/typings/features/board";
|
||||||
import { GridCell } from "@/typings/features/grid";
|
import { GridCell } from "@/typings/features/grid";
|
||||||
import { State } from "@/typings/state";
|
import { State } from "@/typings/state";
|
||||||
import Decimal, { DecimalSource } from "@/util/bignum";
|
import Decimal, { DecimalSource } from "@/util/bignum";
|
||||||
|
@ -87,3 +88,32 @@ export function achievementEffect(layer: string, id: string | number): State | u
|
||||||
export function gridEffect(layer: string, id: string, cell: string | number): State | undefined {
|
export function gridEffect(layer: string, id: string, cell: string | number): State | undefined {
|
||||||
return (layers[layer].grids?.data[id][cell] as GridCell).effect;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueNodeID(board: Board): number {
|
||||||
|
let id = 0;
|
||||||
|
board.nodes.forEach(node => {
|
||||||
|
if (node.id >= id) {
|
||||||
|
id = node.id + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { hotkeys, layers } from "@/game/layers";
|
import { hotkeys, layers } from "@/game/layers";
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { CacheableFunction } from "@/typings/cacheableFunction";
|
import { CacheableFunction } from "@/typings/cacheableFunction";
|
||||||
import { Board, BoardNode, RawBoard } from "@/typings/features/board";
|
import { Board, BoardData, BoardNode, RawBoard } from "@/typings/features/board";
|
||||||
import { Buyable } from "@/typings/features/buyable";
|
import { Buyable } from "@/typings/features/buyable";
|
||||||
import { Challenge } from "@/typings/features/challenge";
|
import { Challenge } from "@/typings/features/challenge";
|
||||||
import { Clickable } from "@/typings/features/clickable";
|
import { Clickable } from "@/typings/features/clickable";
|
||||||
|
@ -73,13 +73,21 @@ export function getStartingChallenges(
|
||||||
|
|
||||||
export function getStartingBoards(
|
export function getStartingBoards(
|
||||||
boards?: Record<string, Board> | Record<string, RawBoard> | undefined
|
boards?: Record<string, Board> | Record<string, RawBoard> | undefined
|
||||||
): Record<string, Array<BoardNode>> {
|
): Record<string, BoardData> {
|
||||||
return boards
|
return boards
|
||||||
? Object.keys(boards).reduce((acc: Record<string, Array<BoardNode>>, curr: string): Record<
|
? Object.keys(boards).reduce((acc: Record<string, BoardData>, curr: string): Record<
|
||||||
string,
|
string,
|
||||||
Array<BoardNode>
|
BoardData
|
||||||
> => {
|
> => {
|
||||||
acc[curr] = boards[curr].startNodes?.() || [];
|
const nodes = boards[curr].startNodes?.() || [];
|
||||||
|
acc[curr] = {
|
||||||
|
nodes: nodes.map((node, index) => ({
|
||||||
|
id: index,
|
||||||
|
...node
|
||||||
|
})),
|
||||||
|
selectedNode: null,
|
||||||
|
selectedAction: null
|
||||||
|
} as BoardData;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
: {};
|
: {};
|
||||||
|
|
|
@ -30,7 +30,7 @@ function travel(
|
||||||
objectProxy: Record<string, any>
|
objectProxy: Record<string, any>
|
||||||
) {
|
) {
|
||||||
for (const key in object) {
|
for (const key in object) {
|
||||||
if (object[key] == undefined || object[key].isProxy) {
|
if (object[key] == undefined || object[key].isProxy || isRef(object[key])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isFunction(object[key])) {
|
if (isFunction(object[key])) {
|
||||||
|
@ -43,7 +43,8 @@ function travel(
|
||||||
object[key] = computed(object[key].bind(objectProxy));
|
object[key] = computed(object[key].bind(objectProxy));
|
||||||
} else if (
|
} else if (
|
||||||
(isPlainObject(object[key]) || Array.isArray(object[key])) &&
|
(isPlainObject(object[key]) || Array.isArray(object[key])) &&
|
||||||
!(object[key] instanceof Decimal)
|
!(object[key] instanceof Decimal) &&
|
||||||
|
typeof object[key].render !== "function"
|
||||||
) {
|
) {
|
||||||
object[key] = callback(object[key]);
|
object[key] = callback(object[key]);
|
||||||
}
|
}
|
||||||
|
@ -62,7 +63,11 @@ const layerHandler: ProxyHandler<Record<string, any>> = {
|
||||||
|
|
||||||
if (isRef(target[key])) {
|
if (isRef(target[key])) {
|
||||||
return target[key].value;
|
return target[key].value;
|
||||||
} else if (target[key].isProxy || target[key] instanceof Decimal) {
|
} else if (
|
||||||
|
target[key].isProxy ||
|
||||||
|
target[key] instanceof Decimal ||
|
||||||
|
typeof target[key].render === "function"
|
||||||
|
) {
|
||||||
return target[key];
|
return target[key];
|
||||||
} else if (
|
} else if (
|
||||||
(isPlainObject(target[key]) || Array.isArray(target[key])) &&
|
(isPlainObject(target[key]) || Array.isArray(target[key])) &&
|
||||||
|
|
|
@ -18,18 +18,20 @@ export function getInitialStore(playerData: Partial<PlayerData> = {}): PlayerDat
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
autosave: true,
|
autosave: true,
|
||||||
offlineProd: true,
|
offlineProd: true,
|
||||||
|
offlineTime: new Decimal(0),
|
||||||
timePlayed: new Decimal(0),
|
timePlayed: new Decimal(0),
|
||||||
keepGoing: false,
|
keepGoing: false,
|
||||||
lastTenTicks: [],
|
lastTenTicks: [],
|
||||||
showTPS: true,
|
showTPS: true,
|
||||||
msDisplay: MilestoneDisplay.All,
|
msDisplay: MilestoneDisplay.All,
|
||||||
hideChallenges: false,
|
hideChallenges: false,
|
||||||
theme: Themes.Paper,
|
theme: Themes.Nordic,
|
||||||
subtabs: {},
|
subtabs: {},
|
||||||
minimized: {},
|
minimized: {},
|
||||||
modID: modInfo.id,
|
modID: modInfo.id,
|
||||||
modVersion: modInfo.versionNumber,
|
modVersion: modInfo.versionNumber,
|
||||||
layers: {},
|
layers: {},
|
||||||
|
justLoaded: false,
|
||||||
...getStartingData(),
|
...getStartingData(),
|
||||||
|
|
||||||
// Values that don't get loaded/saved
|
// Values that don't get loaded/saved
|
||||||
|
@ -147,6 +149,7 @@ export async function loadSave(playerData: Partial<PlayerData>): Promise<void> {
|
||||||
delete player.layers[prop];
|
delete player.layers[prop];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
player.justLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyPlayerData<T extends Record<string, any>>(
|
export function applyPlayerData<T extends Record<string, any>>(
|
||||||
|
@ -186,6 +189,9 @@ window.onbeforeunload = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.save = save;
|
window.save = save;
|
||||||
window.hardReset = () => {
|
export const hardReset = (window.hardReset = async () => {
|
||||||
loadSave(newSave());
|
await loadSave(newSave());
|
||||||
};
|
const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!))));
|
||||||
|
modData.active = player.id;
|
||||||
|
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
|
||||||
|
});
|
||||||
|
|
|
@ -37,12 +37,13 @@ const data = function(): Record<string, unknown> {
|
||||||
return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
|
return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
|
||||||
};
|
};
|
||||||
export function coerceComponent(
|
export function coerceComponent(
|
||||||
component: string | ComponentOptions,
|
component: string | ComponentOptions | Component,
|
||||||
defaultWrapper = "span"
|
defaultWrapper = "span",
|
||||||
|
allowComponentNames = true
|
||||||
): Component | string {
|
): Component | string {
|
||||||
if (typeof component === "string") {
|
if (typeof component === "string") {
|
||||||
component = component.trim();
|
component = component.trim();
|
||||||
if (!(component in vue._context.components)) {
|
if (!allowComponentNames || !(component in vue._context.components)) {
|
||||||
if (component.charAt(0) !== "<") {
|
if (component.charAt(0) !== "<") {
|
||||||
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
|
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +51,7 @@ export function coerceComponent(
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
template: component,
|
template: component,
|
||||||
data,
|
data,
|
||||||
inject: ["tab"],
|
mixins: [InjectLayerMixin],
|
||||||
methods: {
|
methods: {
|
||||||
hasUpgrade,
|
hasUpgrade,
|
||||||
hasMilestone,
|
hasMilestone,
|
||||||
|
@ -109,7 +110,7 @@ export const InjectLayerMixin = {
|
||||||
layer: {
|
layer: {
|
||||||
type: String,
|
type: String,
|
||||||
default(): string {
|
default(): string {
|
||||||
return (inject("tab") as { layer: string }).layer;
|
return (inject("tab", { layer: "" }) as { layer: string }).layer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
publicPath: process.env.NODE_ENV === "production" ? "/The-Modding-Tree-X" : "/",
|
publicPath: process.env.NODE_ENV === "production" ? "./" : "/",
|
||||||
runtimeCompiler: true,
|
runtimeCompiler: true,
|
||||||
chainWebpack(config) {
|
chainWebpack(config) {
|
||||||
config.resolve.alias.delete("@");
|
config.resolve.alias.delete("@");
|
||||||
|
@ -7,5 +7,11 @@ module.exports = {
|
||||||
.plugin("tsconfig-paths")
|
.plugin("tsconfig-paths")
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
.use(require("tsconfig-paths-webpack-plugin"));
|
.use(require("tsconfig-paths-webpack-plugin"));
|
||||||
|
// Remove this if/when all "core" code has no non-ignored more type errors
|
||||||
|
// https://github.com/vuejs/vue-cli/issues/3157#issuecomment-657090338
|
||||||
|
config.plugins.delete('fork-ts-checker');
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
disableHostCheck: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue