Merge branch 'side-project'

This commit is contained in:
thepaperpilot 2021-09-04 13:51:06 -05:00
commit 6b501dd240
31 changed files with 868 additions and 199 deletions

2
.replit Normal file
View file

@ -0,0 +1,2 @@
language = "nodejs"
run = "npm run serve"

26
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -2,20 +2,39 @@
<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">
<transition-group name="link" appear>
<g v-for="link in board.links || []" :key="link">
<BoardLink :link="link" />
</g>
</transition-group>
<transition-group name="grow" :duration="500" appear>
<g v-for="node in nodes" :key="node.id" style="transition-duration: 0s">
<BoardNode <BoardNode
v-for="(node, nodeIndex) in nodes"
:key="nodeIndex"
:index="nodeIndex"
:node="node" :node="node"
:nodeType="board.types[node.type]" :nodeType="board.types[node.type]"
:dragging="draggingNode"
:dragged="dragged"
:hasDragged="hasDragged"
:receivingNode="receivingNode?.id === node.id"
@mouseDown="mouseDown"
@endDragging="endDragging"
/> />
</g> </g>
</transition-group>
</g>
</svg> </svg>
</panZoom> </panZoom>
</template> </template>
@ -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>

View 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>

View file

@ -1,17 +1,81 @@
<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
class="body"
:r="size"
:fill="fillColor"
:stroke="outlineColor"
:stroke-width="4"
/>
<circle
class="progressFill"
v-if="progressDisplay === ProgressDisplay.Fill" v-if="progressDisplay === ProgressDisplay.Fill"
:r="size * progress" :r="Math.max(size * progress - 2, 0)"
:fill="progressColor" :fill="progressColor"
/> />
<circle <circle
@ -21,58 +85,115 @@
fill="transparent" fill="transparent"
:stroke-dasharray="(size + 4.5) * 2 * Math.PI" :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
:stroke-width="5" :stroke-width="5"
:stroke-dashoffset="(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI" :stroke-dashoffset="
(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
"
:stroke="progressColor" :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> <text :fill="titleColor" class="node-title">{{ title }}</text>
</g> </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>
</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>

View file

@ -20,7 +20,7 @@
margin: 0 10px; margin: 0 10px;
} }
.row > * { .row > :not(.feature) {
margin: 0; margin: 0;
display: flex; display: flex;
} }

View file

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

View file

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

View file

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

View file

@ -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)!)))

View file

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

View file

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

View file

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

View file

@ -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: {

View file

@ -33,3 +33,8 @@ export enum ProgressDisplay {
Outline = "Outline", Outline = "Outline",
Fill = "Fill" Fill = "Fill"
} }
export enum Shape {
Circle = "Circle",
Diamond = "Triangle"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
import { ComponentOptions } from "vue"; import { Component, ComponentOptions } from "vue";
export type CoercableComponent = string | ComponentOptions; export type CoercableComponent = string | ComponentOptions | Component;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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])) &&

View file

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

View file

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

View file

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