New layer API WIP
This commit is contained in:
parent
e499447cf5
commit
6f781b33fa
159 changed files with 15366 additions and 26427 deletions
|
@ -1,7 +1,8 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true
|
node: true,
|
||||||
|
'vue/setup-compiler-macros': true
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:vue/vue3-essential",
|
"plugin:vue/vue3-essential",
|
||||||
|
@ -15,6 +16,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
|
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||||
|
"vue/script-setup-uses-vars": "error",
|
||||||
|
"vue/no-mutating-props": "off"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
3
.vs/ProjectSettings.json
Normal file
3
.vs/ProjectSettings.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"CurrentProjectSetting": null
|
||||||
|
}
|
10
.vs/VSWorkspaceState.json
Normal file
10
.vs/VSWorkspaceState.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"ExpandedNodes": [
|
||||||
|
"",
|
||||||
|
"\\src",
|
||||||
|
"\\src\\typings",
|
||||||
|
"\\src\\util"
|
||||||
|
],
|
||||||
|
"SelectedNode": "\\src\\App.vue",
|
||||||
|
"PreviewInSolutionExplorer": false
|
||||||
|
}
|
BIN
.vs/slnx.sqlite
Normal file
BIN
.vs/slnx.sqlite
Normal file
Binary file not shown.
|
@ -6,6 +6,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"@vue/babel-plugin-jsx"
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
21769
package-lock.json
generated
21769
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -10,9 +10,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"vue": "^3.2.2",
|
"nanoevents": "^6.0.2",
|
||||||
"vue-class-component": "^8.0.0-rc.1",
|
"vue": "^3.2.26",
|
||||||
"vue-next-select": "^2.9.0",
|
"vue-next-select": "^2.10.2",
|
||||||
"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",
|
||||||
|
@ -23,25 +23,26 @@
|
||||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||||
"@jetblack/operator-overloading": "^0.2.0",
|
"@jetblack/operator-overloading": "^0.2.0",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||||
"@typescript-eslint/parser": "^4.18.0",
|
"@typescript-eslint/parser": "^5.9.1",
|
||||||
|
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.2.2",
|
"@vue/compiler-sfc": "^3.2.26",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^7.0.0",
|
"@vue/eslint-config-typescript": "^10.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^8.6.0",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"eslint-plugin-vue": "^7.0.0-alpha.0",
|
"eslint-plugin-vue": "^8.3.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"sass": "^1.36.0",
|
"sass": "^1.36.0",
|
||||||
"sass-loader": "^10.2.0",
|
"sass-loader": "^10.2.0",
|
||||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "^4.5.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|
33
src/App.vue
33
src/App.vue
|
@ -2,38 +2,31 @@
|
||||||
<div id="modal-root" :style="theme" />
|
<div id="modal-root" :style="theme" />
|
||||||
<div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
|
<div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
|
||||||
<Nav v-if="useHeader" />
|
<Nav v-if="useHeader" />
|
||||||
<Tabs />
|
<Game />
|
||||||
<TPS v-if="showTPS" />
|
<TPS v-if="showTPS" />
|
||||||
<GameOverScreen />
|
<GameOverScreen />
|
||||||
<NaNScreen />
|
<NaNScreen />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { computed, toRef } from "vue";
|
||||||
|
import Game from "./components/system/Game.vue";
|
||||||
|
import GameOverScreen from "./components/system/GameOverScreen.vue";
|
||||||
|
import NaNScreen from "./components/system/NaNScreen.vue";
|
||||||
|
import TPS from "./components/system/TPS.vue";
|
||||||
import modInfo from "./data/modInfo.json";
|
import modInfo from "./data/modInfo.json";
|
||||||
import themes from "./data/themes";
|
import themes from "./data/themes";
|
||||||
import settings from "./game/settings";
|
import settings from "./game/settings";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
import { mapSettings } from "./util/vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
function updateMouse(/* event */) {
|
||||||
name: "App",
|
|
||||||
data() {
|
|
||||||
return { useHeader: modInfo.useHeader };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapSettings(["showTPS"]),
|
|
||||||
theme() {
|
|
||||||
return themes[settings.theme].variables;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateMouse(/* event */) {
|
|
||||||
// TODO use event to update mouse position for particles
|
// TODO use event to update mouse position for particles
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
const useHeader = modInfo.useHeader;
|
||||||
|
const theme = computed(() => themes[settings.theme].variables);
|
||||||
|
const showTPS = toRef(settings, "showTPS");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,250 +0,0 @@
|
||||||
<template>
|
|
||||||
<panZoom
|
|
||||||
:style="style"
|
|
||||||
selector=".g1"
|
|
||||||
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
|
|
||||||
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%">
|
|
||||||
<g class="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
|
|
||||||
: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>
|
|
||||||
</svg>
|
|
||||||
</panZoom>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { Board, BoardNode } from "@/typings/features/board";
|
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Board",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
lastMousePosition: { x: 0, y: 0 },
|
|
||||||
dragged: { x: 0, y: 0 },
|
|
||||||
dragging: null,
|
|
||||||
hasDragged: false
|
|
||||||
} as {
|
|
||||||
lastMousePosition: { x: number; y: number };
|
|
||||||
dragged: { x: number; y: number };
|
|
||||||
dragging: number | null;
|
|
||||||
hasDragged: boolean;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
board(): Board {
|
|
||||||
return layers[this.layer].boards!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
width: this.board.width,
|
|
||||||
height: this.board.height
|
|
||||||
},
|
|
||||||
layers[this.layer].componentStyles?.board,
|
|
||||||
this.board.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
draggingNode() {
|
|
||||||
return this.dragging == null
|
|
||||||
? null
|
|
||||||
: this.board.nodes.find(node => node.id === this.dragging);
|
|
||||||
},
|
|
||||||
nodes() {
|
|
||||||
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: {
|
|
||||||
getZoomLevel(): number {
|
|
||||||
return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
|
|
||||||
},
|
|
||||||
onInit(panzoomInstance: any) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.vue-pan-zoom-scene {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.g1 {
|
|
||||||
transition-duration: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-enter-from,
|
|
||||||
.link-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,55 +0,0 @@
|
||||||
<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>
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
.can,
|
.can,
|
||||||
.can button {
|
.can button {
|
||||||
|
background-color: var(--layer-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,65 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<tooltip v-if="achievement.unlocked" :display="tooltip">
|
<Tooltip
|
||||||
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
|
:display="tooltip"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:style="style"
|
:style="[{ backgroundImage: (earned && image && `url(${image})`) || '' }, style ?? []]"
|
||||||
:class="{
|
:class="{
|
||||||
[layer]: true,
|
|
||||||
feature: true,
|
feature: true,
|
||||||
achievement: true,
|
achievement: true,
|
||||||
locked: !achievement.earned,
|
locked: !earned,
|
||||||
bought: achievement.earned
|
bought: earned,
|
||||||
|
...classes
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<component v-if="display" :is="display" />
|
<component v-if="component" :is="component" />
|
||||||
<branch-node :branches="achievement.branches" :id="id" featureType="achievement" />
|
<MarkNode :mark="mark" />
|
||||||
|
<LinkNode :id="id" />
|
||||||
</div>
|
</div>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { layers } from "@/game/layers";
|
import { GenericAchievement } from "@/features/achievement";
|
||||||
import { CoercableComponent } from "@/typings/component";
|
import { FeatureComponent } from "@/features/feature";
|
||||||
import { Achievement } from "@/typings/features/achievement";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { computed, toRefs } from "vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
import MarkNode from "./MarkNode.vue";
|
||||||
|
import { Visibility } from "@/features/feature";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericAchievement>>());
|
||||||
name: "achievement",
|
|
||||||
mixins: [InjectLayerMixin],
|
const component = computed(() => {
|
||||||
props: {
|
const display = props.display.value;
|
||||||
id: {
|
return display && coerceComponent(display);
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
achievement(): Achievement {
|
|
||||||
return layers[this.layer].achievements!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
layers[this.layer].componentStyles?.achievement,
|
|
||||||
this.achievement.style,
|
|
||||||
this.achievement.image && this.achievement.earned
|
|
||||||
? {
|
|
||||||
backgroundImage: `url(${this.achievement.image}`
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
];
|
|
||||||
},
|
|
||||||
display(): Component | string {
|
|
||||||
if (this.achievement.display) {
|
|
||||||
return coerceComponent(this.achievement.display, "h3");
|
|
||||||
}
|
|
||||||
return coerceComponent(this.achievement.name!, "h3");
|
|
||||||
},
|
|
||||||
tooltip(): CoercableComponent {
|
|
||||||
if (this.achievement.earned) {
|
|
||||||
return this.achievement.doneTooltip || this.achievement.tooltip || "You did it!";
|
|
||||||
}
|
|
||||||
return this.achievement.goalTooltip || this.achievement.tooltip || "LOCKED";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered" class="table">
|
|
||||||
<template v-if="rows && cols">
|
|
||||||
<div v-for="row in rows" class="row" :key="row">
|
|
||||||
<div v-for="col in cols" :key="col">
|
|
||||||
<achievement
|
|
||||||
v-if="filtered[row * 10 + col] !== undefined"
|
|
||||||
class="align"
|
|
||||||
:id="row * 10 + col"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<row v-else>
|
|
||||||
<achievement v-for="(achievement, id) in filtered" :key="id" :id="id" />
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Achievement } from "@/typings/features/achievement";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "achievements",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Achievement>("achievements")]
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,97 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="bar.unlocked" :style="style" :class="{ [layer]: true, bar: true }">
|
<div
|
||||||
<div class="overlayTextContainer border" :style="borderStyle">
|
v-if="visibility !== Visibility.None"
|
||||||
<component class="overlayText" :style="textStyle" :is="display" />
|
v-show="visibility === Visibility.Visible"
|
||||||
|
:style="[{ width: width + 'px', height: height + 'px' }, style ?? {}]"
|
||||||
|
:class="{
|
||||||
|
bar: true,
|
||||||
|
...classes
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="overlayTextContainer border"
|
||||||
|
:style="[{ width: width + 'px', height: height + 'px' }, borderStyle ?? {}]"
|
||||||
|
>
|
||||||
|
<component v-if="component" class="overlayText" :style="textStyle" :is="component" />
|
||||||
</div>
|
</div>
|
||||||
<div class="border" :style="backgroundStyle">
|
<div
|
||||||
<div class="fill" :style="fillStyle" />
|
class="border"
|
||||||
|
:style="[
|
||||||
|
{ width: width + 'px', height: height + 'px' },
|
||||||
|
style ?? {},
|
||||||
|
baseStyle ?? {},
|
||||||
|
borderStyle ?? {}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="fill" :style="[barStyle, style ?? {}, fillStyle ?? {}]" />
|
||||||
</div>
|
</div>
|
||||||
<branch-node :branches="bar.branches" :id="id" featureType="bar" />
|
<MarkNode :mark="mark" />
|
||||||
|
<LinkNode :id="id" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { Direction } from "@/game/enums";
|
import { Direction, GenericBar } from "@/features/bar";
|
||||||
import { layers } from "@/game/layers";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { Bar } from "@/typings/features/bar";
|
|
||||||
import Decimal from "@/util/bignum";
|
import Decimal from "@/util/bignum";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, CSSProperties, toRefs, unref } from "vue";
|
||||||
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
import MarkNode from "./MarkNode.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericBar>>());
|
||||||
name: "bar",
|
|
||||||
mixins: [InjectLayerMixin],
|
const normalizedProgress = computed(() => {
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
bar(): Bar {
|
|
||||||
return layers[this.layer].bars!.data[this.id];
|
|
||||||
},
|
|
||||||
progress(): number {
|
|
||||||
let progress =
|
let progress =
|
||||||
this.bar.progress instanceof Decimal
|
props.progress.value instanceof Decimal
|
||||||
? this.bar.progress.toNumber()
|
? props.progress.value.toNumber()
|
||||||
: (this.bar.progress as number);
|
: Number(props.progress.value);
|
||||||
return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
|
return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
|
||||||
},
|
});
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
const barStyle = computed(() => {
|
||||||
{ width: this.bar.width + "px", height: this.bar.height + "px" },
|
const barStyle: Partial<CSSProperties> = {
|
||||||
layers[this.layer].componentStyles?.bar,
|
width: unref(props.width) + 0.5 + "px",
|
||||||
this.bar.style
|
height: unref(props.height) + 0.5 + "px"
|
||||||
];
|
|
||||||
},
|
|
||||||
borderStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
{ width: this.bar.width + "px", height: this.bar.height + "px" },
|
|
||||||
this.bar.borderStyle
|
|
||||||
];
|
|
||||||
},
|
|
||||||
textStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [this.bar.style, this.bar.textStyle];
|
|
||||||
},
|
|
||||||
backgroundStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
{ width: this.bar.width + "px", height: this.bar.height + "px" },
|
|
||||||
this.bar.style,
|
|
||||||
this.bar.baseStyle,
|
|
||||||
this.bar.borderStyle
|
|
||||||
];
|
|
||||||
},
|
|
||||||
fillStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
const fillStyle: Partial<CSSStyleDeclaration> = {
|
|
||||||
width: this.bar.width + 0.5 + "px",
|
|
||||||
height: this.bar.height + 0.5 + "px"
|
|
||||||
};
|
};
|
||||||
switch (this.bar.direction) {
|
switch (unref(props.direction)) {
|
||||||
case Direction.Up:
|
case Direction.Up:
|
||||||
fillStyle.clipPath = `inset(${this.progress}% 0% 0% 0%)`;
|
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
|
||||||
fillStyle.width = this.bar.width + 1 + "px";
|
barStyle.width = unref(props.width) + 1 + "px";
|
||||||
break;
|
break;
|
||||||
case Direction.Down:
|
case Direction.Down:
|
||||||
fillStyle.clipPath = `inset(0% 0% ${this.progress}% 0%)`;
|
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
|
||||||
fillStyle.width = this.bar.width + 1 + "px";
|
barStyle.width = unref(props.width) + 1 + "px";
|
||||||
break;
|
break;
|
||||||
case Direction.Right:
|
case Direction.Right:
|
||||||
fillStyle.clipPath = `inset(0% ${this.progress}% 0% 0%)`;
|
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||||
break;
|
break;
|
||||||
case Direction.Left:
|
case Direction.Left:
|
||||||
fillStyle.clipPath = `inset(0% 0% 0% ${this.progress} + '%)`;
|
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
|
||||||
break;
|
break;
|
||||||
case Direction.Default:
|
case Direction.Default:
|
||||||
fillStyle.clipPath = "inset(0% 50% 0% 0%)";
|
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return [fillStyle, this.bar.style, this.bar.fillStyle];
|
return barStyle;
|
||||||
},
|
});
|
||||||
display(): Component | string {
|
|
||||||
return coerceComponent(this.bar.display);
|
const component = computed(() => {
|
||||||
}
|
const display = props.display.value;
|
||||||
}
|
return display && coerceComponent(display);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="buyable.unlocked" style="display: grid">
|
|
||||||
<button
|
|
||||||
:style="style"
|
|
||||||
@click="buyable.buy"
|
|
||||||
@mousedown="start"
|
|
||||||
@mouseleave="stop"
|
|
||||||
@mouseup="stop"
|
|
||||||
@touchstart="start"
|
|
||||||
:class="{
|
|
||||||
feature: true,
|
|
||||||
[layer]: true,
|
|
||||||
buyable: true,
|
|
||||||
can: buyable.canBuy,
|
|
||||||
locked: !buyable.canAfford,
|
|
||||||
bought
|
|
||||||
}"
|
|
||||||
@touchend="stop"
|
|
||||||
@touchcancel="stop"
|
|
||||||
:disabled="!buyable.canBuy"
|
|
||||||
>
|
|
||||||
<div v-if="title">
|
|
||||||
<component :is="title" />
|
|
||||||
</div>
|
|
||||||
<component :is="display" style="white-space: pre-line;" />
|
|
||||||
<mark-node :mark="buyable.mark" />
|
|
||||||
<branch-node :branches="buyable.branches" :id="id" featureType="buyable" />
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
(buyable.sellOne !== undefined && buyable.canSellOne !== false) ||
|
|
||||||
(buyable.sellAll !== undefined && buyable.canSellAll !== false)
|
|
||||||
"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
@click="buyable.sellAll"
|
|
||||||
v-if="buyable.sellAll !== undefined && buyable.canSellAll !== false"
|
|
||||||
:class="{
|
|
||||||
'buyable-button': true,
|
|
||||||
can: buyable.unlocked,
|
|
||||||
locked: !buyable.unlocked,
|
|
||||||
feature: true
|
|
||||||
}"
|
|
||||||
:style="{ 'background-color': buyable.canSellAll ? layerColor : '' }"
|
|
||||||
>
|
|
||||||
Sell All
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="buyable.sellOne"
|
|
||||||
v-if="buyable.sellOne !== undefined && buyable.canSellOne !== false"
|
|
||||||
:class="{
|
|
||||||
'buyable-button': true,
|
|
||||||
can: buyable.unlocked,
|
|
||||||
locked: !buyable.unlocked,
|
|
||||||
feature: true
|
|
||||||
}"
|
|
||||||
:style="{ 'background-color': buyable.canSellOne ? layerColor : '' }"
|
|
||||||
>
|
|
||||||
Sell One
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { Buyable } from "@/typings/features/buyable";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "buyable",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
size: Number
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
interval: null,
|
|
||||||
time: 0
|
|
||||||
} as {
|
|
||||||
interval: number | null;
|
|
||||||
time: number;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
buyable(): Buyable {
|
|
||||||
return layers[this.layer].buyables!.data[this.id];
|
|
||||||
},
|
|
||||||
bought(): boolean {
|
|
||||||
return player.layers[this.layer].buyables[this.id].gte(this.buyable.purchaseLimit);
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.buyable.canBuy ? { backgroundColor: layers[this.layer].color } : undefined,
|
|
||||||
this.size ? { height: this.size + "px", width: this.size + "px" } : undefined,
|
|
||||||
layers[this.layer].componentStyles?.buyable,
|
|
||||||
this.buyable.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
title(): Component | string | null {
|
|
||||||
if (this.buyable.title) {
|
|
||||||
return coerceComponent(this.buyable.title, "h2");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
display(): Component | string {
|
|
||||||
return coerceComponent(this.buyable.display, "div");
|
|
||||||
},
|
|
||||||
layerColor(): string {
|
|
||||||
return layers[this.layer].color;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
start() {
|
|
||||||
if (!this.interval) {
|
|
||||||
this.interval = setInterval(this.buyable.buy, 250);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = null;
|
|
||||||
this.time = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.buyable {
|
|
||||||
min-height: 200px;
|
|
||||||
width: 200px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buyable-button {
|
|
||||||
width: calc(100% - 10px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,81 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered" class="table">
|
|
||||||
<respec-button
|
|
||||||
v-if="showRespec"
|
|
||||||
style="margin-bottom: 12px;"
|
|
||||||
:confirmRespec="confirmRespec"
|
|
||||||
:respecWarningDisplay="respecWarningDisplay"
|
|
||||||
@set-confirm-respec="setConfirmRespec"
|
|
||||||
@respec="respec"
|
|
||||||
/>
|
|
||||||
<template v-if="rows && cols">
|
|
||||||
<div v-for="row in rows" class="row" :key="row">
|
|
||||||
<div v-for="col in cols" :key="col">
|
|
||||||
<buyable
|
|
||||||
v-if="filtered[row * 10 + col] !== undefined"
|
|
||||||
class="align buyable-container"
|
|
||||||
:style="{ height }"
|
|
||||||
:id="row * 10 + col"
|
|
||||||
:size="height === 'inherit' ? null : height"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<row v-else>
|
|
||||||
<buyable
|
|
||||||
v-for="(buyable, id) in filtered"
|
|
||||||
:key="id"
|
|
||||||
class="align buyable-container"
|
|
||||||
:style="{ height }"
|
|
||||||
:id="id"
|
|
||||||
:size="height === 'inherit' ? null : height"
|
|
||||||
/>
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { CoercableComponent } from "@/typings/component";
|
|
||||||
import { Buyable } from "@/typings/features/buyable";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "buyables",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Buyable>("buyables")],
|
|
||||||
props: {
|
|
||||||
height: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: "inherit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
showRespec(): boolean | undefined {
|
|
||||||
return layers[this.layer].buyables!.showRespecButton;
|
|
||||||
},
|
|
||||||
confirmRespec(): boolean {
|
|
||||||
return player.layers[this.layer].confirmRespecBuyables;
|
|
||||||
},
|
|
||||||
respecWarningDisplay(): CoercableComponent | undefined {
|
|
||||||
return layers[this.layer].buyables?.respecWarningDisplay;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setConfirmRespec(value: boolean) {
|
|
||||||
player.layers[this.layer].confirmRespecBuyables = value;
|
|
||||||
},
|
|
||||||
respec() {
|
|
||||||
layers[this.layer].buyables!.respec?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.buyable-container {
|
|
||||||
margin-left: 7px;
|
|
||||||
margin-right: 7px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,87 +1,78 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="challenge.shown"
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
:style="style"
|
:style="style"
|
||||||
:class="{
|
:class="{
|
||||||
feature: true,
|
feature: true,
|
||||||
[layer]: true,
|
|
||||||
challenge: true,
|
challenge: true,
|
||||||
resetNotify: challenge.active,
|
resetNotify: active,
|
||||||
notify: challenge.active && challenge.canComplete,
|
notify: active && canComplete,
|
||||||
done: challenge.completed,
|
done: completed,
|
||||||
canStart: challenge.canStart,
|
canStart,
|
||||||
maxed: challenge.maxed
|
maxed,
|
||||||
|
...classes
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="title"><component :is="title" /></div>
|
<button class="toggleChallenge" @click="toggle">
|
||||||
<button
|
|
||||||
:style="{ backgroundColor: challenge.canStart ? buttonColor : null }"
|
|
||||||
@click="toggle"
|
|
||||||
>
|
|
||||||
{{ buttonText }}
|
{{ buttonText }}
|
||||||
</button>
|
</button>
|
||||||
<component v-if="fullDisplay" :is="fullDisplay" />
|
<component v-if="component" :is="component" />
|
||||||
<default-challenge-display v-else :id="id" />
|
<default-challenge-display v-else :id="id" />
|
||||||
<mark-node :mark="challenge.mark" />
|
<MarkNode :mark="mark" />
|
||||||
<branch-node :branches="challenge.branches" :id="id" featureType="challenge" />
|
<LinkNode :id="id" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="tsx">
|
||||||
import { layers } from "@/game/layers";
|
import { GenericChallenge } from "@/features/challenge";
|
||||||
import { Challenge } from "@/typings/features/challenge";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericChallenge>>());
|
||||||
name: "challenge",
|
|
||||||
mixins: [InjectLayerMixin],
|
const buttonText = computed(() => {
|
||||||
props: {
|
if (props.active.value) {
|
||||||
id: {
|
return props.canComplete.value ? "Finish" : "Exit Early";
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
}
|
||||||
},
|
if (props.maxed.value) {
|
||||||
computed: {
|
|
||||||
challenge(): Challenge {
|
|
||||||
return layers[this.layer].challenges!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [layers[this.layer].componentStyles?.challenge, this.challenge.style];
|
|
||||||
},
|
|
||||||
title(): Component | string | null {
|
|
||||||
if (this.challenge.titleDisplay) {
|
|
||||||
return coerceComponent(this.challenge.titleDisplay, "div");
|
|
||||||
}
|
|
||||||
if (this.challenge.name) {
|
|
||||||
return coerceComponent(this.challenge.name, "h3");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
buttonColor(): string {
|
|
||||||
return layers[this.layer].color;
|
|
||||||
},
|
|
||||||
buttonText(): string {
|
|
||||||
if (this.challenge.active) {
|
|
||||||
return this.challenge.canComplete ? "Finish" : "Exit Early";
|
|
||||||
}
|
|
||||||
if (this.challenge.maxed) {
|
|
||||||
return "Completed";
|
return "Completed";
|
||||||
}
|
}
|
||||||
return "Start";
|
return "Start";
|
||||||
},
|
});
|
||||||
fullDisplay(): Component | string | null {
|
|
||||||
if (this.challenge.fullDisplay) {
|
const component = computed(() => {
|
||||||
return coerceComponent(this.challenge.fullDisplay, "div");
|
const display = props.display.value;
|
||||||
}
|
if (display == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
if (isCoercableComponent(display)) {
|
||||||
methods: {
|
return coerceComponent(display);
|
||||||
toggle() {
|
|
||||||
this.challenge.toggle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<template v-if={display.title}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.title!, "h3")} />
|
||||||
|
</template>
|
||||||
|
<component v-is={coerceComponent(display.description, "div")} />
|
||||||
|
<div v-if={display.goal}>
|
||||||
|
<br />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Goal: <component v-is={coerceComponent(display.goal!)} />
|
||||||
|
</div>
|
||||||
|
<div v-if={display.reward}>
|
||||||
|
<br />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Reward: <component v-is={coerceComponent(display.reward!)} />
|
||||||
|
</div>
|
||||||
|
<div v-if={display.effectDisplay}>
|
||||||
|
Currently: {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.effectDisplay!)} />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -111,5 +102,6 @@ export default defineComponent({
|
||||||
|
|
||||||
.challenge.canStart button {
|
.challenge.canStart button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background-color: var(--layer-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered" class="table">
|
|
||||||
<template v-if="rows && cols">
|
|
||||||
<div v-for="row in rows" class="row" :key="row">
|
|
||||||
<div v-for="col in cols" :key="col">
|
|
||||||
<challenge v-if="filtered[row * 10 + col] !== undefined" :id="row * 10 + col" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<row v-else>
|
|
||||||
<challenge v-for="(challenge, id) in filtered" :key="id" :id="id" />
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Challenge } from "@/typings/features/challenge";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "challenges",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Challenge>("challenges")]
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,95 +1,61 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="clickable.unlocked">
|
<div v-if="visibility !== Visibility.None" v-show="visibility === Visibility.Visible">
|
||||||
<button
|
<button
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="clickable.click"
|
@click="onClick"
|
||||||
@mousedown="start"
|
@mousedown="start"
|
||||||
@mouseleave="stop"
|
@mouseleave="stop"
|
||||||
@mouseup="stop"
|
@mouseup="stop"
|
||||||
@touchstart="start"
|
@touchstart="start"
|
||||||
@touchend="stop"
|
@touchend="stop"
|
||||||
@touchcancel="stop"
|
@touchcancel="stop"
|
||||||
:disabled="!clickable.canClick"
|
:disabled="!canClick"
|
||||||
:class="{
|
:class="{
|
||||||
feature: true,
|
feature: true,
|
||||||
[layer]: true,
|
|
||||||
clickable: true,
|
clickable: true,
|
||||||
can: clickable.canClick,
|
can: props.canClick,
|
||||||
locked: !clickable.canClick
|
locked: !canClick,
|
||||||
|
small,
|
||||||
|
...classes
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="title">
|
<component v-if="component" :is="component" />
|
||||||
<component :is="title" />
|
<MarkNode :mark="mark" />
|
||||||
</div>
|
<LinkNode :id="id" />
|
||||||
<component :is="display" style="white-space: pre-line;" />
|
|
||||||
<mark-node :mark="clickable.mark" />
|
|
||||||
<branch-node :branches="clickable.branches" :id="id" featureType="clickable" />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="tsx">
|
||||||
import { layers } from "@/game/layers";
|
import { GenericClickable } from "@/features/clickable";
|
||||||
import { Clickable } from "@/typings/features/clickable";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent, isCoercableComponent, setupHoldToClick } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
import MarkNode from "./MarkNode.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericClickable>>());
|
||||||
name: "clickable",
|
|
||||||
mixins: [InjectLayerMixin],
|
const component = computed(() => {
|
||||||
props: {
|
const display = unref(props.display);
|
||||||
id: {
|
if (display == null) {
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
size: String
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
interval: null,
|
|
||||||
time: 0
|
|
||||||
} as {
|
|
||||||
interval: number | null;
|
|
||||||
time: number;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
clickable(): Clickable {
|
|
||||||
return layers[this.layer].clickables!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.clickable.canClick ? { backgroundColor: layers[this.layer].color } : undefined,
|
|
||||||
this.size ? { height: this.size, width: this.size } : undefined,
|
|
||||||
layers[this.layer].componentStyles?.clickable,
|
|
||||||
this.clickable.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
title(): Component | string | null {
|
|
||||||
if (this.clickable.title) {
|
|
||||||
return coerceComponent(this.clickable.title, "h2");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
|
||||||
display(): Component | string {
|
|
||||||
return coerceComponent(this.clickable.display, "div");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
start() {
|
|
||||||
if (!this.interval && this.clickable.click) {
|
|
||||||
this.interval = setInterval(this.clickable.click, 250);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = null;
|
|
||||||
this.time = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (isCoercableComponent(display)) {
|
||||||
|
return coerceComponent(display);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<div v-if={display.title}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.title!, "h2")} />
|
||||||
|
</div>
|
||||||
|
<component v-is={coerceComponent(display.description, "div")} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered != undefined" class="table">
|
|
||||||
<master-button v-if="showMaster" style="margin-bottom: 12px;" @press="press" />
|
|
||||||
<template v-if="rows && cols">
|
|
||||||
<div v-for="row in rows" class="row" :key="row">
|
|
||||||
<div v-for="col in cols" :key="col">
|
|
||||||
<clickable
|
|
||||||
v-if="filtered[row * 10 + col] !== undefined"
|
|
||||||
class="align clickable-container"
|
|
||||||
:style="{ height }"
|
|
||||||
:id="row * 10 + col"
|
|
||||||
:size="height === 'inherit' ? null : height"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<row v-else>
|
|
||||||
<clickable
|
|
||||||
v-for="(clickable, id) in filtered"
|
|
||||||
:key="id"
|
|
||||||
class="align clickable-container"
|
|
||||||
:style="{ height }"
|
|
||||||
:id="id"
|
|
||||||
:size="height === 'inherit' ? null : height"
|
|
||||||
/>
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { Clickable } from "@/typings/features/clickable";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "clickables",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Clickable>("clickables")],
|
|
||||||
props: {
|
|
||||||
showMasterButton: {
|
|
||||||
type: Boolean,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: "inherit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
showMaster(): boolean | undefined {
|
|
||||||
if (layers[this.layer].clickables?.masterButtonClick == undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.showMasterButton != undefined) {
|
|
||||||
return this.showMasterButton;
|
|
||||||
}
|
|
||||||
return layers[this.layer].clickables?.showMasterButton;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
press() {
|
|
||||||
layers[this.layer].clickables?.masterButtonClick?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.clickable-container {
|
|
||||||
margin-left: 7px;
|
|
||||||
margin-right: 7px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<component :is="challengeDescription" v-bind="$attrs" />
|
|
||||||
<div>Goal: <component :is="goalDescription" /></div>
|
|
||||||
<div>Reward: <component :is="rewardDescription" /></div>
|
|
||||||
<component v-if="rewardDisplay" :is="rewardDisplay" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { Challenge } from "@/typings/features/challenge";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "default-challenge-display",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
challenge(): Challenge {
|
|
||||||
return layers[this.layer].challenges!.data[this.id];
|
|
||||||
},
|
|
||||||
challengeDescription(): Component | string {
|
|
||||||
return coerceComponent(this.challenge.challengeDescription, "div");
|
|
||||||
},
|
|
||||||
goalDescription(): Component | string {
|
|
||||||
if (this.challenge.goalDescription) {
|
|
||||||
return coerceComponent(this.challenge.goalDescription);
|
|
||||||
}
|
|
||||||
return coerceComponent(
|
|
||||||
`{{ format(${this.challenge.goal}) }} ${this.challenge.currencyDisplayName ||
|
|
||||||
"points"}`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
rewardDescription(): Component | string {
|
|
||||||
return coerceComponent(this.challenge.rewardDescription);
|
|
||||||
},
|
|
||||||
rewardDisplay(): Component | string | null {
|
|
||||||
if (this.challenge.rewardDisplay) {
|
|
||||||
return coerceComponent(`Currently: ${this.challenge.rewardDisplay}`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,79 +0,0 @@
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
{{ resetDescription }}<b>{{ resetGain }}</b>
|
|
||||||
{{ resource }}
|
|
||||||
<br v-if="nextAt" /><br v-if="nextAt" />
|
|
||||||
{{ nextAt }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import Decimal, { format, formatWhole } from "@/util/bignum";
|
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "default-prestige-button-display",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
computed: {
|
|
||||||
resetDescription(): string {
|
|
||||||
if (player.layers[this.layer].points.lt(1e3) || layers[this.layer].type === "static") {
|
|
||||||
return layers[this.layer].resetDescription || "Reset for ";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
resetGain(): string {
|
|
||||||
return formatWhole(layers[this.layer].resetGain);
|
|
||||||
},
|
|
||||||
resource(): string {
|
|
||||||
return layers[this.layer].resource;
|
|
||||||
},
|
|
||||||
showNextAt(): boolean {
|
|
||||||
if (layers[this.layer].showNextAt != undefined) {
|
|
||||||
return layers[this.layer].showNextAt!;
|
|
||||||
} else {
|
|
||||||
return layers[this.layer].type === "static"
|
|
||||||
? player.layers[this.layer].points.lt(30) // static
|
|
||||||
: player.layers[this.layer].points.lt(1e3) &&
|
|
||||||
layers[this.layer].resetGain.lt(100); // normal
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nextAt(): string {
|
|
||||||
if (this.showNextAt) {
|
|
||||||
let prefix;
|
|
||||||
if (layers[this.layer].type === "static") {
|
|
||||||
if (
|
|
||||||
Decimal.gte(layers[this.layer].baseAmount!, layers[this.layer].nextAt) &&
|
|
||||||
layers[this.layer].canBuyMax !== false
|
|
||||||
) {
|
|
||||||
prefix = "Next:";
|
|
||||||
} else {
|
|
||||||
prefix = "Req:";
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseAmount = formatWhole(layers[this.layer].baseAmount!);
|
|
||||||
const nextAt = (layers[this.layer].roundUpCost ? formatWhole : format)(
|
|
||||||
layers[this.layer].nextAtMax
|
|
||||||
);
|
|
||||||
const baseResource = layers[this.layer].baseResource;
|
|
||||||
|
|
||||||
return `${prefix} ${baseAmount} / ${nextAt} ${baseResource}`;
|
|
||||||
} else {
|
|
||||||
let amount;
|
|
||||||
if (layers[this.layer].roundUpCost) {
|
|
||||||
amount = formatWhole(layers[this.layer].nextAt);
|
|
||||||
} else {
|
|
||||||
amount = format(layers[this.layer].nextAt);
|
|
||||||
}
|
|
||||||
return `Next at ${amount} ${layers[this.layer].baseResource}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,56 +0,0 @@
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<div v-if="title"><component :is="title" /></div>
|
|
||||||
<component :is="description" />
|
|
||||||
<div v-if="effectDisplay"><br />Currently: <component :is="effectDisplay" /></div>
|
|
||||||
<br />
|
|
||||||
Cost: {{ cost }} {{ costResource }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { Upgrade } from "@/typings/features/upgrade";
|
|
||||||
import { formatWhole } from "@/util/bignum";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "default-upgrade-display",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
upgrade(): Upgrade {
|
|
||||||
return layers[this.layer].upgrades!.data[this.id];
|
|
||||||
},
|
|
||||||
title(): Component | string | null {
|
|
||||||
if (this.upgrade.title) {
|
|
||||||
return coerceComponent(this.upgrade.title, "h3");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
description(): Component | string {
|
|
||||||
return coerceComponent(this.upgrade.description, "div");
|
|
||||||
},
|
|
||||||
effectDisplay(): Component | string | null {
|
|
||||||
if (this.upgrade.effectDisplay) {
|
|
||||||
return coerceComponent(this.upgrade.effectDisplay);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
cost(): string {
|
|
||||||
return formatWhole(this.upgrade.cost);
|
|
||||||
},
|
|
||||||
costResource(): string {
|
|
||||||
return this.upgrade.currencyDisplayName || layers[this.layer].resource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,34 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="grid" class="table">
|
<div
|
||||||
<div v-for="row in grid.rows" class="row" :key="row">
|
v-if="visibility !== Visibility.None"
|
||||||
<div v-for="col in grid.cols" :key="col">
|
v-show="visibility === Visibility.Visible"
|
||||||
<grid-cell class="align" :id="id" :cell="row * 100 + col" />
|
class="table"
|
||||||
|
>
|
||||||
|
<div v-for="row in rows" class="row" :key="row">
|
||||||
|
<div v-for="col in cols" :key="col">
|
||||||
|
<GridCell v-bind="cells[row * 100 + col]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { layers } from "@/game/layers";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { Grid } from "@/typings/features/grid";
|
import { GenericGrid } from "@/features/grid";
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
import GridCell from "./GridCell.vue";
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps<FeatureComponent<GenericGrid>>();
|
||||||
name: "grid",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
grid(): Grid {
|
|
||||||
return layers[this.layer].grids!.data[this.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="gridCell.unlocked"
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
:class="{ feature: true, tile: true, can: canClick, locked: !canClick }"
|
:class="{ feature: true, tile: true, can: canClick, locked: !canClick }"
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="gridCell.click"
|
@click="onClick"
|
||||||
@mousedown="start"
|
@mousedown="start"
|
||||||
@mouseleave="stop"
|
@mouseleave="stop"
|
||||||
@mouseup="stop"
|
@mouseup="stop"
|
||||||
|
@ -12,80 +13,28 @@
|
||||||
@touchcancel="stop"
|
@touchcancel="stop"
|
||||||
:disabled="!canClick"
|
:disabled="!canClick"
|
||||||
>
|
>
|
||||||
<div v-if="title"><component :is="title" /></div>
|
<div v-if="title"><component :is="titleComponent" /></div>
|
||||||
<component :is="display" style="white-space: pre-line;" />
|
<component :is="component" style="white-space: pre-line;" />
|
||||||
<branch-node :branches="gridCell.branches" :id="id" featureType="gridCell" />
|
<LinkNode :id="id" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { layers } from "@/game/layers";
|
import { Visibility } from "@/features/feature";
|
||||||
import { GridCell } from "@/typings/features/grid";
|
import { GridCell } from "@/features/grid";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent, setupHoldToClick } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<GridCell>());
|
||||||
name: "grid-cell",
|
|
||||||
mixins: [InjectLayerMixin],
|
const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
|
||||||
props: {
|
|
||||||
id: {
|
const titleComponent = computed(() => {
|
||||||
type: [Number, String],
|
const title = unref(props.title);
|
||||||
required: true
|
return title && coerceComponent(title);
|
||||||
},
|
|
||||||
cell: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
size: [Number, String]
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
interval: null,
|
|
||||||
time: 0
|
|
||||||
} as {
|
|
||||||
interval: number | null;
|
|
||||||
time: number;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
gridCell(): GridCell {
|
|
||||||
return layers[this.layer].grids!.data[this.id][this.cell] as GridCell;
|
|
||||||
},
|
|
||||||
canClick(): boolean {
|
|
||||||
return this.gridCell.canClick;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.canClick ? { backgroundColor: layers[this.layer].color } : {},
|
|
||||||
layers[this.layer].componentStyles?.["grid-cell"],
|
|
||||||
this.gridCell.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
title(): Component | string | null {
|
|
||||||
if (this.gridCell.title) {
|
|
||||||
return coerceComponent(this.gridCell.title, "h3");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
display(): Component | string {
|
|
||||||
return coerceComponent(this.gridCell.display, "div");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
start() {
|
|
||||||
if (!this.interval && this.gridCell.click) {
|
|
||||||
this.interval = setInterval(this.gridCell.click, 250);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stop() {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = null;
|
|
||||||
this.time = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const component = computed(() => coerceComponent(unref(props.display)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -93,5 +42,6 @@ export default defineComponent({
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
background-color: var(--layer-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,84 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="infobox" v-if="infobox.unlocked" :style="style" :class="{ collapsed, stacked }">
|
<div
|
||||||
<button class="title" :style="titleStyle" @click="toggle">
|
class="infobox"
|
||||||
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
|
:style="[{ borderColor: color }, style || []]"
|
||||||
|
:class="{ collapsed, stacked, ...classes }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="title"
|
||||||
|
:style="[{ backgroundColor: color }, titleStyle || []]"
|
||||||
|
@click="collapsed = !collapsed"
|
||||||
|
>
|
||||||
<span class="toggle">▼</span>
|
<span class="toggle">▼</span>
|
||||||
<component :is="title" />
|
<component :is="titleComponent" />
|
||||||
</button>
|
</button>
|
||||||
<collapse-transition>
|
<CollapseTransition>
|
||||||
<div v-if="!collapsed" class="body" :style="{ backgroundColor: borderColor }">
|
<div v-if="!collapsed" class="body" :style="{ backgroundColor: color }">
|
||||||
<component :is="body" :style="bodyStyle" />
|
<component :is="bodyComponent" :style="bodyStyle" />
|
||||||
</div>
|
</div>
|
||||||
</collapse-transition>
|
</CollapseTransition>
|
||||||
<branch-node :branches="infobox.branches" :id="id" featureType="infobox" />
|
<LinkNode :id="id" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import themes from "@/data/themes";
|
import themes from "@/data/themes";
|
||||||
import { layers } from "@/game/layers";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import player from "@/game/player";
|
import { GenericInfobox } from "@/features/infobox";
|
||||||
import settings from "@/game/settings";
|
import settings from "@/game/settings";
|
||||||
import { Infobox } from "@/typings/features/infobox";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericInfobox>>());
|
||||||
name: "infobox",
|
|
||||||
mixins: [InjectLayerMixin],
|
const titleComponent = computed(() => coerceComponent(unref(props.title)));
|
||||||
props: {
|
const bodyComponent = computed(() => coerceComponent(unref(props.display)));
|
||||||
id: {
|
const stacked = computed(() => themes[settings.theme].stackedInfoboxes);
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
infobox(): Infobox {
|
|
||||||
return layers[this.layer].infoboxes!.data[this.id];
|
|
||||||
},
|
|
||||||
borderColor(): string {
|
|
||||||
return this.infobox.borderColor || layers[this.layer].color;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
{ borderColor: this.borderColor },
|
|
||||||
layers[this.layer].componentStyles?.infobox,
|
|
||||||
this.infobox.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
titleStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
{ backgroundColor: layers[this.layer].color },
|
|
||||||
layers[this.layer].componentStyles?.["infobox-title"],
|
|
||||||
this.infobox.titleStyle
|
|
||||||
];
|
|
||||||
},
|
|
||||||
bodyStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [layers[this.layer].componentStyles?.["infobox-body"], this.infobox.bodyStyle];
|
|
||||||
},
|
|
||||||
title(): Component | string {
|
|
||||||
if (this.infobox.title) {
|
|
||||||
return coerceComponent(this.infobox.title);
|
|
||||||
}
|
|
||||||
return coerceComponent(layers[this.layer].name || this.layer);
|
|
||||||
},
|
|
||||||
body(): Component | string {
|
|
||||||
return coerceComponent(this.infobox.body);
|
|
||||||
},
|
|
||||||
collapsed(): boolean {
|
|
||||||
return player.layers[this.layer].infoboxes[this.id];
|
|
||||||
},
|
|
||||||
stacked(): boolean {
|
|
||||||
return themes[settings.theme].stackedInfoboxes;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggle() {
|
|
||||||
player.layers[this.layer].infoboxes[this.id] = !player.layers[this.layer].infoboxes[
|
|
||||||
this.id
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,52 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<span v-if="showPrefix">You have </span>
|
<span v-if="showPrefix">You have </span>
|
||||||
<resource :amount="amount" :color="color" />
|
<ResourceVue :resource="resource" :color="color || 'white'" />
|
||||||
{{ resource
|
{{ resource
|
||||||
}}<!-- remove whitespace -->
|
}}<!-- remove whitespace -->
|
||||||
<span v-if="effectDisplay">, <component :is="effectDisplay"/></span>
|
<span v-if="effectComponent">, <component :is="effectComponent"/></span>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { layers } from "@/game/layers";
|
import { CoercableComponent } from "@/features/feature";
|
||||||
import player from "@/game/player";
|
import { Resource } from "@/features/resource";
|
||||||
import { format, formatWhole } from "@/util/bignum";
|
import Decimal from "@/util/bignum";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, StyleValue, toRefs } from "vue";
|
||||||
|
import ResourceVue from "../system/Resource.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "main-display",
|
defineProps<{
|
||||||
mixins: [InjectLayerMixin],
|
resource: Resource;
|
||||||
props: {
|
color?: string;
|
||||||
precision: Number
|
classes?: Record<string, boolean>;
|
||||||
},
|
style?: StyleValue;
|
||||||
computed: {
|
effectDisplay?: CoercableComponent;
|
||||||
style(): Partial<CSSStyleDeclaration> | undefined {
|
}>()
|
||||||
return layers[this.layer].componentStyles?.["main-display"];
|
);
|
||||||
},
|
|
||||||
resource(): string {
|
const effectComponent = computed(() => {
|
||||||
return layers[this.layer].resource;
|
const effectDisplay = props.effectDisplay?.value;
|
||||||
},
|
return effectDisplay && coerceComponent(effectDisplay);
|
||||||
effectDisplay(): Component | string | undefined {
|
});
|
||||||
return (
|
|
||||||
layers[this.layer].effectDisplay &&
|
const showPrefix = computed(() => {
|
||||||
coerceComponent(layers[this.layer].effectDisplay!)
|
return Decimal.lt(props.resource.value, "1e1000");
|
||||||
);
|
|
||||||
},
|
|
||||||
showPrefix(): boolean {
|
|
||||||
return player.layers[this.layer].points.lt("1e1000");
|
|
||||||
},
|
|
||||||
color(): string {
|
|
||||||
return layers[this.layer].color;
|
|
||||||
},
|
|
||||||
amount(): string {
|
|
||||||
return this.precision == undefined
|
|
||||||
? formatWhole(player.layers[this.layer].points)
|
|
||||||
: format(player.layers[this.layer].points, this.precision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
defineProps<{ mark: boolean | string | undefined }>();
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "mark-node",
|
|
||||||
props: {
|
|
||||||
mark: [Boolean, String]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
@click="press"
|
|
||||||
:class="{ feature: true, can: unlocked, locked: !unlocked }"
|
|
||||||
:style="style"
|
|
||||||
>
|
|
||||||
<component :is="masterButtonDisplay" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { CoercableComponent } from "@/typings/component";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "master-button",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
display: [String, Object] as PropType<CoercableComponent>
|
|
||||||
},
|
|
||||||
emits: ["press"],
|
|
||||||
computed: {
|
|
||||||
style(): Partial<CSSStyleDeclaration> | undefined {
|
|
||||||
return layers[this.layer].componentStyles?.["master-button"];
|
|
||||||
},
|
|
||||||
unlocked(): boolean {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
},
|
|
||||||
masterButtonDisplay(): Component | string {
|
|
||||||
if (this.display) {
|
|
||||||
return coerceComponent(this.display);
|
|
||||||
}
|
|
||||||
if (layers[this.layer].clickables?.masterButtonDisplay) {
|
|
||||||
return coerceComponent(layers[this.layer].clickables!.masterButtonDisplay!);
|
|
||||||
}
|
|
||||||
return coerceComponent("Click Me!");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
press() {
|
|
||||||
this.$emit("press");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,57 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="milestone.shown"
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
:style="style"
|
:style="style"
|
||||||
:class="{ feature: true, milestone: true, done: milestone.earned }"
|
:class="{ feature: true, milestone: true, done: earned, ...classes }"
|
||||||
>
|
>
|
||||||
<div v-if="requirementDisplay"><component :is="requirementDisplay" /></div>
|
<component v-if="component" :is="component" />
|
||||||
<div v-if="effectDisplay"><component :is="effectDisplay" /></div>
|
<LinkNode :id="id" />
|
||||||
<component v-if="optionsDisplay" :is="optionsDisplay" />
|
|
||||||
<branch-node :branches="milestone.branches" :id="id" featureType="milestone" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="tsx">
|
||||||
import { layers } from "@/game/layers";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { Milestone } from "@/typings/features/milestone";
|
import { GenericMilestone } from "@/features/milestone";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||||
import { Component, defineComponent } from "vue";
|
import { computed, toRefs } from "vue";
|
||||||
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericMilestone>>());
|
||||||
name: "milestone",
|
|
||||||
mixins: [InjectLayerMixin],
|
const component = computed(() => {
|
||||||
props: {
|
const display = props.display.value;
|
||||||
id: {
|
if (display == null) {
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
milestone(): Milestone {
|
|
||||||
return layers[this.layer].milestones!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [layers[this.layer].componentStyles?.milestone, this.milestone.style];
|
|
||||||
},
|
|
||||||
requirementDisplay(): Component | string | null {
|
|
||||||
if (this.milestone.requirementDisplay) {
|
|
||||||
return coerceComponent(this.milestone.requirementDisplay, "h3");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
effectDisplay(): Component | string | null {
|
|
||||||
if (this.milestone.effectDisplay) {
|
|
||||||
return coerceComponent(this.milestone.effectDisplay, "b");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
optionsDisplay(): Component | string | null {
|
|
||||||
if (this.milestone.optionsDisplay && this.milestone.earned) {
|
|
||||||
return coerceComponent(this.milestone.optionsDisplay, "div");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (isCoercableComponent(display)) {
|
||||||
|
return coerceComponent(display);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<component v-is={coerceComponent(display.requirement, "h3")} />
|
||||||
|
<div v-if={display.effectDisplay}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.effectDisplay!, "b")} />
|
||||||
|
</div>
|
||||||
|
<div v-if={display.optionsDisplay}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.optionsDisplay!, "span")} />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered" class="table">
|
|
||||||
<milestone v-for="(milestone, id) in filtered" :key="id" :id="id" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Milestone } from "@/typings/features/milestone";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "milestones",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Milestone>("milestones")]
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
:style="style"
|
|
||||||
@click="resetLayer"
|
|
||||||
:class="{ [layer]: true, reset: true, locked: !canReset, can: canReset }"
|
|
||||||
>
|
|
||||||
<component v-if="prestigeButtonDisplay" :is="prestigeButtonDisplay" />
|
|
||||||
<default-prestige-button-display v-else />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { resetLayer } from "@/util/layers";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "prestige-button",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
computed: {
|
|
||||||
canReset(): boolean {
|
|
||||||
return layers[this.layer].canReset;
|
|
||||||
},
|
|
||||||
color(): string {
|
|
||||||
return layers[this.layer].color;
|
|
||||||
},
|
|
||||||
prestigeButtonDisplay(): Component | string | null {
|
|
||||||
if (layers[this.layer].prestigeButtonDisplay) {
|
|
||||||
return coerceComponent(layers[this.layer].prestigeButtonDisplay!);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.canReset ? { backgroundColor: this.color } : undefined,
|
|
||||||
layers[this.layer].componentStyles?.["prestige-button"]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resetLayer() {
|
|
||||||
resetLayer(this.layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.reset {
|
|
||||||
min-height: 100px;
|
|
||||||
width: 180px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 4px solid rgba(0, 0, 0, 0.125);
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,76 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="resource-display" :class="{ empty }">
|
|
||||||
<div v-if="baseAmount != undefined && baseResource != undefined">
|
|
||||||
You have {{ baseAmount }} {{ baseResource }}
|
|
||||||
</div>
|
|
||||||
<div v-if="passiveGeneration != undefined">
|
|
||||||
You are gaining {{ passiveGeneration }} {{ resource }} per second
|
|
||||||
</div>
|
|
||||||
<spacer
|
|
||||||
v-if="
|
|
||||||
(baseAmount != undefined || passiveGeneration != undefined) &&
|
|
||||||
(best != undefined || total != undefined)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div v-if="best != undefined">Your best {{ resource }} is {{ best }}</div>
|
|
||||||
<div v-if="total != undefined">You have made a total of {{ total }} {{ resource }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { DecimalSource } from "@/lib/break_eternity";
|
|
||||||
import Decimal, { formatWhole } from "@/util/bignum";
|
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "resource-display",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
computed: {
|
|
||||||
baseAmount(): string | null {
|
|
||||||
return layers[this.layer].baseAmount
|
|
||||||
? formatWhole(layers[this.layer].baseAmount!)
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
baseResource(): string | undefined {
|
|
||||||
return layers[this.layer].baseResource;
|
|
||||||
},
|
|
||||||
passiveGeneration(): string | null {
|
|
||||||
return layers[this.layer].passiveGeneration
|
|
||||||
? formatWhole(
|
|
||||||
Decimal.times(
|
|
||||||
layers[this.layer].resetGain,
|
|
||||||
layers[this.layer].passiveGeneration === true
|
|
||||||
? 1
|
|
||||||
: (layers[this.layer].passiveGeneration as DecimalSource)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
resource(): string {
|
|
||||||
return layers[this.layer].resource;
|
|
||||||
},
|
|
||||||
best(): string | null {
|
|
||||||
return player.layers[this.layer].best
|
|
||||||
? formatWhole(player.layers[this.layer].best as Decimal)
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
total(): string | null {
|
|
||||||
return player.layers[this.layer].total
|
|
||||||
? formatWhole(player.layers[this.layer].total as Decimal)
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
empty(): boolean {
|
|
||||||
return !(this.baseAmount || this.passiveGeneration || this.best || this.total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.resource-display:not(.empty) {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,121 +0,0 @@
|
||||||
<template>
|
|
||||||
<div style="display: flex">
|
|
||||||
<tooltip display="Disable respec confirmation">
|
|
||||||
<Toggle :value="confirmRespec" @change="setConfirmRespec" />
|
|
||||||
</tooltip>
|
|
||||||
<button
|
|
||||||
@click="respec"
|
|
||||||
:class="{ feature: true, respec: true, can: unlocked, locked: !unlocked }"
|
|
||||||
style="margin-right: 18px"
|
|
||||||
:style="style"
|
|
||||||
>
|
|
||||||
<component :is="respecButtonDisplay" />
|
|
||||||
</button>
|
|
||||||
<Modal :show="confirming" @close="cancel">
|
|
||||||
<template v-slot:header>
|
|
||||||
<h2>Confirm Respec</h2>
|
|
||||||
</template>
|
|
||||||
<template v-slot:body>
|
|
||||||
<slot name="respec-warning">
|
|
||||||
<component :is="respecWarning" />
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
<template v-slot:footer>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-flex-grow"></div>
|
|
||||||
<danger-button class="button modal-button" @click="confirm" skipConfirm
|
|
||||||
>Yes</danger-button
|
|
||||||
>
|
|
||||||
<button class="button modal-button" @click="cancel">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { CoercableComponent } from "@/typings/component";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "respec-button",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
confirmRespec: Boolean,
|
|
||||||
display: [String, Object] as PropType<CoercableComponent>,
|
|
||||||
respecWarningDisplay: [String, Object] as PropType<CoercableComponent>
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
confirming: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
emits: ["set-confirm-respec", "respec"],
|
|
||||||
computed: {
|
|
||||||
style(): Partial<CSSStyleDeclaration> | undefined {
|
|
||||||
return layers[this.layer].componentStyles?.["respec-button"];
|
|
||||||
},
|
|
||||||
unlocked(): boolean {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
},
|
|
||||||
respecButtonDisplay(): Component | string {
|
|
||||||
if (this.display) {
|
|
||||||
return coerceComponent(this.display);
|
|
||||||
}
|
|
||||||
return coerceComponent("Respec");
|
|
||||||
},
|
|
||||||
respecWarning(): Component | string {
|
|
||||||
if (this.respecWarningDisplay) {
|
|
||||||
return coerceComponent(this.respecWarningDisplay);
|
|
||||||
}
|
|
||||||
return coerceComponent(
|
|
||||||
`Are you sure you want to respec? This will force you to do a ${layers[this.layer]
|
|
||||||
.name || this.layer} respec as well!`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setConfirmRespec(value: boolean) {
|
|
||||||
this.$emit("set-confirm-respec", value);
|
|
||||||
},
|
|
||||||
respec() {
|
|
||||||
if (this.confirmRespec) {
|
|
||||||
this.confirming = true;
|
|
||||||
} else {
|
|
||||||
this.$emit("respec");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirm() {
|
|
||||||
this.$emit("respec");
|
|
||||||
this.confirming = false;
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
this.confirming = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.respec {
|
|
||||||
height: 40px;
|
|
||||||
width: 80px;
|
|
||||||
background: var(--points);
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.125);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-flex-grow {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,32 +0,0 @@
|
||||||
<template>
|
|
||||||
<LayerProvider :layer="layer" :index="tab.index">
|
|
||||||
<component :is="display" />
|
|
||||||
</LayerProvider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "subtab",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
display(): Component | string | undefined {
|
|
||||||
return (
|
|
||||||
layers[this.layer].subtabs![this.id].display &&
|
|
||||||
coerceComponent(layers[this.layer].subtabs![this.id].display!)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
12
src/components/features/Tab.vue
Normal file
12
src/components/features/Tab.vue
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<component :is="component" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CoercableComponent } from "@/features/feature";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
|
const { display } = toRefs(defineProps<{ display: CoercableComponent }>());
|
||||||
|
const component = computed(() => coerceComponent(display));
|
||||||
|
</script>
|
62
src/components/features/TabButton.vue
Normal file
62
src/components/features/TabButton.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
@click="emits('selectTab')"
|
||||||
|
class="tabButton"
|
||||||
|
:style="style"
|
||||||
|
:class="{
|
||||||
|
active,
|
||||||
|
...classes
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<component :is="component" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FeatureComponent } from "@/features/feature";
|
||||||
|
import { GenericTabButton } from "@/features/tabFamily";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
|
const props = toRefs(defineProps<FeatureComponent<GenericTabButton> & { active: boolean }>());
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: "selectTab"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const component = computed(() => coerceComponent(props.display.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tabButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 20px;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton:hover {
|
||||||
|
transform: scale(1.1, 1.1);
|
||||||
|
text-shadow: 0 0 7px var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(.floating) > .tabButton {
|
||||||
|
height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-width: 4px;
|
||||||
|
border-radius: 0;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(.floating) .tabButton:not(.active) {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
138
src/components/features/TabFamily.vue
Normal file
138
src/components/features/TabFamily.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div class="tab-family-container" :class="classes" :style="style">
|
||||||
|
<Sticky class="tab-buttons-container">
|
||||||
|
<div class="tab-buttons" :class="{ floating }">
|
||||||
|
<TabButton
|
||||||
|
v-for="(button, id) in tabs"
|
||||||
|
@selectTab="selectTab(id)"
|
||||||
|
:key="id"
|
||||||
|
:active="button.tab === activeTab"
|
||||||
|
v-bind="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Sticky>
|
||||||
|
<template v-if="activeTab">
|
||||||
|
<component :is="display" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import themes from "@/data/themes";
|
||||||
|
import { FeatureComponent } from "@/features/feature";
|
||||||
|
import { GenericTabFamily } from "@/features/tabFamily";
|
||||||
|
import settings from "@/game/settings";
|
||||||
|
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import Sticky from "../system/Sticky.vue";
|
||||||
|
|
||||||
|
const props = toRefs(defineProps<FeatureComponent<GenericTabFamily>>());
|
||||||
|
|
||||||
|
const floating = computed(() => {
|
||||||
|
return themes[settings.theme].floatingTabs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const display = computed(() => {
|
||||||
|
const activeTab = props.activeTab.value;
|
||||||
|
return activeTab
|
||||||
|
? coerceComponent(isCoercableComponent(activeTab) ? activeTab : activeTab.display)
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => {
|
||||||
|
const activeTab = props.activeTab.value;
|
||||||
|
const tabClasses =
|
||||||
|
isCoercableComponent(activeTab) || !activeTab ? undefined : unref(activeTab.classes);
|
||||||
|
return tabClasses;
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
const activeTab = props.activeTab.value;
|
||||||
|
return isCoercableComponent(activeTab) || !activeTab ? undefined : unref(activeTab.style);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectTab(tab: string) {
|
||||||
|
props.state.value = tab;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-family-container {
|
||||||
|
margin: var(--feature-margin) -11px;
|
||||||
|
position: relative;
|
||||||
|
border: solid 4px var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons:not(.floating) {
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: inherit;
|
||||||
|
border-width: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-family-container .sticky {
|
||||||
|
margin-left: unset !important;
|
||||||
|
margin-right: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
padding-right: 60px;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container:not(.floating) {
|
||||||
|
border-top: solid 4px var(--outline);
|
||||||
|
border-bottom: solid 4px var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container:not(.floating) .tab-buttons {
|
||||||
|
width: calc(100% + 14px);
|
||||||
|
margin-left: -7px;
|
||||||
|
margin-right: -7px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 14px;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container.floating .tab-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: -25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .tab-buttons {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showGoBack > .tab-buttons-container:not(.floating) .subtabs {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container:not(.floating):first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimizable > .tab-buttons-container:not(.floating):first-child {
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container:not(.floating):first-child .tab-buttons {
|
||||||
|
margin-top: -50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(.showGoBack) > .tab-buttons-container:not(.floating) .tab-buttons {
|
||||||
|
padding-left: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons-container + * {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,64 +1,65 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
v-if="upgrade.unlocked"
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
:style="style"
|
:style="style"
|
||||||
@click="buy"
|
@click="purchase"
|
||||||
:class="{
|
:class="{
|
||||||
feature: true,
|
feature: true,
|
||||||
[layer]: true,
|
|
||||||
upgrade: true,
|
upgrade: true,
|
||||||
can: upgrade.canAfford && !upgrade.bought,
|
can: canPurchase && !bought,
|
||||||
locked: !upgrade.canAfford && !upgrade.bought,
|
locked: !canPurchase && !bought,
|
||||||
bought: upgrade.bought
|
bought,
|
||||||
|
...classes
|
||||||
}"
|
}"
|
||||||
:disabled="!upgrade.canAfford && !upgrade.bought"
|
:disabled="!canPurchase && !bought"
|
||||||
>
|
>
|
||||||
<component v-if="fullDisplay" :is="fullDisplay" />
|
<component v-if="component" :is="component" />
|
||||||
<default-upgrade-display v-else :id="id" />
|
<MarkNode :mark="mark" />
|
||||||
<branch-node :branches="upgrade.branches" :id="id" featureType="upgrade" />
|
<LinkNode :id="id" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="tsx">
|
||||||
import { layers } from "@/game/layers";
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
import { Upgrade } from "@/typings/features/upgrade";
|
import { displayResource } from "@/features/resource";
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
import { GenericUpgrade } from "@/features/upgrade";
|
||||||
import { Component, defineComponent } from "vue";
|
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import LinkNode from "../system/LinkNode.vue";
|
||||||
|
import MarkNode from "./MarkNode.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<GenericUpgrade>>());
|
||||||
name: "upgrade",
|
|
||||||
mixins: [InjectLayerMixin],
|
const component = computed(() => {
|
||||||
props: {
|
const display = unref(props.display);
|
||||||
id: {
|
if (display == null) {
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
upgrade(): Upgrade {
|
|
||||||
return layers[this.layer].upgrades!.data[this.id];
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.upgrade.canAfford && !this.upgrade.bought
|
|
||||||
? { backgroundColor: layers[this.layer].color }
|
|
||||||
: undefined,
|
|
||||||
layers[this.layer].componentStyles?.upgrade,
|
|
||||||
this.upgrade.style
|
|
||||||
];
|
|
||||||
},
|
|
||||||
fullDisplay(): Component | string | null {
|
|
||||||
if (this.upgrade.fullDisplay) {
|
|
||||||
return coerceComponent(this.upgrade.fullDisplay, "div");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
if (isCoercableComponent(display)) {
|
||||||
methods: {
|
return coerceComponent(display);
|
||||||
buy() {
|
|
||||||
this.upgrade.buy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<div v-if={display.title}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
<component v-is={coerceComponent(display.title!, "h2")} />
|
||||||
|
</div>
|
||||||
|
<component v-is={coerceComponent(display.description, "div")} />
|
||||||
|
<div v-if={display.effectDisplay}>
|
||||||
|
<br />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Currently: <component v-is={coerceComponent(display.effectDisplay!)} />
|
||||||
|
</div>
|
||||||
|
<template v-if={unref(props.resource) != null && unref(props.cost) != null}>
|
||||||
|
<br />
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Cost: {displayResource(unref(props.resource)!, unref(props.cost))}{" "}
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
{unref(props.resource)!.displayName}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="filtered" class="table">
|
|
||||||
<template v-if="rows && cols">
|
|
||||||
<div v-for="row in rows" class="row" :key="row">
|
|
||||||
<div v-for="col in cols" :key="col">
|
|
||||||
<upgrade
|
|
||||||
v-if="filtered[row * 10 + col] !== undefined"
|
|
||||||
class="align"
|
|
||||||
:id="row * 10 + col"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<row v-else>
|
|
||||||
<upgrade v-for="(upgrade, id) in filtered" :key="id" class="align" :id="id" />
|
|
||||||
</row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Upgrade } from "@/typings/features/upgrade";
|
|
||||||
import { FilteredFeaturesMixin, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "upgrades",
|
|
||||||
mixins: [InjectLayerMixin, FilteredFeaturesMixin<Upgrade>("upgrades")]
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
230
src/components/features/board/Board.vue
Normal file
230
src/components/features/board/Board.vue
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
<template>
|
||||||
|
<panZoom
|
||||||
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
|
:style="[
|
||||||
|
{
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
},
|
||||||
|
style
|
||||||
|
]"
|
||||||
|
:class="classes"
|
||||||
|
selector=".g1"
|
||||||
|
:options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
|
||||||
|
ref="stage"
|
||||||
|
@init="onInit"
|
||||||
|
@mousemove="drag"
|
||||||
|
@touchmove="drag"
|
||||||
|
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
||||||
|
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
||||||
|
@mouseup="() => endDragging(dragging)"
|
||||||
|
@touchend="() => endDragging(dragging)"
|
||||||
|
@mouseleave="() => endDragging(dragging)"
|
||||||
|
>
|
||||||
|
<svg class="stage" width="100%" height="100%">
|
||||||
|
<g class="g1">
|
||||||
|
<transition-group name="link" appear>
|
||||||
|
<g v-for="(link, i) in links || []" :key="i">
|
||||||
|
<BoardLinkVue :link="link" />
|
||||||
|
</g>
|
||||||
|
</transition-group>
|
||||||
|
<transition-group name="grow" :duration="500" appear>
|
||||||
|
<g v-for="node in sortedNodes" :key="node.id" style="transition-duration: 0s">
|
||||||
|
<BoardNodeVue
|
||||||
|
:node="node"
|
||||||
|
:nodeType="types[node.type]"
|
||||||
|
:dragging="draggingNode"
|
||||||
|
:dragged="dragged"
|
||||||
|
:hasDragged="hasDragged"
|
||||||
|
:receivingNode="receivingNode?.id === node.id"
|
||||||
|
:selectedNode="selectedNode"
|
||||||
|
:selectedAction="selectedAction"
|
||||||
|
@mouseDown="mouseDown"
|
||||||
|
@endDragging="endDragging"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</transition-group>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</panZoom>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BoardNode, GenericBoard, getNodeProperty } from "@/features/board";
|
||||||
|
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||||
|
import { computed, ref, toRefs } from "vue";
|
||||||
|
import panZoom from "vue-panzoom";
|
||||||
|
import BoardLinkVue from "./BoardLink.vue";
|
||||||
|
import BoardNodeVue from "./BoardNode.vue";
|
||||||
|
|
||||||
|
const props = toRefs(defineProps<FeatureComponent<GenericBoard>>());
|
||||||
|
|
||||||
|
const lastMousePosition = ref({ x: 0, y: 0 });
|
||||||
|
const dragged = ref({ x: 0, y: 0 });
|
||||||
|
const dragging = ref<number | null>(null);
|
||||||
|
const hasDragged = ref(false);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const stage = ref<any>(null);
|
||||||
|
|
||||||
|
const draggingNode = computed(() =>
|
||||||
|
dragging.value == null ? undefined : props.nodes.value.find(node => node.id === dragging.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedNodes = computed(() => {
|
||||||
|
const nodes = props.nodes.value.slice();
|
||||||
|
if (draggingNode.value) {
|
||||||
|
const node = nodes.splice(nodes.indexOf(draggingNode.value), 1)[0];
|
||||||
|
nodes.push(node);
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
});
|
||||||
|
|
||||||
|
const receivingNode = computed(() => {
|
||||||
|
const node = draggingNode.value;
|
||||||
|
if (node == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
x: node.position.x + dragged.value.x,
|
||||||
|
y: node.position.y + dragged.value.y
|
||||||
|
};
|
||||||
|
let smallestDistance = Number.MAX_VALUE;
|
||||||
|
return props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
|
||||||
|
if (curr.id === node.id) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
const nodeType = props.types.value[curr.type];
|
||||||
|
const canAccept = getNodeProperty(nodeType.canAccept, curr);
|
||||||
|
if (!canAccept) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distanceSquared =
|
||||||
|
Math.pow(position.x - curr.position.x, 2) + Math.pow(position.y - curr.position.y, 2);
|
||||||
|
let size = getNodeProperty(nodeType.size, curr);
|
||||||
|
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
smallestDistance = distanceSquared;
|
||||||
|
return curr;
|
||||||
|
}, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function onInit(panzoomInstance: any) {
|
||||||
|
panzoomInstance.setTransformOrigin(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, draggable = false) {
|
||||||
|
if (dragging.value == 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;
|
||||||
|
}
|
||||||
|
lastMousePosition.value = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
dragged.value = { x: 0, y: 0 };
|
||||||
|
hasDragged.value = false;
|
||||||
|
|
||||||
|
if (draggable) {
|
||||||
|
dragging.value = nodeID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeID != null) {
|
||||||
|
props.state.value.selectedNode = null;
|
||||||
|
props.state.value.selectedAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(e: MouseEvent | TouchEvent) {
|
||||||
|
const zoom = stage.value.$panZoomInstance.getTransform().scale;
|
||||||
|
|
||||||
|
let clientX, clientY;
|
||||||
|
if ("touches" in e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
clientX = e.touches[0].clientX;
|
||||||
|
clientY = e.touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
endDragging(dragging.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientX = e.clientX;
|
||||||
|
clientY = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragged.value = {
|
||||||
|
x: dragged.value.x + (clientX - lastMousePosition.value.x) / zoom,
|
||||||
|
y: dragged.value.y + (clientY - lastMousePosition.value.y) / zoom
|
||||||
|
};
|
||||||
|
lastMousePosition.value = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Math.abs(dragged.value.x) > 10 || Math.abs(dragged.value.y) > 10) {
|
||||||
|
hasDragged.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragging.value) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDragging(nodeID: number | null) {
|
||||||
|
if (dragging.value != null && dragging.value === nodeID && draggingNode.value != null) {
|
||||||
|
draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
|
||||||
|
draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
|
||||||
|
|
||||||
|
const nodes = props.nodes.value;
|
||||||
|
nodes.splice(nodes.indexOf(draggingNode.value), 1);
|
||||||
|
nodes.push(draggingNode.value);
|
||||||
|
|
||||||
|
if (receivingNode.value) {
|
||||||
|
props.types.value[receivingNode.value.type].onDrop?.(
|
||||||
|
receivingNode.value,
|
||||||
|
draggingNode.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging.value = null;
|
||||||
|
} else if (!hasDragged.value) {
|
||||||
|
props.state.value.selectedNode = null;
|
||||||
|
props.state.value.selectedAction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vue-pan-zoom-scene {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g1 {
|
||||||
|
transition-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-enter-from,
|
||||||
|
.link-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
60
src/components/features/board/BoardLink.vue
Normal file
60
src/components/features/board/BoardLink.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<line
|
||||||
|
class="link"
|
||||||
|
v-bind="link"
|
||||||
|
:class="{ pulsing: link.pulsing }"
|
||||||
|
:x1="startPosition.x"
|
||||||
|
:y1="startPosition.y"
|
||||||
|
:x2="endPosition.x"
|
||||||
|
:y2="endPosition.y"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BoardNodeLink } from "@/features/board";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
|
||||||
|
const props = toRefs(
|
||||||
|
defineProps<{
|
||||||
|
link: BoardNodeLink;
|
||||||
|
}>()
|
||||||
|
);
|
||||||
|
|
||||||
|
const startPosition = computed(() => {
|
||||||
|
const position = props.link.value.startNode.position;
|
||||||
|
if (props.link.value.offsetStart) {
|
||||||
|
position.x += unref(props.link.value.offsetStart).x;
|
||||||
|
position.y += unref(props.link.value.offsetStart).y;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
});
|
||||||
|
|
||||||
|
const endPosition = computed(() => {
|
||||||
|
const position = props.link.value.endNode.position;
|
||||||
|
if (props.link.value.offsetEnd) {
|
||||||
|
position.x += unref(props.link.value.offsetEnd).x;
|
||||||
|
position.y += unref(props.link.value.offsetEnd).y;
|
||||||
|
}
|
||||||
|
return 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>
|
|
@ -6,7 +6,7 @@
|
||||||
:transform="`translate(${position.x},${position.y})`"
|
:transform="`translate(${position.x},${position.y})`"
|
||||||
>
|
>
|
||||||
<transition name="actions" appear>
|
<transition name="actions" appear>
|
||||||
<g v-if="selected && actions">
|
<g v-if="isSelected && actions">
|
||||||
<!-- TODO move to separate file -->
|
<!-- TODO move to separate file -->
|
||||||
<g
|
<g
|
||||||
v-for="(action, index) in actions"
|
v-for="(action, index) in actions"
|
||||||
|
@ -27,19 +27,13 @@
|
||||||
@touchend.stop="e => actionMouseUp(e, action)"
|
@touchend.stop="e => actionMouseUp(e, action)"
|
||||||
>
|
>
|
||||||
<circle
|
<circle
|
||||||
:fill="
|
:fill="getNodeProperty(action.fillColor, node)"
|
||||||
action.fillColor
|
|
||||||
? typeof action.fillColor === 'function'
|
|
||||||
? action.fillColor(node)
|
|
||||||
: action.fillColor
|
|
||||||
: fillColor
|
|
||||||
"
|
|
||||||
r="20"
|
r="20"
|
||||||
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
||||||
:stroke="outlineColor"
|
:stroke="outlineColor"
|
||||||
/>
|
/>
|
||||||
<text :fill="titleColor" class="material-icons">{{
|
<text :fill="titleColor" class="material-icons">{{
|
||||||
typeof action.icon === "function" ? action.icon(node) : action.icon
|
getNodeProperty(action.icon, node)
|
||||||
}}</text>
|
}}</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
@ -47,8 +41,8 @@
|
||||||
|
|
||||||
<g
|
<g
|
||||||
class="node-container"
|
class="node-container"
|
||||||
@mouseenter="mouseEnter"
|
@mouseenter="isHovering = true"
|
||||||
@mouseleave="mouseLeave"
|
@mouseleave="isHovering = false"
|
||||||
@mousedown="mouseDown"
|
@mousedown="mouseDown"
|
||||||
@touchstart="mouseDown"
|
@touchstart="mouseDown"
|
||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
|
@ -163,7 +157,7 @@
|
||||||
|
|
||||||
<transition name="fade" appear>
|
<transition name="fade" appear>
|
||||||
<text
|
<text
|
||||||
v-if="selected && selectedAction"
|
v-if="isSelected && selectedAction"
|
||||||
:fill="titleColor"
|
:fill="titleColor"
|
||||||
class="node-title"
|
class="node-title"
|
||||||
:y="size + 75"
|
:y="size + 75"
|
||||||
|
@ -173,192 +167,140 @@
|
||||||
</g>
|
</g>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import themes from "@/data/themes";
|
import themes from "@/data/themes";
|
||||||
import { ProgressDisplay, Shape } from "@/game/enums";
|
import {
|
||||||
import { layers } from "@/game/layers";
|
BoardNode,
|
||||||
import player from "@/game/player";
|
GenericBoardNodeAction,
|
||||||
import settings from "@/game/settings";
|
GenericNodeType,
|
||||||
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
|
getNodeProperty,
|
||||||
import { getNodeTypeProperty } from "@/util/features";
|
|
||||||
import { defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "BoardNode",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
ProgressDisplay,
|
ProgressDisplay,
|
||||||
Shape,
|
Shape
|
||||||
hovering: false,
|
} from "@/features/board";
|
||||||
sqrtTwo: Math.sqrt(2)
|
import { Visibility } from "@/features/feature";
|
||||||
|
import settings from "@/game/settings";
|
||||||
|
import { computed, ref, toRefs, unref, watch } from "vue";
|
||||||
|
|
||||||
|
const sqrtTwo = Math.sqrt(2);
|
||||||
|
|
||||||
|
const props = toRefs(
|
||||||
|
defineProps<{
|
||||||
|
node: BoardNode;
|
||||||
|
nodeType: GenericNodeType;
|
||||||
|
dragging?: BoardNode;
|
||||||
|
dragged?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
};
|
};
|
||||||
},
|
hasDragged?: boolean;
|
||||||
emits: ["mouseDown", "endDragging"],
|
receivingNode?: boolean;
|
||||||
props: {
|
selectedNode?: BoardNode | null;
|
||||||
node: {
|
selectedAction?: GenericBoardNodeAction | null;
|
||||||
type: Object as PropType<BoardNode>,
|
}>()
|
||||||
required: true
|
);
|
||||||
},
|
const emit = defineEmits<{
|
||||||
nodeType: {
|
(e: "mouseDown", event: MouseEvent | TouchEvent, node: number, isDraggable: boolean): void;
|
||||||
type: Object as PropType<NodeType>,
|
(e: "endDragging", node: number): void;
|
||||||
required: true
|
}>();
|
||||||
},
|
|
||||||
dragging: {
|
const isHovering = ref(false);
|
||||||
type: Object as PropType<BoardNode>
|
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
|
||||||
},
|
const isDraggable = computed(() =>
|
||||||
dragged: {
|
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
||||||
type: Object as PropType<{ x: number; y: number }>,
|
);
|
||||||
required: true
|
|
||||||
},
|
watch(isDraggable, value => {
|
||||||
hasDragged: {
|
const node = unref(props.node);
|
||||||
type: Boolean,
|
if (unref(props.dragging) === node && !value) {
|
||||||
default: false
|
emit("endDragging", node.id);
|
||||||
},
|
|
||||||
receivingNode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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 {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
|
|
||||||
},
|
|
||||||
position(): { x: number; y: number } {
|
|
||||||
return this.draggable && this.dragging?.id === this.node.id
|
|
||||||
? {
|
|
||||||
x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
|
|
||||||
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
|
|
||||||
}
|
|
||||||
: this.node.position;
|
|
||||||
},
|
|
||||||
shape(): Shape {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "shape");
|
|
||||||
},
|
|
||||||
size(): number {
|
|
||||||
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 {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "title");
|
|
||||||
},
|
|
||||||
label(): NodeLabel | null | undefined {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "label");
|
|
||||||
},
|
|
||||||
progress(): number {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
|
|
||||||
},
|
|
||||||
backgroundColor(): string {
|
|
||||||
return themes[settings.theme].variables["--background"];
|
|
||||||
},
|
|
||||||
outlineColor(): string {
|
|
||||||
return (
|
|
||||||
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
|
|
||||||
themes[settings.theme].variables["--outline"]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
fillColor(): string {
|
|
||||||
return (
|
|
||||||
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
|
|
||||||
themes[settings.theme].variables["--raised-background"]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progressColor(): string {
|
|
||||||
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
|
|
||||||
},
|
|
||||||
titleColor(): string {
|
|
||||||
return (
|
|
||||||
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
|
|
||||||
themes[settings.theme].variables["--foreground"]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
progressDisplay(): ProgressDisplay {
|
|
||||||
return (
|
|
||||||
getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
|
|
||||||
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: {
|
|
||||||
mouseDown(e: MouseEvent | TouchEvent) {
|
|
||||||
this.$emit("mouseDown", e, this.node.id, this.draggable);
|
|
||||||
},
|
|
||||||
mouseUp() {
|
|
||||||
if (!this.hasDragged) {
|
|
||||||
this.nodeType.onClick?.(this.node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mouseEnter() {
|
|
||||||
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.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
|
|
||||||
if (this.board.selectedAction?.id === action.id) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
onDraggableChanged() {
|
|
||||||
if (this.dragging === this.node && !this.draggable) {
|
|
||||||
this.$emit("endDragging", this.node.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const actions = computed(() => {
|
||||||
|
const node = unref(props.node);
|
||||||
|
return getNodeProperty(props.nodeType.value.actions, node)?.filter(
|
||||||
|
action => getNodeProperty(action.visibility, node) !== Visibility.None
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const position = computed(() => {
|
||||||
|
const node = unref(props.node);
|
||||||
|
const dragged = unref(props.dragged);
|
||||||
|
|
||||||
|
return getNodeProperty(props.nodeType.value.draggable, node) &&
|
||||||
|
unref(props.dragging)?.id === node.id &&
|
||||||
|
dragged
|
||||||
|
? {
|
||||||
|
x: node.position.x + Math.round(dragged.x / 25) * 25,
|
||||||
|
y: node.position.y + Math.round(dragged.y / 25) * 25
|
||||||
|
}
|
||||||
|
: node.position;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
|
||||||
|
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
|
||||||
|
const label = computed(() => getNodeProperty(props.nodeType.value.label, unref(props.node)));
|
||||||
|
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
||||||
|
const progress = computed(
|
||||||
|
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) || 0
|
||||||
|
);
|
||||||
|
const backgroundColor = computed(() => themes[settings.theme].variables["--background"]);
|
||||||
|
const outlineColor = computed(
|
||||||
|
() =>
|
||||||
|
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ||
|
||||||
|
themes[settings.theme].variables["--outline"]
|
||||||
|
);
|
||||||
|
const fillColor = computed(
|
||||||
|
() =>
|
||||||
|
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ||
|
||||||
|
themes[settings.theme].variables["--raised-background"]
|
||||||
|
);
|
||||||
|
const progressColor = computed(() =>
|
||||||
|
getNodeProperty(props.nodeType.value.progressColor, unref(props.node))
|
||||||
|
);
|
||||||
|
const titleColor = computed(
|
||||||
|
() =>
|
||||||
|
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ||
|
||||||
|
themes[settings.theme].variables["--foreground"]
|
||||||
|
);
|
||||||
|
const progressDisplay = computed(() =>
|
||||||
|
getNodeProperty(props.nodeType.value.progressDisplay, unref(props.node))
|
||||||
|
);
|
||||||
|
const canAccept = computed(
|
||||||
|
() =>
|
||||||
|
unref(props.dragging) != null &&
|
||||||
|
unref(props.hasDragged) &&
|
||||||
|
getNodeProperty(props.nodeType.value.canAccept, unref(props.node))
|
||||||
|
);
|
||||||
|
const actionDistance = computed(() =>
|
||||||
|
getNodeProperty(props.nodeType.value.actionDistance, unref(props.node))
|
||||||
|
);
|
||||||
|
|
||||||
|
function mouseDown(e: MouseEvent | TouchEvent) {
|
||||||
|
emit("mouseDown", e, props.node.value.id, isDraggable.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseUp() {
|
||||||
|
if (!props.hasDragged?.value) {
|
||||||
|
props.nodeType.value.onClick?.(props.node.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
||||||
|
// If the onClick function made this action selected,
|
||||||
|
// don't propagate the event (which will deselect everything)
|
||||||
|
if (action.onClick(unref(props.node)) || unref(props.selectedAction)?.id === action.id) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
||||||
|
if (unref(props.selectedAction)?.id === action.id) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
47
src/components/features/tree/Tree.vue
Normal file
47
src/components/features/tree/Tree.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<span class="row" v-for="(row, index) in nodes" :key="index">
|
||||||
|
<TreeNode v-for="(node, nodeIndex) in row" :key="nodeIndex" v-bind="wrapFeature(node)" />
|
||||||
|
</span>
|
||||||
|
<span class="left-side-nodes" v-if="leftSideNodes">
|
||||||
|
<TreeNode
|
||||||
|
v-for="(node, nodeIndex) in leftSideNodes"
|
||||||
|
:key="nodeIndex"
|
||||||
|
v-bind="wrapFeature(node)"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="side-nodes" v-if="rightSideNodes">
|
||||||
|
<TreeNode
|
||||||
|
v-for="(node, nodeIndex) in rightSideNodes"
|
||||||
|
:key="nodeIndex"
|
||||||
|
v-bind="wrapFeature(node)"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FeatureComponent, wrapFeature } from "@/features/feature";
|
||||||
|
import { GenericTree } from "@/features/tree";
|
||||||
|
import TreeNode from "./TreeNode.vue";
|
||||||
|
|
||||||
|
defineProps<FeatureComponent<GenericTree>>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.row {
|
||||||
|
margin: 50px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-side-nodes {
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
top: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-nodes {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 65px;
|
||||||
|
}
|
||||||
|
</style>
|
106
src/components/features/tree/TreeNode.vue
Normal file
106
src/components/features/tree/TreeNode.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<Tooltip
|
||||||
|
v-if="visibility !== Visibility.None"
|
||||||
|
v-show="visibility === Visibility.Visible"
|
||||||
|
v-bind="typeof tooltip === 'object' ? wrapFeature(tooltip) : null"
|
||||||
|
:display="typeof tooltip === 'object' ? unref(tooltip.display) : tooltip || ''"
|
||||||
|
:force="forceTooltip"
|
||||||
|
:class="{
|
||||||
|
treeNode: true,
|
||||||
|
can: canClick,
|
||||||
|
small,
|
||||||
|
...classes
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="click"
|
||||||
|
@mousedown="start"
|
||||||
|
@mouseleave="stop"
|
||||||
|
@mouseup="stop"
|
||||||
|
@touchstart="start"
|
||||||
|
@touchend="stop"
|
||||||
|
@touchcancel="stop"
|
||||||
|
:style="[
|
||||||
|
{
|
||||||
|
backgroundColor: color,
|
||||||
|
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${glowColor}`
|
||||||
|
},
|
||||||
|
style ?? []
|
||||||
|
]"
|
||||||
|
:disabled="!canClick"
|
||||||
|
>
|
||||||
|
<component :is="component" />
|
||||||
|
</button>
|
||||||
|
<MarkNode :mark="mark" />
|
||||||
|
<LinkNode :id="id" />
|
||||||
|
</Tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { GenericTreeNode } from "@/features/tree";
|
||||||
|
import { coerceComponent, setupHoldToClick } from "@/util/vue";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import Tooltip from "@/components/system/Tooltip.vue";
|
||||||
|
import MarkNode from "../MarkNode.vue";
|
||||||
|
import { FeatureComponent, Visibility, wrapFeature } from "@/features/feature";
|
||||||
|
import LinkNode from "../../system/LinkNode.vue";
|
||||||
|
|
||||||
|
const props = toRefs(
|
||||||
|
defineProps<
|
||||||
|
FeatureComponent<GenericTreeNode> & {
|
||||||
|
small?: boolean;
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
);
|
||||||
|
|
||||||
|
function click(e: MouseEvent) {
|
||||||
|
if (e.shiftKey && props.tooltip) {
|
||||||
|
props.forceTooltip.value = !props.forceTooltip.value;
|
||||||
|
} else {
|
||||||
|
unref(props.onClick)?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = computed(() => {
|
||||||
|
const display = unref(props.display);
|
||||||
|
return display && coerceComponent(display);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.treeNode {
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 10px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeNode button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.125);
|
||||||
|
border-radius: inherit;
|
||||||
|
font-size: 40px;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0px 0px 20px var(--background);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeNode.small {
|
||||||
|
height: 60px;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeNode.small button {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,50 +1,48 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="container" :class="{ confirming }">
|
<span class="container" :class="{ confirming: isConfirming }">
|
||||||
<span v-if="confirming">Are you sure?</span>
|
<span v-if="isConfirming">Are you sure?</span>
|
||||||
<button @click.stop="click" class="button danger" :disabled="disabled">
|
<button @click.stop="click" class="button danger" :disabled="disabled">
|
||||||
<span v-if="confirming">Yes</span>
|
<span v-if="isConfirming">Yes</span>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</button>
|
</button>
|
||||||
<button v-if="confirming" class="button" @click.stop="cancel">No</button>
|
<button v-if="isConfirming" class="button" @click.stop="cancel">No</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { ref, toRefs, unref, watch } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "danger-button",
|
defineProps<{
|
||||||
data() {
|
disabled?: boolean;
|
||||||
return {
|
skipConfirm?: boolean;
|
||||||
confirming: false
|
}>()
|
||||||
};
|
);
|
||||||
},
|
const emit = defineEmits<{
|
||||||
props: {
|
(e: "click"): void;
|
||||||
disabled: Boolean,
|
(e: "confirmingChanged", value: boolean): void;
|
||||||
skipConfirm: Boolean
|
}>();
|
||||||
},
|
|
||||||
emits: ["click", "confirmingChanged"],
|
const isConfirming = ref(false);
|
||||||
watch: {
|
|
||||||
confirming(newValue) {
|
watch(isConfirming, isConfirming => {
|
||||||
this.$emit("confirmingChanged", newValue);
|
emit("confirmingChanged", isConfirming);
|
||||||
}
|
});
|
||||||
},
|
|
||||||
methods: {
|
function click() {
|
||||||
click() {
|
if (unref(props.skipConfirm)) {
|
||||||
if (this.skipConfirm) {
|
emit("click");
|
||||||
this.$emit("click");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.confirming) {
|
if (isConfirming.value) {
|
||||||
this.$emit("click");
|
emit("click");
|
||||||
}
|
}
|
||||||
this.confirming = !this.confirming;
|
isConfirming.value = !isConfirming.value;
|
||||||
},
|
}
|
||||||
cancel() {
|
|
||||||
this.confirming = false;
|
function cancel() {
|
||||||
}
|
isConfirming.value = false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -4,40 +4,34 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { nextTick, ref, toRefs } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
toRefs(
|
||||||
name: "feedback-button",
|
defineProps<{
|
||||||
data() {
|
left?: boolean;
|
||||||
return {
|
}>()
|
||||||
activated: false,
|
);
|
||||||
activatedTimeout: null
|
const emit = defineEmits<{
|
||||||
} as {
|
(e: "click"): void;
|
||||||
activated: boolean;
|
}>();
|
||||||
activatedTimeout: number | null;
|
|
||||||
};
|
const activated = ref(false);
|
||||||
},
|
const activatedTimeout = ref<number | null>(null);
|
||||||
props: {
|
|
||||||
left: Boolean
|
function click() {
|
||||||
},
|
emit("click");
|
||||||
emits: ["click"],
|
|
||||||
methods: {
|
|
||||||
click() {
|
|
||||||
this.$emit("click");
|
|
||||||
|
|
||||||
// Give feedback to user
|
// Give feedback to user
|
||||||
if (this.activatedTimeout) {
|
if (activatedTimeout.value) {
|
||||||
clearTimeout(this.activatedTimeout);
|
clearTimeout(activatedTimeout.value);
|
||||||
}
|
}
|
||||||
this.activated = false;
|
activated.value = false;
|
||||||
this.$nextTick(() => {
|
nextTick(() => {
|
||||||
this.activated = true;
|
activated.value = true;
|
||||||
this.activatedTimeout = setTimeout(() => (this.activated = false), 500);
|
activatedTimeout.value = setTimeout(() => (activated.value = false), 500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,40 +1,49 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-title" v-if="title">{{ title }}</span>
|
<span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
|
||||||
<vue-select
|
<VueNextSelect
|
||||||
:options="options"
|
:options="options"
|
||||||
:model-value="value"
|
v-model="value"
|
||||||
@update:modelValue="setSelected"
|
|
||||||
label-by="label"
|
label-by="label"
|
||||||
:value-by="getValue"
|
:reduce="(option: SelectOption) => option.value"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:close-on-select="closeOnSelect"
|
:close-on-select="closeOnSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { CoercableComponent } from "@/features/feature";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import VueNextSelect from "vue-next-select";
|
||||||
|
import "vue-next-select/dist/index.css";
|
||||||
|
|
||||||
export default defineComponent({
|
export type SelectOption = { label: string; value: unknown };
|
||||||
name: "Select",
|
|
||||||
props: {
|
const props = toRefs(
|
||||||
title: String,
|
defineProps<{
|
||||||
options: Array, // https://vue-select.org/guide/options.html#options-prop
|
title?: CoercableComponent;
|
||||||
value: [String, Object],
|
modelValue?: unknown;
|
||||||
default: [String, Object],
|
options: SelectOption[];
|
||||||
placeholder: String,
|
placeholder?: string;
|
||||||
closeOnSelect: Boolean
|
closeOnSelect?: boolean;
|
||||||
|
}>()
|
||||||
|
);
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: unknown): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const titleComponent = computed(
|
||||||
|
() => props.title?.value && coerceComponent(props.title.value, "span")
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return unref(props.modelValue);
|
||||||
},
|
},
|
||||||
emits: ["change"],
|
set(value: unknown) {
|
||||||
methods: {
|
emit("update:modelValue", value);
|
||||||
setSelected(value: any) {
|
|
||||||
value = value || this.default;
|
|
||||||
this.$emit("change", value);
|
|
||||||
},
|
|
||||||
getValue(item?: { value: any }) {
|
|
||||||
return item?.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,30 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-title" v-if="title">{{ title }}</span>
|
<span class="field-title" v-if="title">{{ title }}</span>
|
||||||
<tooltip :display="`${value}`" :class="{ fullWidth: !title }">
|
<Tooltip :display="`${value}`" :class="{ fullWidth: !title }">
|
||||||
<input
|
<input type="range" v-model="value" :min="min" :max="max" />
|
||||||
type="range"
|
</Tooltip>
|
||||||
:value="value"
|
|
||||||
@input="e => $emit('change', parseInt(e.target.value))"
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
/>
|
|
||||||
</tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
import Tooltip from "../system/Tooltip.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "Slider",
|
defineProps<{
|
||||||
props: {
|
title?: string;
|
||||||
title: String,
|
modelValue?: number;
|
||||||
value: Number,
|
min?: number;
|
||||||
min: Number,
|
max?: number;
|
||||||
max: Number
|
}>()
|
||||||
|
);
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return unref(props.modelValue) || 0;
|
||||||
},
|
},
|
||||||
emits: ["change"]
|
set(value: number) {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,69 @@
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="$emit('submit')">
|
<form @submit.prevent="submit">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-title" v-if="title">{{ title }}</span>
|
<span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
|
||||||
<textarea-autosize
|
<VueTextareaAutosize
|
||||||
v-if="textarea"
|
v-if="textArea"
|
||||||
|
v-model="value"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:value="val"
|
|
||||||
:maxHeight="maxHeight"
|
:maxHeight="maxHeight"
|
||||||
@input="change"
|
@blur="submit"
|
||||||
@blur="() => $emit('blur')"
|
|
||||||
ref="field"
|
ref="field"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
type="text"
|
type="text"
|
||||||
:value="val"
|
v-model="value"
|
||||||
@input="e => change(e.target.value)"
|
|
||||||
@blur="() => $emit('blur')"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
ref="field"
|
|
||||||
:class="{ fullWidth: !title }"
|
:class="{ fullWidth: !title }"
|
||||||
|
@blur="submit"
|
||||||
|
ref="field"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { CoercableComponent } from "@/features/feature";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { computed, onMounted, ref, toRefs, unref } from "vue";
|
||||||
|
import VueTextareaAutosize from "vue-textarea-autosize";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "TextField",
|
defineProps<{
|
||||||
props: {
|
title?: CoercableComponent;
|
||||||
title: String,
|
modelValue?: string;
|
||||||
value: String,
|
textArea?: boolean;
|
||||||
modelValue: String,
|
placeholder?: string;
|
||||||
textarea: Boolean,
|
maxHeight?: number;
|
||||||
placeholder: String,
|
}>()
|
||||||
maxHeight: Number
|
);
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: string): void;
|
||||||
|
(e: "submit"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const titleComponent = computed(
|
||||||
|
() => props.title?.value && coerceComponent(unref(props.title.value), "span")
|
||||||
|
);
|
||||||
|
|
||||||
|
const field = ref<HTMLElement | null>(null);
|
||||||
|
onMounted(() => {
|
||||||
|
field.value?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return unref(props.modelValue) || "";
|
||||||
},
|
},
|
||||||
emits: ["change", "submit", "blur", "update:modelValue"],
|
set(value: string) {
|
||||||
mounted() {
|
emit("update:modelValue", value);
|
||||||
(this.$refs.field as HTMLElement).focus();
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
val() {
|
|
||||||
return this.modelValue || this.value || "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
change(value: string) {
|
|
||||||
this.$emit("change", value);
|
|
||||||
this.$emit("update:modelValue", value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
emit("submit");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<input type="checkbox" class="toggle" :checked="value" @input="handleInput" />
|
<input type="checkbox" class="toggle" v-model="value" />
|
||||||
<component :is="display" />
|
<component :is="component" />
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { CoercableComponent } from "@/features/feature";
|
||||||
import { coerceComponent } from "@/util/vue";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { defineComponent } from "vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
|
||||||
// Reference: https://codepen.io/finnhvman/pen/pOeyjE
|
const props = toRefs(
|
||||||
export default defineComponent({
|
defineProps<{
|
||||||
name: "Toggle",
|
title?: CoercableComponent;
|
||||||
props: {
|
modelValue?: boolean;
|
||||||
title: String,
|
}>()
|
||||||
value: Boolean
|
);
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const component = computed(() => coerceComponent(unref(props.title) || "", "span"));
|
||||||
|
|
||||||
|
const value = computed({
|
||||||
|
get() {
|
||||||
|
return !!unref(props.modelValue);
|
||||||
},
|
},
|
||||||
emits: ["change"],
|
set(value: boolean) {
|
||||||
computed: {
|
emit("update:modelValue", value);
|
||||||
display() {
|
|
||||||
return coerceComponent(this.title || "", "span");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleInput(e: InputEvent) {
|
|
||||||
this.$emit("change", (e.target as HTMLInputElement).checked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
// Import and register all components,
|
|
||||||
// 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 { App } from "vue";
|
|
||||||
import VueNextSelect from "vue-next-select";
|
|
||||||
import "vue-next-select/dist/index.css";
|
|
||||||
import panZoom from "vue-panzoom";
|
|
||||||
import Sortable from "vue-sortable";
|
|
||||||
import VueTextareaAutosize from "vue-textarea-autosize";
|
|
||||||
import Toast from "vue-toastification";
|
|
||||||
import "vue-toastification/dist/index.css";
|
|
||||||
import Changelog from "../data/Changelog.vue";
|
|
||||||
|
|
||||||
export function registerComponents(vue: App): void {
|
|
||||||
/* from files */
|
|
||||||
const componentsContext = require.context("./");
|
|
||||||
componentsContext.keys().forEach(path => {
|
|
||||||
const component = componentsContext(path).default;
|
|
||||||
if (component && !(component.name in vue._context.components)) {
|
|
||||||
vue.component(component.name, component);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
vue.component("Changelog", Changelog);
|
|
||||||
|
|
||||||
/* from packages */
|
|
||||||
vue.component("collapse-transition", CollapseTransition);
|
|
||||||
vue.use(VueTextareaAutosize);
|
|
||||||
vue.use(Sortable);
|
|
||||||
vue.component("vue-select", VueNextSelect);
|
|
||||||
vue.use(panZoom);
|
|
||||||
vue.use(Toast);
|
|
||||||
}
|
|
|
@ -6,10 +6,4 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts"></script>
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "column"
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<infobox v-if="infobox != undefined" :id="infobox" />
|
|
||||||
<main-display />
|
|
||||||
<sticky v-if="showPrestigeButton"><prestige-button /></sticky>
|
|
||||||
<resource-display />
|
|
||||||
<milestones />
|
|
||||||
<component v-if="midsection" :is="midsection" />
|
|
||||||
<clickables />
|
|
||||||
<buyables />
|
|
||||||
<upgrades />
|
|
||||||
<challenges />
|
|
||||||
<achievements />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "default-layer-tab",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
computed: {
|
|
||||||
infobox(): string | undefined {
|
|
||||||
return (
|
|
||||||
layers[this.layer].infoboxes && Object.keys(layers[this.layer].infoboxes!.data)[0]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
midsection(): Component | string | null {
|
|
||||||
if (layers[this.layer].midsection) {
|
|
||||||
return coerceComponent(layers[this.layer].midsection!);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
showPrestigeButton(): boolean {
|
|
||||||
return layers[this.layer].type !== "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
82
src/components/system/Game.vue
Normal file
82
src/components/system/Game.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="tabs-container">
|
||||||
|
<div v-for="(tab, index) in tabs" :key="index" class="tab" :ref="`tab-${index}`">
|
||||||
|
<Nav v-if="index === 0 && !useHeader" />
|
||||||
|
<div class="inner-tab">
|
||||||
|
<Layer
|
||||||
|
v-if="layerKeys.includes(tab)"
|
||||||
|
v-bind="wrapFeature(layers[tab])"
|
||||||
|
:index="index"
|
||||||
|
:tab="() => ($refs[`tab-${index}`] as HTMLElement | undefined)"
|
||||||
|
/>
|
||||||
|
<component :is="tab" :index="index" v-else />
|
||||||
|
</div>
|
||||||
|
<div class="separator" v-if="index !== tabs.length - 1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import modInfo from "@/data/modInfo.json";
|
||||||
|
import { wrapFeature } from "@/features/feature";
|
||||||
|
import { layers } from "@/game/layers";
|
||||||
|
import player from "@/game/player";
|
||||||
|
import { computed, toRef } from "vue";
|
||||||
|
import Layer from "./Layer.vue";
|
||||||
|
import Nav from "./Nav.vue";
|
||||||
|
|
||||||
|
const tabs = toRef(player, "tabs");
|
||||||
|
const layerKeys = computed(() => Object.keys(layers));
|
||||||
|
const useHeader = modInfo.useHeader;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tabs-container {
|
||||||
|
width: 100vw;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
transition-duration: 0s;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-tab {
|
||||||
|
padding: 50px 10px;
|
||||||
|
min-height: calc(100% - 100px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
position: absolute;
|
||||||
|
right: -3px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
background: var(--outline);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tab hr {
|
||||||
|
height: 4px;
|
||||||
|
border: none;
|
||||||
|
background: var(--outline);
|
||||||
|
margin: 7px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab .modal-body hr {
|
||||||
|
margin: 7px 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="show">
|
<Modal :model-value="isOpen">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="game-over-modal-header">
|
<div class="game-over-modal-header">
|
||||||
<img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="modInfo" />
|
<img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="title" />
|
||||||
<div class="game-over-modal-title">
|
<div class="game-over-modal-title">
|
||||||
<h2>Congratulations!</h2>
|
<h2>Congratulations!</h2>
|
||||||
<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
|
<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
|
||||||
|
@ -35,43 +35,27 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Modal from "@/components/system/Modal.vue";
|
||||||
import { hasWon } from "@/data/mod";
|
import { hasWon } from "@/data/mod";
|
||||||
import modInfo from "@/data/modInfo.json";
|
import modInfo from "@/data/modInfo.json";
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { formatTime } from "@/util/bignum";
|
import { formatTime } from "@/util/bignum";
|
||||||
import { defineComponent } from "vue";
|
import { loadSave, newSave } from "@/util/save";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
||||||
name: "GameOverScreen",
|
|
||||||
data() {
|
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
const isOpen = computed(() => hasWon.value && !player.keepGoing);
|
||||||
return {
|
|
||||||
title,
|
function keepGoing() {
|
||||||
logo,
|
|
||||||
discordName,
|
|
||||||
discordLink,
|
|
||||||
versionNumber,
|
|
||||||
versionTitle
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
timePlayed() {
|
|
||||||
return formatTime(player.timePlayed);
|
|
||||||
},
|
|
||||||
show() {
|
|
||||||
return hasWon.value && !player.keepGoing;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
keepGoing() {
|
|
||||||
player.keepGoing = true;
|
player.keepGoing = true;
|
||||||
},
|
}
|
||||||
playAgain() {
|
|
||||||
console.warn("Not yet implemented!");
|
function playAgain() {
|
||||||
}
|
loadSave(newSave());
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="show" @close="$emit('closeDialog', 'Info')">
|
<Modal v-model="isOpen">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="info-modal-header">
|
<div class="info-modal-header">
|
||||||
<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
|
<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
Aarex
|
Aarex
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div class="link" @click="$emit('openDialog', 'Changelog')">
|
<div class="link" @click="openChangelog">
|
||||||
Changelog
|
Changelog
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
@ -51,60 +51,36 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>Time Played: {{ timePlayed }}</div>
|
<div>Time Played: {{ timePlayed }}</div>
|
||||||
<div v-if="hotkeys.length > 0">
|
|
||||||
<br />
|
|
||||||
<h4>Hotkeys</h4>
|
|
||||||
<div v-for="key in hotkeys" :key="key.key">
|
|
||||||
{{ key.key }}: {{ key.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Modal from "@/components/system/Modal.vue";
|
||||||
|
import type Changelog from "@/data/Changelog.vue";
|
||||||
import modInfo from "@/data/modInfo.json";
|
import modInfo from "@/data/modInfo.json";
|
||||||
import { hotkeys } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { formatTime } from "@/util/bignum";
|
import { formatTime } from "@/util/bignum";
|
||||||
import { defineComponent } from "vue";
|
import { computed, ref, toRefs, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
||||||
name: "Info",
|
|
||||||
data() {
|
const props = toRefs(defineProps<{ changelog: typeof Changelog | null }>());
|
||||||
const {
|
|
||||||
title,
|
const isOpen = ref(false);
|
||||||
logo,
|
|
||||||
author,
|
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||||
discordName,
|
|
||||||
discordLink,
|
defineExpose({
|
||||||
versionNumber,
|
open() {
|
||||||
versionTitle
|
isOpen.value = true;
|
||||||
} = modInfo;
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
logo,
|
|
||||||
author,
|
|
||||||
discordName,
|
|
||||||
discordLink,
|
|
||||||
versionNumber,
|
|
||||||
versionTitle
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: Boolean
|
|
||||||
},
|
|
||||||
emits: ["closeDialog", "openDialog"],
|
|
||||||
computed: {
|
|
||||||
timePlayed() {
|
|
||||||
return formatTime(player.timePlayed);
|
|
||||||
},
|
|
||||||
hotkeys() {
|
|
||||||
return hotkeys.filter(hotkey => hotkey.unlocked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function openChangelog() {
|
||||||
|
unref(props.changelog)?.open();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
173
src/components/system/Layer.vue
Normal file
173
src/components/system/Layer.vue
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<template>
|
||||||
|
<div class="layer-container">
|
||||||
|
<button v-if="showGoBack" class="goBack" @click="goBack">
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<button class="layer-tab minimized" v-if="minimized" @click="minimized = false">
|
||||||
|
<div>{{ name }}</div>
|
||||||
|
</button>
|
||||||
|
<div class="layer-tab" :style="style" :class="classes" v-else>
|
||||||
|
<Links v-if="links" :links="links">
|
||||||
|
<component :is="component" />
|
||||||
|
</Links>
|
||||||
|
<component v-else :is="component" />
|
||||||
|
</div>
|
||||||
|
<button v-if="minimizable" class="minimize" @click="minimized = true">
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Links from "@/components/system/Links.vue";
|
||||||
|
import { FeatureComponent } from "@/features/feature";
|
||||||
|
import { GenericLayer } from "@/game/layers";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { computed, nextTick, toRefs, unref, watch } from "vue";
|
||||||
|
import modInfo from "@/data/modInfo.json";
|
||||||
|
import player from "@/game/player";
|
||||||
|
|
||||||
|
const props = toRefs(
|
||||||
|
defineProps<
|
||||||
|
FeatureComponent<GenericLayer> & {
|
||||||
|
index: number;
|
||||||
|
tab: () => HTMLElement | undefined;
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = computed(() => coerceComponent(unref(props.display)));
|
||||||
|
const showGoBack = computed(
|
||||||
|
() => modInfo.allowGoBack && unref(props.index) > 0 && !props.minimized.value
|
||||||
|
);
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
player.tabs = player.tabs.slice(0, unref(props.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => updateTab(props.minimized.value, props.minWidth.value));
|
||||||
|
watch([props.minimized, props.minWidth], ([minimized, minWidth]) => updateTab(minimized, minWidth));
|
||||||
|
|
||||||
|
function updateTab(minimized: boolean, minWidth: number) {
|
||||||
|
const tabValue = props.tab.value();
|
||||||
|
if (tabValue != undefined) {
|
||||||
|
if (minimized) {
|
||||||
|
tabValue.style.flexGrow = "0";
|
||||||
|
tabValue.style.flexShrink = "0";
|
||||||
|
tabValue.style.width = "60px";
|
||||||
|
tabValue.style.minWidth = tabValue.style.flexBasis = "";
|
||||||
|
tabValue.style.margin = "0";
|
||||||
|
} else {
|
||||||
|
tabValue.style.flexGrow = "";
|
||||||
|
tabValue.style.flexShrink = "";
|
||||||
|
tabValue.style.width = "";
|
||||||
|
tabValue.style.minWidth = tabValue.style.flexBasis = `${minWidth}px`;
|
||||||
|
tabValue.style.margin = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layer-container {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-tab:not(.minimized) {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-tab.minimized {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 55px;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 40px;
|
||||||
|
color: var(--foreground);
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-tab.minimized div {
|
||||||
|
margin: 0;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
padding-left: 10px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
|
||||||
|
margin: -50px -10px;
|
||||||
|
padding: 50px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .layer-tab {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body .layer-tab:not(.hasSubtabs) {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimize {
|
||||||
|
position: sticky;
|
||||||
|
top: 6px;
|
||||||
|
right: 9px;
|
||||||
|
z-index: 7;
|
||||||
|
line-height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
border: none;
|
||||||
|
background: var(--background);
|
||||||
|
box-shadow: var(--background) 0 2px 3px 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: -44px;
|
||||||
|
margin-right: -30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimized + .minimize {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
top: 10px;
|
||||||
|
right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goBack {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 20px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 40px;
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goBack:hover {
|
||||||
|
transform: scale(1.1, 1.1);
|
||||||
|
text-shadow: 0 0 7px var(--foreground);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,33 +0,0 @@
|
||||||
<template>
|
|
||||||
<slot />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, reactive } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "LayerProvider",
|
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
tab: reactive({
|
|
||||||
layer: this.layer,
|
|
||||||
index: this.index
|
|
||||||
})
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
layer: String,
|
|
||||||
index: Number
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
layer(layer) {
|
|
||||||
this.$.provides.tab.layer = layer;
|
|
||||||
},
|
|
||||||
index(index) {
|
|
||||||
this.$.provides.tab.index = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,350 +0,0 @@
|
||||||
<template>
|
|
||||||
<LayerProvider :layer="layer" :index="index">
|
|
||||||
<div class="layer-container">
|
|
||||||
<button
|
|
||||||
v-if="index > 0 && allowGoBack && !minimized"
|
|
||||||
class="goBack"
|
|
||||||
@click="goBack(index)"
|
|
||||||
>
|
|
||||||
←
|
|
||||||
</button>
|
|
||||||
<button class="layer-tab minimized" v-if="minimized" @click="toggleMinimized">
|
|
||||||
<div>{{ name }}</div>
|
|
||||||
</button>
|
|
||||||
<div class="layer-tab" :style="style" :class="{ hasSubtabs: subtabs }" v-else>
|
|
||||||
<branches>
|
|
||||||
<sticky
|
|
||||||
v-if="subtabs"
|
|
||||||
class="subtabs-container"
|
|
||||||
:class="{
|
|
||||||
floating,
|
|
||||||
firstTab: firstTab || !allowGoBack,
|
|
||||||
minimizable
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="subtabs">
|
|
||||||
<tab-button
|
|
||||||
v-for="(subtab, id) in subtabs"
|
|
||||||
@selectTab="selectSubtab(id)"
|
|
||||||
:key="id"
|
|
||||||
:activeTab="subtab.active"
|
|
||||||
:options="subtab"
|
|
||||||
:text="id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</sticky>
|
|
||||||
<component v-if="display" :is="display" />
|
|
||||||
<default-layer-tab v-else />
|
|
||||||
</branches>
|
|
||||||
</div>
|
|
||||||
<button v-if="minimizable" class="minimize" @click="toggleMinimized">
|
|
||||||
▼
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</LayerProvider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import modInfo from "@/data/modInfo.json";
|
|
||||||
import themes from "@/data/themes";
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import settings from "@/game/settings";
|
|
||||||
import { Subtab } from "@/typings/features/subtab";
|
|
||||||
import { coerceComponent } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "layer-tab",
|
|
||||||
props: {
|
|
||||||
layer: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
index: Number,
|
|
||||||
forceFirstTab: Boolean,
|
|
||||||
minimizable: Boolean,
|
|
||||||
tab: Function
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return { allowGoBack: modInfo.allowGoBack };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
minimized(): boolean {
|
|
||||||
return this.minimizable && player.minimized[this.layer];
|
|
||||||
},
|
|
||||||
name(): string {
|
|
||||||
return layers[this.layer].name || this.layer;
|
|
||||||
},
|
|
||||||
floating(): boolean {
|
|
||||||
return themes[settings.theme].floatingTabs;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
const style = [];
|
|
||||||
if (layers[this.layer].style) {
|
|
||||||
style.push(layers[this.layer].style);
|
|
||||||
}
|
|
||||||
if (layers[this.layer].activeSubtab?.style) {
|
|
||||||
style.push(layers[this.layer].activeSubtab!.style);
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
},
|
|
||||||
display(): Component | string | null {
|
|
||||||
if (layers[this.layer].activeSubtab?.display) {
|
|
||||||
return coerceComponent(layers[this.layer].activeSubtab!.display!);
|
|
||||||
}
|
|
||||||
if (layers[this.layer].display) {
|
|
||||||
return coerceComponent(layers[this.layer].display!);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
subtabs(): Record<string, Subtab> | null {
|
|
||||||
if (layers[this.layer].subtabs) {
|
|
||||||
const subtabs = Object.entries(layers[this.layer].subtabs!)
|
|
||||||
.filter(subtab => subtab[1].unlocked !== false)
|
|
||||||
.reduce((acc: Record<string, Subtab>, curr: [string, Subtab]) => {
|
|
||||||
acc[curr[0]] = curr[1];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
if (Object.keys(subtabs).length === 1 && !themes[settings.theme].showSingleTab) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return subtabs;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
firstTab(): boolean {
|
|
||||||
if (this.forceFirstTab != undefined) {
|
|
||||||
return this.forceFirstTab;
|
|
||||||
}
|
|
||||||
return this.index === 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
minimized(newValue) {
|
|
||||||
if (this.tab == undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tab = this.tab();
|
|
||||||
if (tab != undefined) {
|
|
||||||
if (newValue) {
|
|
||||||
tab.style.flexGrow = 0;
|
|
||||||
tab.style.flexShrink = 0;
|
|
||||||
tab.style.width = "60px";
|
|
||||||
tab.style.minWidth = tab.style.flexBasis = null;
|
|
||||||
tab.style.margin = 0;
|
|
||||||
} else {
|
|
||||||
tab.style.flexGrow = null;
|
|
||||||
tab.style.flexShrink = null;
|
|
||||||
tab.style.width = null;
|
|
||||||
tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
|
|
||||||
tab.style.margin = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setup();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setup() {
|
|
||||||
if (this.tab == undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tab = this.tab();
|
|
||||||
if (tab != undefined) {
|
|
||||||
if (this.minimized) {
|
|
||||||
tab.style.flexGrow = 0;
|
|
||||||
tab.style.flexShrink = 0;
|
|
||||||
tab.style.width = "60px";
|
|
||||||
tab.style.minWidth = tab.style.flexBasis = null;
|
|
||||||
tab.style.margin = 0;
|
|
||||||
} else {
|
|
||||||
tab.style.flexGrow = null;
|
|
||||||
tab.style.flexShrink = null;
|
|
||||||
tab.style.width = null;
|
|
||||||
tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
|
|
||||||
tab.style.margin = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$nextTick(this.setup);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectSubtab(subtab: string) {
|
|
||||||
player.subtabs[this.layer].mainTabs = subtab;
|
|
||||||
},
|
|
||||||
toggleMinimized() {
|
|
||||||
player.minimized[this.layer] = !player.minimized[this.layer];
|
|
||||||
},
|
|
||||||
goBack(index: number) {
|
|
||||||
player.tabs = player.tabs.slice(0, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.layer-container {
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-tab:not(.minimized) {
|
|
||||||
padding-top: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
min-height: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
|
|
||||||
padding-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-tab.minimized {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
padding: 0;
|
|
||||||
padding-top: 55px;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 40px;
|
|
||||||
color: var(--foreground);
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-tab.minimized div {
|
|
||||||
margin: 0;
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
padding-left: 10px;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
|
|
||||||
margin: -50px -10px;
|
|
||||||
padding: 50px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-tab .subtabs {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: wrap;
|
|
||||||
padding-right: 60px;
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating) {
|
|
||||||
border-top: solid 4px var(--outline);
|
|
||||||
border-bottom: solid 4px var(--outline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating) .subtabs {
|
|
||||||
width: calc(100% + 14px);
|
|
||||||
margin-left: -7px;
|
|
||||||
margin-right: -7px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: left;
|
|
||||||
padding-left: 14px;
|
|
||||||
margin-bottom: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container.floating .subtabs {
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: -25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body .layer-tab {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body .layer-tab:not(.hasSubtabs) {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body .subtabs {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating).firstTab .subtabs {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating):first-child {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container.minimizable:not(.floating):first-child {
|
|
||||||
padding-right: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating):first-child .subtabs {
|
|
||||||
margin-top: -50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtabs-container:not(.floating):not(.firstTab) .subtabs {
|
|
||||||
padding-left: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimize {
|
|
||||||
position: sticky;
|
|
||||||
top: 6px;
|
|
||||||
right: 9px;
|
|
||||||
z-index: 7;
|
|
||||||
line-height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
border: none;
|
|
||||||
background: var(--background);
|
|
||||||
box-shadow: var(--background) 0 2px 3px 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
margin-top: -44px;
|
|
||||||
margin-right: -30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.minimized + .minimize {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
top: 10px;
|
|
||||||
right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goBack {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 20px;
|
|
||||||
background-color: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
line-height: 40px;
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goBack:hover {
|
|
||||||
transform: scale(1.1, 1.1);
|
|
||||||
text-shadow: 0 0 7px var(--foreground);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.subtabs-container + * {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
42
src/components/system/Link.vue
Normal file
42
src/components/system/Link.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<line
|
||||||
|
v-bind="link"
|
||||||
|
:x1="startPosition.x"
|
||||||
|
:y1="startPosition.y"
|
||||||
|
:x2="endPosition.x"
|
||||||
|
:y2="endPosition.y"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Link, LinkNode } from "@/features/links";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
|
||||||
|
const props = toRefs(
|
||||||
|
defineProps<{
|
||||||
|
link: Link;
|
||||||
|
startNode: LinkNode;
|
||||||
|
endNode: LinkNode;
|
||||||
|
}>()
|
||||||
|
);
|
||||||
|
|
||||||
|
const startPosition = computed(() => {
|
||||||
|
const position = { x: props.startNode.value.x || 0, y: props.startNode.value.y || 0 };
|
||||||
|
if (props.link.value.offsetStart) {
|
||||||
|
position.x += unref(props.link.value.offsetStart).x;
|
||||||
|
position.y += unref(props.link.value.offsetStart).y;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
});
|
||||||
|
|
||||||
|
const endPosition = computed(() => {
|
||||||
|
const position = { x: props.endNode.value.x || 0, y: props.endNode.value.y || 0 };
|
||||||
|
if (props.link.value.offsetEnd) {
|
||||||
|
position.x += unref(props.link.value.offsetEnd).x;
|
||||||
|
position.y += unref(props.link.value.offsetEnd).y;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
39
src/components/system/LinkNode.vue
Normal file
39
src/components/system/LinkNode.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="branch" ref="node"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RegisterLinkNodeInjectionKey, UnregisterLinkNodeInjectionKey } from "@/features/links";
|
||||||
|
import { computed, inject, ref, toRefs, unref, watch } from "vue";
|
||||||
|
|
||||||
|
const props = toRefs(defineProps<{ id: string }>());
|
||||||
|
|
||||||
|
const register = inject(RegisterLinkNodeInjectionKey);
|
||||||
|
const unregister = inject(UnregisterLinkNodeInjectionKey);
|
||||||
|
|
||||||
|
const node = ref<HTMLElement | null>(null);
|
||||||
|
const parentNode = computed(() => node.value && node.value.parentElement);
|
||||||
|
|
||||||
|
if (register && unregister) {
|
||||||
|
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
|
||||||
|
if (prevNode) {
|
||||||
|
unregister(unref(prevID));
|
||||||
|
}
|
||||||
|
if (newNode) {
|
||||||
|
register(newID, newNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.branch {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
111
src/components/system/Links.vue
Normal file
111
src/components/system/Links.vue
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<slot />
|
||||||
|
<div ref="resizeListener" class="resize-listener" />
|
||||||
|
<svg v-bind="$attrs" v-if="validLinks">
|
||||||
|
<LinkVue
|
||||||
|
v-for="(link, index) in validLinks"
|
||||||
|
:key="index"
|
||||||
|
:link="link"
|
||||||
|
:startNode="nodes[link.startNode.id]"
|
||||||
|
:endNode="nodes[link.endNode.id]"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
LinkNode,
|
||||||
|
RegisterLinkNodeInjectionKey,
|
||||||
|
UnregisterLinkNodeInjectionKey
|
||||||
|
} from "@/features/links";
|
||||||
|
import { computed, nextTick, onMounted, provide, ref, toRefs, unref } from "vue";
|
||||||
|
import LinkVue from "./Link.vue";
|
||||||
|
|
||||||
|
const props = toRefs(defineProps<{ links: Link[] }>());
|
||||||
|
|
||||||
|
const validLinks = computed(() =>
|
||||||
|
unref(props.links.value).filter(link => {
|
||||||
|
const n = nodes.value;
|
||||||
|
return (
|
||||||
|
link.startNode.id in n &&
|
||||||
|
link.endNode.id in n &&
|
||||||
|
n[link.startNode.id].x != undefined &&
|
||||||
|
n[link.startNode.id].y != undefined &&
|
||||||
|
n[link.endNode.id].x != undefined &&
|
||||||
|
n[link.endNode.id].y != undefined
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const observerOptions = {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
};
|
||||||
|
|
||||||
|
provide(RegisterLinkNodeInjectionKey, (id, element) => {
|
||||||
|
nodes.value[id] = { element };
|
||||||
|
observer.observe(element, observerOptions);
|
||||||
|
nextTick(() => {
|
||||||
|
if (resizeListener.value != null) {
|
||||||
|
updateNode(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
provide(UnregisterLinkNodeInjectionKey, id => {
|
||||||
|
delete nodes.value[id];
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateNodes() {
|
||||||
|
if (resizeListener.value != null) {
|
||||||
|
Object.keys(nodes.value).forEach(id => updateNode(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNode(id: string) {
|
||||||
|
if (!(id in nodes.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const linkStartRect = nodes.value[id].element.getBoundingClientRect();
|
||||||
|
nodes.value[id].x = linkStartRect.x + linkStartRect.width / 2 - boundingRect.value.x;
|
||||||
|
nodes.value[id].y = linkStartRect.y + linkStartRect.height / 2 - boundingRect.value.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBounds() {
|
||||||
|
if (resizeListener.value != null) {
|
||||||
|
boundingRect.value = resizeListener.value.getBoundingClientRect();
|
||||||
|
updateNodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(updateNodes);
|
||||||
|
const resizeObserver = new ResizeObserver(updateBounds);
|
||||||
|
|
||||||
|
const nodes = ref<Record<string, LinkNode>>({});
|
||||||
|
const boundingRect = ref(new DOMRect());
|
||||||
|
|
||||||
|
const resizeListener = ref<Element | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||||
|
const resListener = resizeListener.value;
|
||||||
|
if (resListener != null) {
|
||||||
|
resizeObserver.observe(resListener);
|
||||||
|
}
|
||||||
|
updateNodes();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg,
|
||||||
|
.resize-listener {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,107 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="microtabs" class="microtabs">
|
|
||||||
<LayerProvider :layer="layer" :index="tab.index">
|
|
||||||
<div v-if="microtabs && this.id == undefined" class="tabs" :class="{ floating }">
|
|
||||||
<tab-button
|
|
||||||
v-for="(microtab, id) in microtabs"
|
|
||||||
@selectTab="selectMicrotab(id)"
|
|
||||||
:key="id"
|
|
||||||
:activeTab="id === activeMicrotab?.id"
|
|
||||||
:options="microtab"
|
|
||||||
:text="id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<template v-if="activeMicrotab">
|
|
||||||
<layer-tab v-if="embed" :layer="embed" />
|
|
||||||
<component v-else :is="display" />
|
|
||||||
</template>
|
|
||||||
</LayerProvider>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import themes from "@/data/themes";
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import settings from "@/game/settings";
|
|
||||||
import { Microtab } from "@/typings/features/subtab";
|
|
||||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "microtab",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
family: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
id: String
|
|
||||||
},
|
|
||||||
inject: ["tab"],
|
|
||||||
computed: {
|
|
||||||
floating() {
|
|
||||||
return themes[settings.theme].floatingTabs;
|
|
||||||
},
|
|
||||||
tabFamily() {
|
|
||||||
return layers[this.layer].microtabs![this.family];
|
|
||||||
},
|
|
||||||
microtabs() {
|
|
||||||
const microtabs = Object.keys(this.tabFamily.data)
|
|
||||||
.filter(
|
|
||||||
microtab =>
|
|
||||||
microtab !== "activeMicrotab" &&
|
|
||||||
this.tabFamily.data[microtab].isProxy &&
|
|
||||||
this.tabFamily.data[microtab].unlocked !== false
|
|
||||||
)
|
|
||||||
.reduce((acc: Record<string, Microtab>, curr) => {
|
|
||||||
acc[curr] = this.tabFamily.data[curr];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
if (Object.keys(microtabs).length === 1 && !themes[settings.theme].showSingleTab) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return microtabs;
|
|
||||||
},
|
|
||||||
activeMicrotab() {
|
|
||||||
return this.id != undefined
|
|
||||||
? this.tabFamily.data[this.id]
|
|
||||||
: this.tabFamily.activeMicrotab!;
|
|
||||||
},
|
|
||||||
embed() {
|
|
||||||
return this.activeMicrotab!.embedLayer;
|
|
||||||
},
|
|
||||||
display() {
|
|
||||||
return this.activeMicrotab!.display && coerceComponent(this.activeMicrotab!.display);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectMicrotab(tab: string) {
|
|
||||||
player.subtabs[this.layer][this.family] = tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.microtabs {
|
|
||||||
margin: var(--feature-margin) -11px;
|
|
||||||
position: relative;
|
|
||||||
border: solid 4px var(--outline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs:not(.floating) {
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: inherit;
|
|
||||||
border-width: 4px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.microtabs .sticky {
|
|
||||||
margin-left: unset !important;
|
|
||||||
margin-right: unset !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -7,32 +7,29 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-mask"
|
class="modal-mask"
|
||||||
v-show="show"
|
v-show="modelValue"
|
||||||
v-on:pointerdown.self="$emit('close')"
|
v-on:pointerdown.self="close"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<div class="modal-wrapper">
|
<div class="modal-wrapper">
|
||||||
<div class="modal-container">
|
<div class="modal-container">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<slot name="header" :shown="isVisible">
|
<slot name="header" :shown="isOpen">
|
||||||
default header
|
default header
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<branches>
|
<branches>
|
||||||
<slot name="body" :shown="isVisible">
|
<slot name="body" :shown="isOpen">
|
||||||
default body
|
default body
|
||||||
</slot>
|
</slot>
|
||||||
</branches>
|
</branches>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<slot name="footer" :shown="isVisible">
|
<slot name="footer" :shown="isOpen">
|
||||||
<div class="modal-default-footer">
|
<div class="modal-default-footer">
|
||||||
<div class="modal-default-flex-grow"></div>
|
<div class="modal-default-flex-grow"></div>
|
||||||
<button
|
<button class="button modal-default-button" @click="close">
|
||||||
class="button modal-default-button"
|
|
||||||
@click="$emit('close')"
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,31 +42,25 @@
|
||||||
</teleport>
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
name: "Modal",
|
modelValue: boolean;
|
||||||
data() {
|
}>();
|
||||||
return {
|
const emit = defineEmits<{
|
||||||
isAnimating: false
|
(e: "update:modelValue", value: boolean): void;
|
||||||
};
|
}>();
|
||||||
},
|
|
||||||
props: {
|
const isOpen = computed(() => props.modelValue || isAnimating.value);
|
||||||
show: Boolean
|
function close() {
|
||||||
},
|
emit("update:modelValue", false);
|
||||||
emits: ["close"],
|
}
|
||||||
computed: {
|
|
||||||
isVisible(): boolean {
|
const isAnimating = ref(false);
|
||||||
return this.show || this.isAnimating;
|
function setAnimating(value: boolean) {
|
||||||
}
|
isAnimating.value = value;
|
||||||
},
|
}
|
||||||
methods: {
|
|
||||||
setAnimating(isAnimating: boolean) {
|
|
||||||
this.isAnimating = isAnimating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="hasNaN" v-bind="$attrs">
|
<Modal v-model="hasNaN" v-bind="$attrs">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="nan-modal-header">
|
<div class="nan-modal-header">
|
||||||
<h2>NaN value detected!</h2>
|
<h2>NaN value detected!</h2>
|
||||||
|
@ -7,9 +7,10 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div>
|
<div>
|
||||||
Attempted to assign "{{ path }}" to NaN (previously {{ format(previous) }}).
|
Attempted to assign "{{ path }}" to NaN<span v-if="previous">
|
||||||
Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console for
|
{{ " " }}(previously {{ format(previous) }})</span
|
||||||
more details, and consider sharing it with the developers on discord.
|
>. Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console
|
||||||
|
for more details, and consider sharing it with the developers on discord.
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
|
@ -19,20 +20,20 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<Toggle title="Autosave" :value="autosave" @change="setAutosave" />
|
<Toggle title="Autosave" v-model="autosave" />
|
||||||
<Toggle title="Pause game" :value="paused" @change="togglePaused" />
|
<Toggle title="Pause game" v-model="isPaused" />
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:footer>
|
<template v-slot:footer>
|
||||||
<div class="nan-footer">
|
<div class="nan-footer">
|
||||||
<button @click="toggleSavesManager" class="button">
|
<button @click="savesManager?.open()" class="button">
|
||||||
Open Saves Manager
|
Open Saves Manager
|
||||||
</button>
|
</button>
|
||||||
<button @click="setZero" class="button">Set to 0</button>
|
<button @click="setZero" class="button">Set to 0</button>
|
||||||
<button @click="setOne" class="button">Set to 1</button>
|
<button @click="setOne" class="button">Set to 1</button>
|
||||||
<button
|
<button
|
||||||
@click="setPrev"
|
@click="hasNaN = false"
|
||||||
class="button"
|
class="button"
|
||||||
v-if="previous && previous.neq(0) && previous.neq(1)"
|
v-if="previous && Decimal.neq(previous, 0) && Decimal.neq(previous, 1)"
|
||||||
>
|
>
|
||||||
Set to previous
|
Set to previous
|
||||||
</button>
|
</button>
|
||||||
|
@ -40,75 +41,61 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
<SavesManager :show="showSaves" @closeDialog="toggleSavesManager" />
|
<SavesManager ref="savesManager" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Modal from "@/components/system/Modal.vue";
|
||||||
import modInfo from "@/data/modInfo.json";
|
import modInfo from "@/data/modInfo.json";
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import state from "@/game/state";
|
import state from "@/game/state";
|
||||||
import Decimal, { format } from "@/util/bignum";
|
import Decimal, { DecimalSource, format } from "@/util/bignum";
|
||||||
import { mapPlayer, mapState } from "@/util/vue";
|
import { computed, ref, toRef } from "vue";
|
||||||
import { defineComponent } from "vue";
|
import Toggle from "../fields/Toggle.vue";
|
||||||
|
import SavesManager from "./SavesManager.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const { discordName, discordLink } = modInfo;
|
||||||
name: "NaNScreen",
|
const autosave = toRef(player, "autosave");
|
||||||
data() {
|
const hasNaN = toRef(state, "hasNaN");
|
||||||
const { discordName, discordLink } = modInfo;
|
const savesManager = ref<typeof SavesManager | null>(null);
|
||||||
return { discordName, discordLink, format, showSaves: false };
|
|
||||||
},
|
const path = computed(() => state.NaNPath?.join("."));
|
||||||
computed: {
|
const property = computed(() => state.NaNPath?.slice(-1)[0]);
|
||||||
...mapPlayer(["autosave"]),
|
const previous = computed<DecimalSource | null>(() => {
|
||||||
...mapState(["hasNaN"]),
|
if (state.NaNReceiver && property.value) {
|
||||||
path(): string | undefined {
|
return state.NaNReceiver[property.value] as DecimalSource;
|
||||||
return state.NaNPath?.join(".");
|
|
||||||
},
|
|
||||||
previous(): unknown {
|
|
||||||
if (state.NaNReceiver && this.property) {
|
|
||||||
return state.NaNReceiver[this.property];
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
});
|
||||||
paused() {
|
const isPaused = computed({
|
||||||
|
get() {
|
||||||
return player.devSpeed === 0;
|
return player.devSpeed === 0;
|
||||||
},
|
},
|
||||||
property(): string | undefined {
|
set(value: boolean) {
|
||||||
return state.NaNPath?.slice(-1)[0];
|
player.devSpeed = value ? null : 0;
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setZero() {
|
|
||||||
if (state.NaNReceiver && this.property) {
|
|
||||||
state.NaNReceiver[this.property] = new Decimal(0);
|
|
||||||
state.hasNaN = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setOne() {
|
|
||||||
if (state.NaNReceiver && this.property) {
|
|
||||||
state.NaNReceiver[this.property] = new Decimal(1);
|
|
||||||
state.hasNaN = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setPrev() {
|
|
||||||
state.hasNaN = false;
|
|
||||||
},
|
|
||||||
ignore() {
|
|
||||||
if (state.NaNReceiver && this.property) {
|
|
||||||
state.NaNReceiver[this.property] = new Decimal(NaN);
|
|
||||||
state.hasNaN = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setAutosave(autosave: boolean) {
|
|
||||||
player.autosave = autosave;
|
|
||||||
},
|
|
||||||
toggleSavesManager() {
|
|
||||||
this.showSaves = !this.showSaves;
|
|
||||||
},
|
|
||||||
togglePaused() {
|
|
||||||
player.devSpeed = this.paused ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setZero() {
|
||||||
|
if (state.NaNReceiver && property.value) {
|
||||||
|
state.NaNReceiver[property.value] = new Decimal(0);
|
||||||
|
state.hasNaN = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOne() {
|
||||||
|
if (state.NaNReceiver && property.value) {
|
||||||
|
state.NaNReceiver[property.value] = new Decimal(1);
|
||||||
|
state.hasNaN = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ignore() {
|
||||||
|
if (state.NaNReceiver && property.value) {
|
||||||
|
state.NaNReceiver[property.value] = new Decimal(NaN);
|
||||||
|
state.hasNaN = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
<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="changelog?.open()" class="version-container">
|
||||||
<tooltip display="<span>Changelog</span>" bottom class="version"
|
<Tooltip display="<span>Changelog</span>" bottom class="version"
|
||||||
><span>v{{ version }}</span></tooltip
|
><span>v{{ versionNumber }}</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">
|
||||||
<span @click="openDialog('Info')" class="material-icons">discord</span>
|
<span @click="openDiscord" class="material-icons">discord</span>
|
||||||
<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>
|
||||||
|
@ -29,57 +29,57 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://forums.moddingtree.com/" target="_blank">
|
<a href="https://forums.moddingtree.com/" target="_blank">
|
||||||
<tooltip display="Forums" bottom yoffset="5px">
|
<Tooltip display="Forums" bottom yoffset="5px">
|
||||||
<span class="material-icons">forum</span>
|
<span class="material-icons">forum</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Info')">
|
<div @click="info?.open()">
|
||||||
<tooltip display="Info" bottom class="info">
|
<Tooltip display="Info" bottom class="info">
|
||||||
<span class="material-icons">info</span>
|
<span class="material-icons">info</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Saves')">
|
<div @click="savesManager?.open()">
|
||||||
<tooltip display="Saves" bottom xoffset="-20px">
|
<Tooltip display="Saves" bottom xoffset="-20px">
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons">library_books</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Options')">
|
<div @click="options?.open()">
|
||||||
<tooltip display="Options" bottom xoffset="-66px">
|
<Tooltip display="Options" bottom xoffset="-66px">
|
||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="overlay-nav" v-bind="$attrs">
|
<div v-else class="overlay-nav" v-bind="$attrs">
|
||||||
<div @click="openDialog('Changelog')" class="version-container">
|
<div @click="changelog?.open()" class="version-container">
|
||||||
<tooltip display="Changelog" right xoffset="25%" class="version">
|
<Tooltip display="Changelog" right xoffset="25%" class="version">
|
||||||
<span>v{{ version }}</span>
|
<span>v{{ versionNumber }}</span>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Saves')">
|
<div @click="savesManager?.open()">
|
||||||
<tooltip display="Saves" right>
|
<Tooltip display="Saves" right>
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons">library_books</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Options')">
|
<div @click="options?.open()">
|
||||||
<tooltip display="Options" right>
|
<Tooltip display="Options" right>
|
||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="openDialog('Info')">
|
<div @click="info?.open()">
|
||||||
<tooltip display="Info" right>
|
<Tooltip display="Info" right>
|
||||||
<span class="material-icons">info</span>
|
<span class="material-icons">info</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://forums.moddingtree.com/" target="_blank">
|
<a href="https://forums.moddingtree.com/" target="_blank">
|
||||||
<tooltip display="Forums" right xoffset="7px">
|
<Tooltip display="Forums" right xoffset="7px">
|
||||||
<span class="material-icons">forum</span>
|
<span class="material-icons">forum</span>
|
||||||
</tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="discord">
|
<div class="discord">
|
||||||
<span @click="openDialog('Info')" class="material-icons">discord</span>
|
<span @click="openDiscord" class="material-icons">discord</span>
|
||||||
<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>
|
||||||
|
@ -98,47 +98,33 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
|
<Info ref="info" :changelog="changelog" />
|
||||||
<SavesManager :show="showSaves" @closeDialog="closeDialog" />
|
<SavesManager ref="savesManager" />
|
||||||
<Options :show="showOptions" @closeDialog="closeDialog" />
|
<Options ref="options" />
|
||||||
<Changelog :show="showChangelog" @closeDialog="closeDialog" />
|
<Changelog ref="changelog" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Changelog from "@/data/Changelog.vue";
|
||||||
import modInfo from "@/data/modInfo.json";
|
import modInfo from "@/data/modInfo.json";
|
||||||
import { defineComponent } from "vue";
|
import { ComponentPublicInstance, ref } from "vue";
|
||||||
|
import Info from "./Info.vue";
|
||||||
|
import Options from "./Options.vue";
|
||||||
|
import SavesManager from "./SavesManager.vue";
|
||||||
|
import Tooltip from "./Tooltip.vue";
|
||||||
|
|
||||||
type modals = "Info" | "Saves" | "Options" | "Changelog";
|
const info = ref<typeof Info | null>(null);
|
||||||
type showModals = "showInfo" | "showSaves" | "showOptions" | "showChangelog";
|
const savesManager = ref<typeof SavesManager | null>(null);
|
||||||
|
const options = ref<typeof Options | null>(null);
|
||||||
|
// For some reason Info won't accept the changelog unless I do this:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const changelog = ref<ComponentPublicInstance<any> | null>(null);
|
||||||
|
|
||||||
export default defineComponent({
|
const { useHeader, banner, title, discordName, discordLink, versionNumber } = modInfo;
|
||||||
name: "Nav",
|
|
||||||
data() {
|
function openDiscord() {
|
||||||
return {
|
window.open(discordLink, "mywindow");
|
||||||
useHeader: modInfo.useHeader,
|
}
|
||||||
banner: modInfo.banner,
|
|
||||||
title: modInfo.title,
|
|
||||||
discordName: modInfo.discordName,
|
|
||||||
discordLink: modInfo.discordLink,
|
|
||||||
version: modInfo.versionNumber,
|
|
||||||
showInfo: false,
|
|
||||||
showSaves: false,
|
|
||||||
showOptions: false,
|
|
||||||
showChangelog: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openDiscord() {
|
|
||||||
window.open(this.discordLink, "mywindow");
|
|
||||||
},
|
|
||||||
openDialog(dialog: modals) {
|
|
||||||
this[`show${dialog}` as showModals] = true;
|
|
||||||
},
|
|
||||||
closeDialog(dialog: modals) {
|
|
||||||
this[`show${dialog}` as showModals] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,107 +1,70 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="show" @close="$emit('closeDialog', 'Options')">
|
<Modal v-model="isOpen">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Options</h2>
|
<h2>Options</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<Select
|
<Select title="Theme" :options="themes" v-model="theme" />
|
||||||
title="Theme"
|
<Select title="Show Milestones" :options="msDisplayOptions" v-model="msDisplay" />
|
||||||
:options="themes"
|
<Toggle title="Show TPS" v-model="showTPS" />
|
||||||
:value="theme"
|
<Toggle title="Hide Maxed Challenges" v-model="hideChallenges" />
|
||||||
@change="setTheme"
|
<Toggle title="Unthrottled" v-model="unthrottled" />
|
||||||
default="classic"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
title="Show Milestones"
|
|
||||||
:options="msDisplayOptions"
|
|
||||||
:value="msDisplay"
|
|
||||||
@change="setMSDisplay"
|
|
||||||
default="all"
|
|
||||||
/>
|
|
||||||
<Toggle title="Show TPS" :value="showTPS" @change="toggleSettingsOption('showTPS')" />
|
|
||||||
<Toggle
|
|
||||||
title="Hide Maxed Challenges"
|
|
||||||
:value="hideChallenges"
|
|
||||||
@change="toggleSettingsOption('hideChallenges')"
|
|
||||||
/>
|
|
||||||
<Toggle
|
|
||||||
title="Unthrottled"
|
|
||||||
:value="unthrottled"
|
|
||||||
@change="toggleSettingsOption('unthrottled')"
|
|
||||||
/>
|
|
||||||
<Toggle
|
<Toggle
|
||||||
title="Offline Production<tooltip display='Save-specific'>*</tooltip>"
|
title="Offline Production<tooltip display='Save-specific'>*</tooltip>"
|
||||||
:value="offlineProd"
|
v-model="offlineProd"
|
||||||
@change="togglePlayerOption('offlineProd')"
|
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
title="Autosave<tooltip display='Save-specific'>*</tooltip>"
|
title="Autosave<tooltip display='Save-specific'>*</tooltip>"
|
||||||
:value="autosave"
|
v-model="autosave"
|
||||||
@change="togglePlayerOption('autosave')"
|
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
title="Pause game<tooltip display='Save-specific'>*</tooltip>"
|
title="Pause game<tooltip display='Save-specific'>*</tooltip>"
|
||||||
:value="paused"
|
v-model="isPaused"
|
||||||
@change="togglePaused"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import Modal from "@/components/system/Modal.vue";
|
||||||
import themes, { Themes } from "@/data/themes";
|
import rawThemes from "@/data/themes";
|
||||||
import { camelToTitle } from "@/util/common";
|
import { MilestoneDisplay } from "@/features/milestone";
|
||||||
import { mapPlayer, mapSettings } from "@/util/vue";
|
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { MilestoneDisplay } from "@/game/enums";
|
|
||||||
import { PlayerData } from "@/typings/player";
|
|
||||||
import settings from "@/game/settings";
|
import settings from "@/game/settings";
|
||||||
import { Settings } from "@/typings/settings";
|
import { camelToTitle } from "@/util/common";
|
||||||
|
import { computed, ref, toRefs } from "vue";
|
||||||
|
import Toggle from "../fields/Toggle.vue";
|
||||||
|
import Select from "../fields/Select.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const isOpen = ref(false);
|
||||||
name: "Options",
|
|
||||||
props: {
|
defineExpose({
|
||||||
show: Boolean
|
open() {
|
||||||
},
|
isOpen.value = true;
|
||||||
emits: ["closeDialog"],
|
}
|
||||||
data() {
|
});
|
||||||
return {
|
|
||||||
themes: Object.keys(themes).map(theme => ({
|
const themes = Object.keys(rawThemes).map(theme => ({
|
||||||
label: camelToTitle(theme),
|
label: camelToTitle(theme),
|
||||||
value: theme
|
value: theme
|
||||||
})),
|
}));
|
||||||
msDisplayOptions: Object.values(MilestoneDisplay).map(option => ({
|
|
||||||
|
// TODO allow features to register options
|
||||||
|
const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
|
||||||
label: camelToTitle(option),
|
label: camelToTitle(option),
|
||||||
value: option
|
value: option
|
||||||
}))
|
}));
|
||||||
};
|
|
||||||
|
const { showTPS, hideChallenges, theme, msDisplay, unthrottled } = toRefs(settings);
|
||||||
|
const { autosave, offlineProd, devSpeed } = toRefs(player);
|
||||||
|
const isPaused = computed({
|
||||||
|
get() {
|
||||||
|
return devSpeed.value === 0;
|
||||||
},
|
},
|
||||||
computed: {
|
set(value: boolean) {
|
||||||
...mapSettings(["showTPS", "hideChallenges", "theme", "msDisplay", "unthrottled"]),
|
devSpeed.value = value ? null : 0;
|
||||||
...mapPlayer(["autosave", "offlineProd"]),
|
|
||||||
paused() {
|
|
||||||
return player.devSpeed === 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
togglePlayerOption(option: keyof PlayerData) {
|
|
||||||
player[option] = !player[option];
|
|
||||||
},
|
|
||||||
toggleSettingsOption(option: keyof Settings) {
|
|
||||||
settings[option] = !settings[option];
|
|
||||||
},
|
|
||||||
setTheme(theme: Themes) {
|
|
||||||
settings.theme = theme;
|
|
||||||
},
|
|
||||||
setMSDisplay(msDisplay: MilestoneDisplay) {
|
|
||||||
settings.msDisplay = msDisplay;
|
|
||||||
},
|
|
||||||
togglePaused() {
|
|
||||||
player.devSpeed = this.paused ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { displayResource, Resource } from "@/features/resource";
|
||||||
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "resource",
|
defineProps<{
|
||||||
props: {
|
resource: Resource;
|
||||||
color: String,
|
color: string;
|
||||||
amount: String
|
}>()
|
||||||
}
|
);
|
||||||
});
|
|
||||||
|
const amount = computed(() => displayResource(props.resource));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
@ -6,10 +6,4 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts"></script>
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "row"
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,111 +1,103 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="save" :class="{ active }">
|
<div class="save" :class="{ active: isActive }">
|
||||||
<div class="handle material-icons">drag_handle</div>
|
<div class="handle material-icons">drag_handle</div>
|
||||||
<div class="actions" v-if="!editing">
|
<div class="actions" v-if="!isEditing">
|
||||||
<feedback-button
|
<FeedbackButton
|
||||||
@click="$emit('export')"
|
@click="emit('export')"
|
||||||
class="button"
|
class="button"
|
||||||
left
|
left
|
||||||
v-if="save.error == undefined && !confirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
<span class="material-icons">content_paste</span>
|
<span class="material-icons">content_paste</span>
|
||||||
</feedback-button>
|
</FeedbackButton>
|
||||||
<button
|
<button
|
||||||
@click="$emit('duplicate')"
|
@click="emit('duplicate')"
|
||||||
class="button"
|
class="button"
|
||||||
v-if="save.error == undefined && !confirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
<span class="material-icons">content_copy</span>
|
<span class="material-icons">content_copy</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="toggleEditing"
|
@click="isEditing = !isEditing"
|
||||||
class="button"
|
class="button"
|
||||||
v-if="save.error == undefined && !confirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
<span class="material-icons">edit</span>
|
<span class="material-icons">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<danger-button
|
<DangerButton
|
||||||
:disabled="active"
|
:disabled="isActive"
|
||||||
@click="$emit('delete')"
|
@click="emit('delete')"
|
||||||
@confirmingChanged="confirmingChanged"
|
@confirmingChanged="value => (isConfirming = value)"
|
||||||
>
|
>
|
||||||
<span class="material-icons" style="margin: -2px">delete</span>
|
<span class="material-icons" style="margin: -2px">delete</span>
|
||||||
</danger-button>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-else>
|
<div class="actions" v-else>
|
||||||
<button @click="changeName" class="button">
|
<button @click="changeName" class="button">
|
||||||
<span class="material-icons">check</span>
|
<span class="material-icons">check</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleEditing" class="button">
|
<button @click="isEditing = !isEditing" class="button">
|
||||||
<span class="material-icons">close</span>
|
<span class="material-icons">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-if="save.error == undefined && !editing">
|
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||||
<button class="button open" @click="$emit('open')">
|
<button class="button open" @click="emit('open')">
|
||||||
<h3>{{ save.name }}</h3>
|
<h3>{{ save.name }}</h3>
|
||||||
</button>
|
</button>
|
||||||
<span class="save-version">v{{ save.modVersion }}</span
|
<span class="save-version">v{{ save.modVersion }}</span
|
||||||
><br />
|
><br />
|
||||||
<div v-if="time">Last played {{ dateFormat.format(time) }}</div>
|
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-else-if="save.error == undefined && editing">
|
<div class="details" v-else-if="save.error == undefined && isEditing">
|
||||||
<TextField v-model="newName" class="editname" @submit="changeName" @blur="changeName" />
|
<Text v-model="newName" class="editname" @submit="changeName" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
|
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import player from "@/game/player";
|
import player from "@/game/player";
|
||||||
import { PlayerData } from "@/typings/player";
|
import { computed, ref, toRefs, unref, watch } from "vue";
|
||||||
import { defineComponent, PropType } from "vue";
|
import DangerButton from "../fields/DangerButton.vue";
|
||||||
|
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||||
|
import Text from "../fields/Text.vue";
|
||||||
|
import { LoadablePlayerData } from "./SavesManager.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(
|
||||||
name: "save",
|
defineProps<{
|
||||||
props: {
|
save: LoadablePlayerData;
|
||||||
save: {
|
}>()
|
||||||
type: Object as PropType<Partial<PlayerData>>,
|
);
|
||||||
required: true
|
const emit = defineEmits<{
|
||||||
}
|
(e: "export"): void;
|
||||||
},
|
(e: "open"): void;
|
||||||
emits: ["export", "open", "duplicate", "delete", "editSave"],
|
(e: "duplicate"): void;
|
||||||
data() {
|
(e: "delete"): void;
|
||||||
return {
|
(e: "editName", name: string): void;
|
||||||
dateFormat: new Intl.DateTimeFormat("en-US", {
|
}>();
|
||||||
|
|
||||||
|
const dateFormat = new Intl.DateTimeFormat("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "numeric",
|
month: "numeric",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
second: "numeric"
|
second: "numeric"
|
||||||
}),
|
|
||||||
editing: false,
|
|
||||||
confirming: false,
|
|
||||||
newName: ""
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
active(): boolean {
|
|
||||||
return this.save.id === player.id;
|
|
||||||
},
|
|
||||||
time(): number | undefined {
|
|
||||||
return this.active ? player.time : this.save.time;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirmingChanged(confirming: boolean) {
|
|
||||||
this.confirming = confirming;
|
|
||||||
},
|
|
||||||
toggleEditing() {
|
|
||||||
this.newName = this.save.name || "";
|
|
||||||
this.editing = !this.editing;
|
|
||||||
},
|
|
||||||
changeName() {
|
|
||||||
this.$emit("editSave", this.newName);
|
|
||||||
this.editing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const isConfirming = ref(false);
|
||||||
|
const newName = ref("");
|
||||||
|
|
||||||
|
watch(isEditing, () => (newName.value = ""));
|
||||||
|
|
||||||
|
const isActive = computed(() => unref(props.save).id === player.id);
|
||||||
|
const currentTime = computed(() => (isActive.value ? player.time : unref(props.save).time));
|
||||||
|
|
||||||
|
function changeName() {
|
||||||
|
emit("editName", newName.value);
|
||||||
|
isEditing.value = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="show" @close="$emit('closeDialog', 'Saves')">
|
<Modal v-model="isOpen">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<h2>Saves Manager</h2>
|
<h2>Saves Manager</h2>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div v-sortable="{ update, handle: '.handle' }">
|
<div v-sortable="{ update, handle: '.handle' }">
|
||||||
<save
|
<Save
|
||||||
v-for="(save, index) in saves"
|
v-for="(save, index) in saves"
|
||||||
:key="index"
|
:key="index"
|
||||||
:save="save"
|
:save="save"
|
||||||
@open="openSave(save.id)"
|
@open="openSave(save.id)"
|
||||||
@export="exportSave(save.id)"
|
@export="exportSave(save.id)"
|
||||||
@editSave="name => editSave(save.id, name)"
|
@editName="name => editSave(save.id, name)"
|
||||||
@duplicate="duplicateSave(save.id)"
|
@duplicate="duplicateSave(save.id)"
|
||||||
@delete="deleteSave(save.id)"
|
@delete="deleteSave(save.id)"
|
||||||
/>
|
/>
|
||||||
|
@ -19,10 +19,8 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:footer>
|
<template v-slot:footer>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<TextField
|
<Text
|
||||||
:value="saveToImport"
|
v-model="saveToImport"
|
||||||
@submit="importSave"
|
|
||||||
@change="importSave"
|
|
||||||
title="Import Save"
|
title="Import Save"
|
||||||
placeholder="Paste your save here!"
|
placeholder="Paste your save here!"
|
||||||
:class="{ importingFailed }"
|
:class="{ importingFailed }"
|
||||||
|
@ -34,20 +32,17 @@
|
||||||
<Select
|
<Select
|
||||||
v-if="Object.keys(bank).length > 0"
|
v-if="Object.keys(bank).length > 0"
|
||||||
:options="bank"
|
:options="bank"
|
||||||
|
:modelValue="[]"
|
||||||
|
@update:modelValue="preset => newFromPreset(preset as string)"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
@change="newFromPreset"
|
|
||||||
placeholder="Select preset"
|
placeholder="Select preset"
|
||||||
class="presets"
|
class="presets"
|
||||||
:value="[]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div style="flex-grow: 1"></div>
|
<div style="flex-grow: 1"></div>
|
||||||
<button
|
<button class="button modal-default-button" @click="isOpen = false">
|
||||||
class="button modal-default-button"
|
|
||||||
@click="$emit('closeDialog', 'Saves')"
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,64 +51,81 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import player from "@/game/player";
|
import Modal from "@/components/system/Modal.vue";
|
||||||
|
import player, { PlayerData } from "@/game/player";
|
||||||
import settings from "@/game/settings";
|
import settings from "@/game/settings";
|
||||||
import state from "@/game/state";
|
import { getUniqueID, loadSave, save, newSave as createNewSave } from "@/util/save";
|
||||||
import { PlayerData } from "@/typings/player";
|
import { nextTick, ref, watch } from "vue";
|
||||||
import { getUniqueID, loadSave, newSave, save } from "@/util/save";
|
import Select from "../fields/Select.vue";
|
||||||
import { defineComponent } from "vue";
|
import Text from "../fields/Text.vue";
|
||||||
|
import Save from "./Save.vue";
|
||||||
|
import vSortable from "vue-sortable";
|
||||||
|
|
||||||
export default defineComponent({
|
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
|
||||||
name: "SavesManager",
|
|
||||||
props: {
|
const isOpen = ref(false);
|
||||||
show: Boolean
|
|
||||||
},
|
defineExpose({
|
||||||
emits: ["closeDialog"],
|
open() {
|
||||||
data() {
|
isOpen.value = true;
|
||||||
let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/);
|
}
|
||||||
let bank = bankContext
|
});
|
||||||
.keys()
|
|
||||||
.reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
const importingFailed = ref(false);
|
||||||
|
const saveToImport = ref("");
|
||||||
|
|
||||||
|
watch(isOpen, isOpen => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadSaveData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(saveToImport, save => {
|
||||||
|
if (save) {
|
||||||
|
nextTick(() => {
|
||||||
|
try {
|
||||||
|
const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
|
||||||
|
if (typeof playerData !== "object") {
|
||||||
|
importingFailed.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = getUniqueID();
|
||||||
|
playerData.id = id;
|
||||||
|
localStorage.setItem(
|
||||||
|
id,
|
||||||
|
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||||
|
);
|
||||||
|
saves.value[id] = playerData;
|
||||||
|
saveToImport.value = "";
|
||||||
|
importingFailed.value = false;
|
||||||
|
|
||||||
|
settings.saves.push(id);
|
||||||
|
} catch (e) {
|
||||||
|
importingFailed.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
importingFailed.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/);
|
||||||
|
let bank = ref(
|
||||||
|
bankContext.keys().reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
||||||
// .slice(2, -4) strips the leading ./ and the trailing .txt
|
// .slice(2, -4) strips the leading ./ and the trailing .txt
|
||||||
acc.push({
|
acc.push({
|
||||||
label: curr.slice(2, -4),
|
label: curr.slice(2, -4),
|
||||||
value: bankContext(curr).default
|
value: bankContext(curr).default
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, [])
|
||||||
return {
|
);
|
||||||
importingFailed: false,
|
|
||||||
saves: {}, // Gets populated when the modal is opened
|
const saves = ref<Record<string, LoadablePlayerData>>({});
|
||||||
saveToImport: "",
|
|
||||||
bank
|
function loadSaveData() {
|
||||||
} as {
|
saves.value = settings.saves.reduce((acc: Record<string, LoadablePlayerData>, curr: string) => {
|
||||||
importingFailed: boolean;
|
|
||||||
saves: Record<
|
|
||||||
string,
|
|
||||||
Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }
|
|
||||||
>;
|
|
||||||
saveToImport: string;
|
|
||||||
bank: Array<{ label: string; value: string }>;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
show(newValue) {
|
|
||||||
if (newValue) {
|
|
||||||
this.loadSaveData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadSaveData() {
|
|
||||||
this.saves = settings.saves.reduce(
|
|
||||||
(
|
|
||||||
acc: Record<
|
|
||||||
string,
|
|
||||||
Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }
|
|
||||||
>,
|
|
||||||
curr: string
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const save = localStorage.getItem(curr);
|
const save = localStorage.getItem(curr);
|
||||||
if (save == null) {
|
if (save == null) {
|
||||||
|
@ -127,17 +139,15 @@ export default defineComponent({
|
||||||
acc[curr] = { error, id: curr };
|
acc[curr] = { error, id: curr };
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, {});
|
||||||
{}
|
}
|
||||||
);
|
|
||||||
},
|
function exportSave(id: string) {
|
||||||
exportSave(id: string) {
|
|
||||||
let saveToExport;
|
let saveToExport;
|
||||||
if (player.id === id) {
|
if (player.id === id) {
|
||||||
save();
|
saveToExport = save();
|
||||||
saveToExport = state.saveToExport;
|
|
||||||
} else {
|
} else {
|
||||||
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))));
|
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
||||||
|
@ -148,36 +158,41 @@ export default defineComponent({
|
||||||
el.setSelectionRange(0, 99999);
|
el.setSelectionRange(0, 99999);
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(el);
|
document.body.removeChild(el);
|
||||||
},
|
}
|
||||||
duplicateSave(id: string) {
|
|
||||||
|
function duplicateSave(id: string) {
|
||||||
if (player.id === id) {
|
if (player.id === id) {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerData = { ...this.saves[id], id: getUniqueID() };
|
const playerData = { ...saves.value[id], id: getUniqueID() };
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
playerData.id,
|
playerData.id,
|
||||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||||
);
|
);
|
||||||
|
|
||||||
settings.saves.push(playerData.id);
|
settings.saves.push(playerData.id);
|
||||||
this.saves[playerData.id] = playerData;
|
saves.value[playerData.id] = playerData;
|
||||||
},
|
}
|
||||||
deleteSave(id: string) {
|
|
||||||
|
function deleteSave(id: string) {
|
||||||
settings.saves = settings.saves.filter((save: string) => save !== id);
|
settings.saves = settings.saves.filter((save: string) => save !== id);
|
||||||
localStorage.removeItem(id);
|
localStorage.removeItem(id);
|
||||||
delete this.saves[id];
|
delete saves.value[id];
|
||||||
},
|
}
|
||||||
openSave(id: string) {
|
|
||||||
this.saves[player.id].time = player.time;
|
function openSave(id: string) {
|
||||||
|
saves.value[player.id].time = player.time;
|
||||||
save();
|
save();
|
||||||
loadSave(this.saves[id]);
|
loadSave(saves.value[id]);
|
||||||
},
|
}
|
||||||
newSave() {
|
|
||||||
const playerData = newSave();
|
function newSave() {
|
||||||
this.saves[playerData.id] = playerData;
|
const playerData = createNewSave();
|
||||||
},
|
saves.value[playerData.id] = playerData;
|
||||||
newFromPreset(preset: string) {
|
}
|
||||||
|
|
||||||
|
function newFromPreset(preset: string) {
|
||||||
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
|
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
|
||||||
playerData.id = getUniqueID();
|
playerData.id = getUniqueID();
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
@ -186,54 +201,25 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
|
|
||||||
settings.saves.push(playerData.id);
|
settings.saves.push(playerData.id);
|
||||||
this.saves[playerData.id] = playerData;
|
saves.value[playerData.id] = playerData;
|
||||||
},
|
}
|
||||||
editSave(id: string, newName: string) {
|
|
||||||
this.saves[id].name = newName;
|
function editSave(id: string, newName: string) {
|
||||||
|
saves.value[id].name = newName;
|
||||||
if (player.id === id) {
|
if (player.id === id) {
|
||||||
player.name = newName;
|
player.name = newName;
|
||||||
save();
|
save();
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
id,
|
id,
|
||||||
btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))))
|
btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
importSave(text: string) {
|
|
||||||
this.saveToImport = text;
|
|
||||||
if (text) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
try {
|
|
||||||
const playerData = JSON.parse(decodeURIComponent(escape(atob(text))));
|
|
||||||
if (typeof playerData !== "object") {
|
|
||||||
this.importingFailed = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = getUniqueID();
|
|
||||||
playerData.id = id;
|
|
||||||
localStorage.setItem(
|
|
||||||
id,
|
|
||||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
|
||||||
);
|
|
||||||
this.saves[id] = playerData;
|
|
||||||
this.saveToImport = "";
|
|
||||||
this.importingFailed = false;
|
|
||||||
|
|
||||||
settings.saves.push(id);
|
function update(e: { newIndex: number; oldIndex: number }) {
|
||||||
} catch (e) {
|
|
||||||
this.importingFailed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.importingFailed = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update(e: { newIndex: number; oldIndex: number }) {
|
|
||||||
settings.saves.splice(e.newIndex, 0, settings.saves.splice(e.oldIndex, 1)[0]);
|
settings.saves.splice(e.newIndex, 0, settings.saves.splice(e.oldIndex, 1)[0]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div :style="{ width: spacingWidth, height: spacingHeight }"></div>
|
<div :style="{ width, height }"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
export default defineComponent({
|
width: string;
|
||||||
name: "spacer",
|
height: string;
|
||||||
props: {
|
}>(),
|
||||||
width: String,
|
{
|
||||||
height: String
|
width: "8px",
|
||||||
},
|
height: "17px"
|
||||||
computed: {
|
|
||||||
spacingWidth(): string {
|
|
||||||
return this.width || "8px";
|
|
||||||
},
|
|
||||||
spacingHeight(): string {
|
|
||||||
return this.height || "17px";
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,54 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="sticky" :style="{ top }" ref="sticky" data-v-sticky>
|
<div class="sticky" :style="{ top }" ref="element" data-v-sticky>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { nextTick, onMounted, ref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const top = ref("0");
|
||||||
name: "sticky",
|
const observer = new ResizeObserver(updateTop);
|
||||||
data() {
|
const element = ref<HTMLElement | null>(null);
|
||||||
return {
|
|
||||||
top: "0",
|
function updateTop() {
|
||||||
observer: null
|
let el = element.value;
|
||||||
} as {
|
|
||||||
top: string;
|
|
||||||
observer: ResizeObserver | null;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setup();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setup() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.sticky == undefined) {
|
|
||||||
this.$nextTick(this.setup);
|
|
||||||
} else {
|
|
||||||
this.updateTop();
|
|
||||||
this.observer = new ResizeObserver(this.updateTop);
|
|
||||||
this.observer.observe((this.$refs.sticky as HTMLElement).parentElement!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateTop() {
|
|
||||||
let el = this.$refs.sticky as HTMLElement;
|
|
||||||
if (el == undefined) {
|
if (el == undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let top = 0;
|
let newTop = 0;
|
||||||
while (el.previousSibling) {
|
while (el.previousSibling) {
|
||||||
const sibling = el.previousSibling as HTMLElement;
|
const sibling = el.previousSibling as HTMLElement;
|
||||||
if (sibling.dataset && "vSticky" in sibling.dataset) {
|
if (sibling.dataset && "vSticky" in sibling.dataset) {
|
||||||
top += sibling.offsetHeight;
|
newTop += sibling.offsetHeight;
|
||||||
}
|
}
|
||||||
el = sibling;
|
el = sibling;
|
||||||
}
|
}
|
||||||
this.top = top + "px";
|
top.value = newTop + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextTick(updateTop);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const el = element.value?.parentElement;
|
||||||
|
if (el) {
|
||||||
|
observer.observe(el);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tpsDisplay" v-if="tps !== 'NaN'">TPS: {{ tps }}</div>
|
<div class="tpsDisplay" v-if="!tps.isNan">TPS: {{ tps }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import state from "@/game/state";
|
import state from "@/game/state";
|
||||||
import Decimal, { formatWhole } from "@/util/bignum";
|
import Decimal from "@/util/bignum";
|
||||||
import { defineComponent } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const tps = computed(() =>
|
||||||
name: "TPS",
|
|
||||||
computed: {
|
|
||||||
tps() {
|
|
||||||
return formatWhole(
|
|
||||||
Decimal.div(
|
Decimal.div(
|
||||||
state.lastTenTicks.length,
|
state.lastTenTicks.length,
|
||||||
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
@click="$emit('selectTab')"
|
|
||||||
class="tabButton"
|
|
||||||
:style="style"
|
|
||||||
:class="{
|
|
||||||
notify: options.notify,
|
|
||||||
resetNotify: options.resetNotify,
|
|
||||||
floating,
|
|
||||||
activeTab
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ text }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import themes from "@/data/themes";
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import settings from "@/game/settings";
|
|
||||||
import { Subtab } from "@/typings/features/subtab";
|
|
||||||
import { InjectLayerMixin } from "@/util/vue";
|
|
||||||
import { defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "tab-button",
|
|
||||||
mixins: [InjectLayerMixin],
|
|
||||||
props: {
|
|
||||||
text: String,
|
|
||||||
options: {
|
|
||||||
type: Object as PropType<Subtab>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
activeTab: Boolean
|
|
||||||
},
|
|
||||||
emits: ["selectTab"],
|
|
||||||
computed: {
|
|
||||||
floating(): boolean {
|
|
||||||
return themes[settings.theme].floatingTabs;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.floating || this.activeTab
|
|
||||||
? {
|
|
||||||
borderColor: layers[this.layer].color
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
layers[this.layer].componentStyles?.["tab-button"],
|
|
||||||
this.options.resetNotify && this.options.glowColor
|
|
||||||
? {
|
|
||||||
boxShadow: this.floating
|
|
||||||
? `-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 8px ${this.options.glowColor}`
|
|
||||||
: `0px 10px 7px -10px ${this.options.glowColor}`
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
this.options.notify && this.options.glowColor
|
|
||||||
? {
|
|
||||||
boxShadow: this.floating
|
|
||||||
? `-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 20px ${this.options.glowColor}`
|
|
||||||
: `0px 15px 7px -10px ${this.options.glowColor}`
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
this.options.buttonStyle
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tabButton {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--foreground);
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 5px 20px;
|
|
||||||
margin: 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 2px solid;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabButton:hover {
|
|
||||||
transform: scale(1.1, 1.1);
|
|
||||||
text-shadow: 0 0 7px var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabButton:not(.floating) {
|
|
||||||
height: 50px;
|
|
||||||
margin: 0;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
border-top: none;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
border-radius: 0;
|
|
||||||
transform: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabButton:not(.floating):not(.activeTab) {
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,115 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="tabs-container">
|
|
||||||
<div v-for="(tab, index) in tabs" :key="index" class="tab" :ref="`tab-${index}`">
|
|
||||||
<Nav v-if="index === 0 && !useHeader" />
|
|
||||||
<div class="inner-tab">
|
|
||||||
<LayerProvider
|
|
||||||
:layer="tab"
|
|
||||||
:index="index"
|
|
||||||
v-if="tab in components && components[tab]"
|
|
||||||
>
|
|
||||||
<component :is="components[tab]" />
|
|
||||||
</LayerProvider>
|
|
||||||
<layer-tab
|
|
||||||
:layer="tab"
|
|
||||||
:index="index"
|
|
||||||
v-else-if="tab in components"
|
|
||||||
:minimizable="minimizable[tab]"
|
|
||||||
:tab="() => $refs[`tab-${index}`]"
|
|
||||||
/>
|
|
||||||
<component :is="tab" :index="index" v-else />
|
|
||||||
</div>
|
|
||||||
<div class="separator" v-if="index !== tabs.length - 1"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import modInfo from "@/data/modInfo.json";
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { coerceComponent, mapPlayer } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Tabs",
|
|
||||||
data() {
|
|
||||||
return { useHeader: modInfo.useHeader };
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapPlayer(["tabs"]),
|
|
||||||
components() {
|
|
||||||
return Object.keys(layers).reduce(
|
|
||||||
(acc: Record<string, Component | string | false>, curr) => {
|
|
||||||
acc[curr] =
|
|
||||||
(layers[curr].component && coerceComponent(layers[curr].component!)) ||
|
|
||||||
false;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
minimizable() {
|
|
||||||
return Object.keys(layers).reduce((acc: Record<string, boolean>, curr) => {
|
|
||||||
acc[curr] = layers[curr].minimizable !== false;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.tabs-container {
|
|
||||||
width: 100vw;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: hidden;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
transition-duration: 0s;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-tab {
|
|
||||||
padding: 50px 10px;
|
|
||||||
min-height: calc(100% - 100px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
position: absolute;
|
|
||||||
right: -3px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 6px;
|
|
||||||
background: var(--outline);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.tab hr {
|
|
||||||
height: 4px;
|
|
||||||
border: none;
|
|
||||||
background: var(--outline);
|
|
||||||
margin: 7px -10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab .modal-body hr {
|
|
||||||
margin: 7px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="tooltip-container"
|
class="tooltip-container"
|
||||||
:class="{ shown }"
|
:class="{ shown: isShown }"
|
||||||
@mouseenter="setHover(true)"
|
@mouseenter="setHover(true)"
|
||||||
@mouseleave="setHover(false)"
|
@mouseleave="setHover(false)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="shown"
|
v-if="isShown"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:class="{ top, left, right, bottom }"
|
:class="{ top, left, right, bottom }"
|
||||||
:style="{
|
:style="{
|
||||||
|
@ -16,56 +16,28 @@
|
||||||
'--yoffset': yoffset || '0px'
|
'--yoffset': yoffset || '0px'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<component :is="tooltipDisplay" />
|
<component :is="component" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { CoercableComponent } from "@/typings/component";
|
import { FeatureComponent } from "@/features/feature";
|
||||||
|
import { Tooltip } from "@/features/tooltip";
|
||||||
import { coerceComponent } from "@/util/vue";
|
import { coerceComponent } from "@/util/vue";
|
||||||
import { Component, defineComponent, PropType } from "vue";
|
import { computed, ref, toRefs, unref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = toRefs(defineProps<FeatureComponent<Tooltip>>());
|
||||||
name: "tooltip",
|
|
||||||
data() {
|
const isHovered = ref(false);
|
||||||
return {
|
|
||||||
hover: false
|
function setHover(hover: boolean) {
|
||||||
};
|
isHovered.value = hover;
|
||||||
},
|
}
|
||||||
props: {
|
|
||||||
force: Boolean,
|
const isShown = computed(() => unref(props.force) || isHovered.value);
|
||||||
display: {
|
const component = computed(() => props.display.value && coerceComponent(unref(props.display)));
|
||||||
type: [String, Object] as PropType<CoercableComponent>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
top: Boolean,
|
|
||||||
left: Boolean,
|
|
||||||
right: Boolean,
|
|
||||||
bottom: Boolean,
|
|
||||||
xoffset: String,
|
|
||||||
yoffset: String
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
tooltipDisplay(): Component | string {
|
|
||||||
return coerceComponent(this.display, "span", false);
|
|
||||||
},
|
|
||||||
shown(): boolean {
|
|
||||||
return this.force || this.hover;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
tab() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setHover(hover: boolean) {
|
|
||||||
this.hover = hover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -2,15 +2,10 @@
|
||||||
<div class="vr" :style="{ height }"></div>
|
<div class="vr" :style="{ height }"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
defineProps<{
|
||||||
|
height?: string;
|
||||||
export default defineComponent({
|
}>();
|
||||||
name: "vr",
|
|
||||||
props: {
|
|
||||||
height: String
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
<template>
|
|
||||||
<line
|
|
||||||
:stroke="stroke"
|
|
||||||
:stroke-width="strokeWidth"
|
|
||||||
v-bind="typeof options === 'string' ? [] : options"
|
|
||||||
:x1="startPosition.x"
|
|
||||||
:y1="startPosition.y"
|
|
||||||
:x2="endPosition.x"
|
|
||||||
:y2="endPosition.y"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { BranchNode, BranchOptions, Position } from "@/typings/branches";
|
|
||||||
import { defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "branch-line",
|
|
||||||
props: {
|
|
||||||
options: {
|
|
||||||
type: [String, Object] as PropType<string | BranchOptions>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
startNode: {
|
|
||||||
type: Object as PropType<BranchNode>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
endNode: {
|
|
||||||
type: Object as PropType<BranchNode>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
stroke(): string {
|
|
||||||
if (typeof this.options === "string" || !("stroke" in this.options)) {
|
|
||||||
return "white";
|
|
||||||
}
|
|
||||||
return this.options.stroke!;
|
|
||||||
},
|
|
||||||
strokeWidth(): number | string {
|
|
||||||
if (typeof this.options === "string" || !("strokeWidth" in this.options)) {
|
|
||||||
return "15";
|
|
||||||
}
|
|
||||||
return this.options["strokeWidth"]!;
|
|
||||||
},
|
|
||||||
startPosition(): Position {
|
|
||||||
const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
|
|
||||||
if (typeof this.options !== "string" && "startOffset" in this.options) {
|
|
||||||
position.x += this.options.startOffset?.x || 0;
|
|
||||||
position.y += this.options.startOffset?.y || 0;
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
},
|
|
||||||
endPosition(): Position {
|
|
||||||
const position = { x: this.endNode.x || 0, y: this.endNode.y || 0 };
|
|
||||||
if (typeof this.options !== "string" && "endOffset" in this.options) {
|
|
||||||
position.x += this.options.endOffset?.x || 0;
|
|
||||||
position.y += this.options.endOffset?.y || 0;
|
|
||||||
}
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,125 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="branch"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { BranchOptions } from "@/typings/branches";
|
|
||||||
import { ComponentPublicInstance, defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
// Annoying work-around for injected functions not appearing on `this`
|
|
||||||
// Also requires those annoying 3 lines in any function that uses this
|
|
||||||
type BranchInjectedComponent<T extends ComponentPublicInstance> = {
|
|
||||||
registerNode?: (id: string, component: ComponentPublicInstance) => void;
|
|
||||||
unregisterNode?: (id: string) => void;
|
|
||||||
registerBranch?: (start: string, options: string | BranchOptions) => void;
|
|
||||||
unregisterBranch?: (start: string, options: string | BranchOptions) => void;
|
|
||||||
} & T;
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "branch-node",
|
|
||||||
props: {
|
|
||||||
featureType: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
branches: Array as PropType<Array<string | BranchOptions>>
|
|
||||||
},
|
|
||||||
inject: ["registerNode", "unregisterNode", "registerBranch", "unregisterBranch"],
|
|
||||||
mounted() {
|
|
||||||
const id = `${this.featureType}@${this.id}`;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const _this = this;
|
|
||||||
const injectedThis = this as BranchInjectedComponent<typeof _this>;
|
|
||||||
if (injectedThis.registerNode) {
|
|
||||||
injectedThis.registerNode(id, this);
|
|
||||||
}
|
|
||||||
if (injectedThis.registerBranch) {
|
|
||||||
this.branches
|
|
||||||
?.map(this.handleBranch)
|
|
||||||
.forEach(branch => injectedThis.registerBranch!(id, branch));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
const id = `${this.featureType}@${this.id}`;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const _this = this;
|
|
||||||
const injectedThis = this as BranchInjectedComponent<typeof _this>;
|
|
||||||
if (injectedThis.unregisterNode) {
|
|
||||||
injectedThis.unregisterNode(id);
|
|
||||||
}
|
|
||||||
if (injectedThis.unregisterBranch) {
|
|
||||||
this.branches
|
|
||||||
?.map(this.handleBranch)
|
|
||||||
.forEach(branch => injectedThis.unregisterBranch!(id, branch));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
featureType(newValue, oldValue) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const _this = this;
|
|
||||||
const injectedThis = this as BranchInjectedComponent<typeof _this>;
|
|
||||||
if (injectedThis.registerNode && injectedThis.unregisterNode) {
|
|
||||||
injectedThis.unregisterNode(`${oldValue}@${this.id}`);
|
|
||||||
injectedThis.registerNode(`${newValue}@${this.id}`, this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
id(newValue, oldValue) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const _this = this;
|
|
||||||
const injectedThis = this as BranchInjectedComponent<typeof _this>;
|
|
||||||
if (injectedThis.registerNode && injectedThis.unregisterNode) {
|
|
||||||
injectedThis.unregisterNode(`${this.featureType}@${oldValue}`);
|
|
||||||
injectedThis.registerNode(`${this.featureType}@${newValue}`, this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
branches(newValue, oldValue) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
const _this = this;
|
|
||||||
const injectedThis = this as BranchInjectedComponent<typeof _this>;
|
|
||||||
if (injectedThis.registerBranch && injectedThis.unregisterBranch) {
|
|
||||||
const id = `${this.featureType}@${this.id}`;
|
|
||||||
oldValue
|
|
||||||
?.map(this.handleBranch)
|
|
||||||
.forEach((branch: string | BranchOptions) =>
|
|
||||||
injectedThis.unregisterBranch!(id, branch)
|
|
||||||
);
|
|
||||||
newValue
|
|
||||||
?.map(this.handleBranch)
|
|
||||||
.forEach((branch: string | BranchOptions) =>
|
|
||||||
injectedThis.registerBranch!(id, branch)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleBranch(branch: string | BranchOptions) {
|
|
||||||
if (typeof branch === "string") {
|
|
||||||
return branch.includes("@") ? branch : `${this.featureType}@${branch}`;
|
|
||||||
}
|
|
||||||
if (!branch.target?.includes("@")) {
|
|
||||||
return {
|
|
||||||
...branch,
|
|
||||||
target: `${branch.featureType || this.featureType}@${branch.target}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return branch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.branch {
|
|
||||||
position: absolute;
|
|
||||||
z-index: -10;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,122 +0,0 @@
|
||||||
<template>
|
|
||||||
<slot />
|
|
||||||
<div ref="resizeListener" class="resize-listener" />
|
|
||||||
<svg v-bind="$attrs">
|
|
||||||
<branch-line
|
|
||||||
v-for="(branch, index) in branches"
|
|
||||||
:key="index"
|
|
||||||
:startNode="nodes[branch.start]"
|
|
||||||
:endNode="nodes[branch.end]"
|
|
||||||
:options="branch.options"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { BranchLink, BranchNode, BranchOptions } from "@/typings/branches";
|
|
||||||
import { ComponentPublicInstance, defineComponent } from "vue";
|
|
||||||
|
|
||||||
const observerOptions = {
|
|
||||||
attributes: true,
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "branches",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
observer: new MutationObserver(this.updateNodes as (...args: unknown[]) => void),
|
|
||||||
resizeObserver: new ResizeObserver(this.updateBounds as (...args: unknown[]) => void),
|
|
||||||
nodes: {},
|
|
||||||
links: [],
|
|
||||||
boundingRect: new DOMRect()
|
|
||||||
} as {
|
|
||||||
observer: MutationObserver;
|
|
||||||
resizeObserver: ResizeObserver;
|
|
||||||
nodes: Record<string, BranchNode>;
|
|
||||||
links: Array<BranchLink>;
|
|
||||||
boundingRect: DOMRect;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
|
||||||
this.resizeObserver.observe(this.$refs.resizeListener as HTMLElement);
|
|
||||||
this.updateNodes();
|
|
||||||
},
|
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
registerNode: this.registerNode,
|
|
||||||
unregisterNode: this.unregisterNode,
|
|
||||||
registerBranch: this.registerBranch,
|
|
||||||
unregisterBranch: this.unregisterBranch
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
branches(): Array<BranchLink> {
|
|
||||||
return this.links.filter(
|
|
||||||
link =>
|
|
||||||
link.start in this.nodes &&
|
|
||||||
link.end in this.nodes &&
|
|
||||||
this.nodes[link.start].x != undefined &&
|
|
||||||
this.nodes[link.start].y != undefined &&
|
|
||||||
this.nodes[link.end].x != undefined &&
|
|
||||||
this.nodes[link.end].y != undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateBounds() {
|
|
||||||
if (this.$refs.resizeListener != undefined) {
|
|
||||||
this.boundingRect = (this.$refs
|
|
||||||
.resizeListener as HTMLElement).getBoundingClientRect();
|
|
||||||
this.updateNodes();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateNodes() {
|
|
||||||
if (this.$refs.resizeListener != undefined) {
|
|
||||||
Object.keys(this.nodes).forEach(id => this.updateNode(id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateNode(id: string) {
|
|
||||||
const linkStartRect = this.nodes[id].element.getBoundingClientRect();
|
|
||||||
this.nodes[id].x = linkStartRect.x + linkStartRect.width / 2 - this.boundingRect.x;
|
|
||||||
this.nodes[id].y = linkStartRect.y + linkStartRect.height / 2 - this.boundingRect.y;
|
|
||||||
},
|
|
||||||
registerNode(id: string, component: ComponentPublicInstance) {
|
|
||||||
const element = component.$el.parentElement;
|
|
||||||
this.nodes[id] = { component, element };
|
|
||||||
this.observer.observe(element, observerOptions);
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.resizeListener != undefined) {
|
|
||||||
this.updateNode(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unregisterNode(id: string) {
|
|
||||||
delete this.nodes[id];
|
|
||||||
},
|
|
||||||
registerBranch(start: string, options: string | BranchOptions) {
|
|
||||||
const end = typeof options === "string" ? options : options.target;
|
|
||||||
this.links.push({ start, end: end!, options });
|
|
||||||
},
|
|
||||||
unregisterBranch(start: string, options: string | BranchOptions) {
|
|
||||||
const index = this.links.findIndex(l => l.start === start && l.options === options);
|
|
||||||
this.links.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
svg,
|
|
||||||
.resize-listener {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -10;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="row" v-for="(row, index) in rows" :key="index">
|
|
||||||
<template v-if="index !== 'side'">
|
|
||||||
<tree-node
|
|
||||||
v-for="(node, nodeIndex) in row"
|
|
||||||
:key="nodeIndex"
|
|
||||||
:id="node"
|
|
||||||
@show-modal="openModal"
|
|
||||||
:append="append"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
<span class="side-nodes" v-if="rows.side">
|
|
||||||
<tree-node
|
|
||||||
v-for="(node, nodeIndex) in rows.side"
|
|
||||||
:key="nodeIndex"
|
|
||||||
:id="node"
|
|
||||||
@show-modal="openModal"
|
|
||||||
:append="append"
|
|
||||||
small
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<modal :show="showModal" @close="closeModal">
|
|
||||||
<template v-slot:header
|
|
||||||
><h2 v-if="modalHeader">{{ modalHeader }}</h2></template
|
|
||||||
>
|
|
||||||
<template v-slot:body
|
|
||||||
><layer-tab v-if="modal" :layer="modal" :index="tab.index" :forceFirstTab="true"
|
|
||||||
/></template>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import { defineComponent, PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "tree",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showModal: false,
|
|
||||||
modal: null
|
|
||||||
} as {
|
|
||||||
showModal: boolean;
|
|
||||||
modal: string | null;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
nodes: Object as PropType<Record<string, Array<string>>>,
|
|
||||||
append: Boolean
|
|
||||||
},
|
|
||||||
inject: ["tab"],
|
|
||||||
computed: {
|
|
||||||
modalHeader(): string | null {
|
|
||||||
if (this.modal == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return layers[this.modal].name || this.modal;
|
|
||||||
},
|
|
||||||
rows(): Record<string, Array<string>> {
|
|
||||||
if (this.nodes != undefined) {
|
|
||||||
return this.nodes;
|
|
||||||
}
|
|
||||||
const rows = Object.keys(layers).reduce((acc: Record<string, Array<string>>, curr) => {
|
|
||||||
if (!(layers[curr].displayRow in acc)) {
|
|
||||||
acc[layers[curr].displayRow] = [];
|
|
||||||
}
|
|
||||||
if (layers[curr].position != undefined) {
|
|
||||||
acc[layers[curr].displayRow][layers[curr].position!] = curr;
|
|
||||||
} else if (layers[curr].displayRow) {
|
|
||||||
acc[layers[curr].displayRow].push(curr);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
return Object.keys(rows).reduce((acc: Record<string, Array<string>>, curr) => {
|
|
||||||
acc[curr] = rows[curr].filter(layer => layer);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openModal(id: string) {
|
|
||||||
this.showModal = true;
|
|
||||||
this.modal = id;
|
|
||||||
},
|
|
||||||
closeModal() {
|
|
||||||
this.showModal = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.row {
|
|
||||||
margin: 50px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-nodes {
|
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
|
||||||
top: 65px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,164 +0,0 @@
|
||||||
<template>
|
|
||||||
<tooltip
|
|
||||||
:display="tooltip"
|
|
||||||
:force="forceTooltip"
|
|
||||||
:class="{
|
|
||||||
ghost: layer.layerShown === 'ghost',
|
|
||||||
treeNode: true,
|
|
||||||
[id]: true,
|
|
||||||
hidden: !layer.layerShown,
|
|
||||||
locked: !unlocked,
|
|
||||||
notify: layer.notify && unlocked,
|
|
||||||
resetNotify: layer.resetNotify,
|
|
||||||
can: unlocked,
|
|
||||||
small
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<LayerProvider :index="tab.index" :layer="id">
|
|
||||||
<button v-if="layer.shown" @click="clickTab" :style="style" :disabled="!unlocked">
|
|
||||||
<component :is="display" />
|
|
||||||
<branch-node :branches="layer.branches" :id="id" featureType="tree-node" />
|
|
||||||
</button>
|
|
||||||
<mark-node :mark="layer.mark" />
|
|
||||||
</LayerProvider>
|
|
||||||
</tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { CoercableComponent } from "@/typings/component";
|
|
||||||
import { Layer } from "@/typings/layer";
|
|
||||||
import { coerceComponent } from "@/util/vue";
|
|
||||||
import { Component, defineComponent } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "tree-node",
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
small: Boolean,
|
|
||||||
append: Boolean
|
|
||||||
},
|
|
||||||
emits: ["show-modal"],
|
|
||||||
inject: ["tab"],
|
|
||||||
computed: {
|
|
||||||
layer(): Layer {
|
|
||||||
return layers[this.id];
|
|
||||||
},
|
|
||||||
unlocked(): boolean {
|
|
||||||
if (this.layer.canClick != undefined) {
|
|
||||||
return this.layer.canClick;
|
|
||||||
}
|
|
||||||
return this.layer.unlocked;
|
|
||||||
},
|
|
||||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
|
||||||
return [
|
|
||||||
this.unlocked ? { backgroundColor: this.layer.color } : undefined,
|
|
||||||
this.layer.notify && this.unlocked
|
|
||||||
? {
|
|
||||||
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${this.layer.trueGlowColor}`
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
this.layer.nodeStyle
|
|
||||||
];
|
|
||||||
},
|
|
||||||
display(): Component | string {
|
|
||||||
if (this.layer.nodeDisplay != undefined) {
|
|
||||||
return coerceComponent(this.layer.nodeDisplay);
|
|
||||||
} else if (this.layer.image != undefined) {
|
|
||||||
return coerceComponent(`<img src=${this.layer.image}/>`);
|
|
||||||
} else {
|
|
||||||
return coerceComponent(this.layer.symbol);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
forceTooltip(): boolean {
|
|
||||||
return player.layers[this.id].forceTooltip === true;
|
|
||||||
},
|
|
||||||
tooltip(): CoercableComponent {
|
|
||||||
if (this.layer.canClick != undefined) {
|
|
||||||
if (this.layer.canClick) {
|
|
||||||
return this.layer.tooltip || "I am a button!";
|
|
||||||
} else {
|
|
||||||
return this.layer.tooltipLocked || this.layer.tooltip || "I am a button!";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (player.layers[this.id].unlocked) {
|
|
||||||
return (
|
|
||||||
this.layer.tooltip ||
|
|
||||||
`{{ formatWhole(player.layers.${this.id}.points) }} {{ layers.${this.id}.resource }}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
this.layer.tooltipLocked ||
|
|
||||||
`Reach {{ formatWhole(layers.${this.id}.requires) }} {{ layers.${this.id}.baseResource }} to unlock (You have {{ formatWhole(layers.${this.id}.baseAmount) }} {{ layers.${this.id}.baseResource }})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickTab(e: MouseEvent) {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
player.layers[this.id].forceTooltip = !player.layers[this.id].forceTooltip;
|
|
||||||
} else if (this.layer.click != undefined) {
|
|
||||||
this.layer.click();
|
|
||||||
} else if (this.layer.modal) {
|
|
||||||
this.$emit("show-modal", this.id);
|
|
||||||
} else if (this.append) {
|
|
||||||
if (player.tabs.includes(this.id.toString())) {
|
|
||||||
const index = player.tabs.lastIndexOf(this.id.toString());
|
|
||||||
player.tabs = [...player.tabs.slice(0, index), ...player.tabs.slice(index + 1)];
|
|
||||||
} else {
|
|
||||||
player.tabs = [...player.tabs, this.id.toString()];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
player.tabs = [
|
|
||||||
...player.tabs.slice(
|
|
||||||
0,
|
|
||||||
((this as unknown) as { tab: { index: number } }).tab.index + 1
|
|
||||||
),
|
|
||||||
this.id.toString()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.treeNode {
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 10px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.treeNode button {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.125);
|
|
||||||
border-radius: inherit;
|
|
||||||
font-size: 40px;
|
|
||||||
color: rgba(0, 0, 0, 0.5);
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
|
|
||||||
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0px 0px 20px var(--background);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.treeNode.small {
|
|
||||||
height: 60px;
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.treeNode.small button {
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost {
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :show="show" @close="$emit('closeDialog', 'Changelog')">
|
<Modal v-model="isOpen">
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<h2>Changelog</h2>
|
<h2>Changelog</h2>
|
||||||
</template>
|
</template>
|
||||||
|
@ -18,15 +18,16 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import Modal from "@/components/system/Modal.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
const isOpen = ref(false);
|
||||||
name: "Changelog",
|
|
||||||
props: {
|
defineExpose({
|
||||||
show: Boolean
|
open() {
|
||||||
},
|
isOpen.value = true;
|
||||||
emits: ["closeDialog"]
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
145
src/data/common.tsx
Normal file
145
src/data/common.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import {
|
||||||
|
Clickable,
|
||||||
|
ClickableOptions,
|
||||||
|
createClickable,
|
||||||
|
GenericClickable
|
||||||
|
} from "@/features/clickable";
|
||||||
|
import { GenericConversion } from "@/features/conversion";
|
||||||
|
import { CoercableComponent, Replace, setDefault } from "@/features/feature";
|
||||||
|
import { displayResource } from "@/features/resource";
|
||||||
|
import {
|
||||||
|
createTreeNode,
|
||||||
|
GenericTree,
|
||||||
|
GenericTreeNode,
|
||||||
|
TreeNode,
|
||||||
|
TreeNodeOptions
|
||||||
|
} from "@/features/tree";
|
||||||
|
import player from "@/game/player";
|
||||||
|
import Decimal from "@/util/bignum";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { computed, Ref, unref } from "vue";
|
||||||
|
|
||||||
|
export interface ResetButtonOptions extends ClickableOptions {
|
||||||
|
conversion: GenericConversion;
|
||||||
|
tree: GenericTree;
|
||||||
|
treeNode: GenericTreeNode;
|
||||||
|
resetDescription?: Computable<string>;
|
||||||
|
showNextAt?: Computable<boolean>;
|
||||||
|
display?: Computable<CoercableComponent>;
|
||||||
|
canClick?: Computable<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetButton<T extends ResetButtonOptions> = Replace<
|
||||||
|
Clickable<T>,
|
||||||
|
{
|
||||||
|
resetDescription: GetComputableTypeWithDefault<T["resetDescription"], Ref<string>>;
|
||||||
|
showNextAt: GetComputableTypeWithDefault<T["showNextAt"], true>;
|
||||||
|
display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>;
|
||||||
|
canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>;
|
||||||
|
onClick: VoidFunction;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericResetButton = Replace<
|
||||||
|
GenericClickable & ResetButton<ResetButtonOptions>,
|
||||||
|
{
|
||||||
|
resetDescription: ProcessedComputable<string>;
|
||||||
|
showNextAt: ProcessedComputable<boolean>;
|
||||||
|
display: ProcessedComputable<CoercableComponent>;
|
||||||
|
canClick: ProcessedComputable<boolean>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
||||||
|
options: T
|
||||||
|
): ResetButton<T> {
|
||||||
|
setDefault(options, "showNextAt", true);
|
||||||
|
if (options.resetDescription == null) {
|
||||||
|
options.resetDescription = computed(() =>
|
||||||
|
Decimal.lt(proxy.conversion.gainResource.value, 1e3) ? "Reset for " : ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.display == null) {
|
||||||
|
options.display = computed(() => {
|
||||||
|
const nextAt = unref(proxy.showNextAt) && (
|
||||||
|
<template>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Next:{" "}
|
||||||
|
{displayResource(
|
||||||
|
proxy.conversion.baseResource,
|
||||||
|
unref(proxy.conversion.nextAt)
|
||||||
|
)}{" "}
|
||||||
|
{proxy.conversion.baseResource.displayName}
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{proxy.resetDescription}
|
||||||
|
<b>
|
||||||
|
{displayResource(
|
||||||
|
proxy.conversion.gainResource,
|
||||||
|
unref(proxy.conversion.currentGain)
|
||||||
|
)}
|
||||||
|
</b>
|
||||||
|
{proxy.conversion.gainResource.displayName}
|
||||||
|
{nextAt}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.canClick == null) {
|
||||||
|
options.canClick = computed(() => Decimal.gt(unref(proxy.conversion.currentGain), 0));
|
||||||
|
}
|
||||||
|
const onClick = options.onClick;
|
||||||
|
options.onClick = function() {
|
||||||
|
proxy.conversion.convert();
|
||||||
|
proxy.tree.reset(proxy.treeNode);
|
||||||
|
onClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxy = (createClickable(options) as unknown) as ResetButton<T>;
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayerTreeNodeOptions extends TreeNodeOptions {
|
||||||
|
layerID: string;
|
||||||
|
color: string;
|
||||||
|
append?: boolean;
|
||||||
|
}
|
||||||
|
export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
|
||||||
|
TreeNode<T>,
|
||||||
|
{
|
||||||
|
append: ProcessedComputable<boolean>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(options: T): LayerTreeNode<T> {
|
||||||
|
processComputable(options as T, "append");
|
||||||
|
|
||||||
|
return (createTreeNode({
|
||||||
|
...options,
|
||||||
|
display: options.layerID,
|
||||||
|
onClick:
|
||||||
|
options.append != null && options.append
|
||||||
|
? function() {
|
||||||
|
if (player.tabs.includes(options.layerID)) {
|
||||||
|
const index = player.tabs.lastIndexOf(options.layerID);
|
||||||
|
player.tabs = [
|
||||||
|
...player.tabs.slice(0, index),
|
||||||
|
...player.tabs.slice(index + 1)
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
player.tabs = [...player.tabs, options.layerID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: function() {
|
||||||
|
player.tabs.splice(1, 1, options.layerID);
|
||||||
|
}
|
||||||
|
}) as unknown) as LayerTreeNode<T>;
|
||||||
|
}
|
|
@ -1,103 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { GridCell } from "@/typings/features/grid";
|
|
||||||
import { RawLayer } from "@/typings/layer";
|
|
||||||
import Decimal from "@/util/bignum";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: "a",
|
|
||||||
startData() {
|
|
||||||
return {
|
|
||||||
unlocked: true,
|
|
||||||
points: new Decimal(0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
color: "yellow",
|
|
||||||
modal: true,
|
|
||||||
name: "Achievements",
|
|
||||||
resource: "achievement power",
|
|
||||||
row: "side",
|
|
||||||
tooltip() {
|
|
||||||
// Optional, tooltip displays when the layer is locked
|
|
||||||
return "Achievements";
|
|
||||||
},
|
|
||||||
achievementPopups: true,
|
|
||||||
achievements: {
|
|
||||||
data: {
|
|
||||||
11: {
|
|
||||||
image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
|
|
||||||
name: "Get me!",
|
|
||||||
done() {
|
|
||||||
return true;
|
|
||||||
}, // This one is a freebie
|
|
||||||
goalTooltip: "How did this happen?", // Shows when achievement is not completed
|
|
||||||
doneTooltip: "You did it!" // Showed when the achievement is completed
|
|
||||||
},
|
|
||||||
12: {
|
|
||||||
name: "Impossible!",
|
|
||||||
done() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
goalTooltip: "Mwahahaha!", // Shows when achievement is not completed
|
|
||||||
doneTooltip: "HOW????", // Showed when the achievement is completed
|
|
||||||
style: { color: "#04e050" }
|
|
||||||
},
|
|
||||||
13: {
|
|
||||||
name: "EIEIO",
|
|
||||||
done() {
|
|
||||||
return player.layers.f.points.gte(1);
|
|
||||||
},
|
|
||||||
tooltip:
|
|
||||||
"Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).", // Showed when the achievement is completed
|
|
||||||
onComplete() {
|
|
||||||
console.log("Bork bork bork!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
midsection: "<grid id='test' />",
|
|
||||||
grids: {
|
|
||||||
data: {
|
|
||||||
test: {
|
|
||||||
maxRows: 3,
|
|
||||||
rows: 2,
|
|
||||||
cols: 2,
|
|
||||||
getStartState(cell: string) {
|
|
||||||
return cell;
|
|
||||||
},
|
|
||||||
getUnlocked() {
|
|
||||||
// Default
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
getCanClick() {
|
|
||||||
return player.points.eq(10);
|
|
||||||
},
|
|
||||||
getStyle(cell) {
|
|
||||||
return { backgroundColor: "#" + ((Number((this[cell] as GridCell).state) * 1234) % 999999) };
|
|
||||||
},
|
|
||||||
click(cell) {
|
|
||||||
// Don't forget onHold
|
|
||||||
(this[cell] as GridCell).state = ((this[cell] as GridCell).state as number) + 1;
|
|
||||||
},
|
|
||||||
getTitle(cell) {
|
|
||||||
let direction;
|
|
||||||
if (cell === "101") {
|
|
||||||
direction = "top";
|
|
||||||
} else if (cell === "102") {
|
|
||||||
direction = "bottom";
|
|
||||||
} else if (cell === "201") {
|
|
||||||
direction = "left";
|
|
||||||
} else if (cell === "202") {
|
|
||||||
direction = "right";
|
|
||||||
}
|
|
||||||
return `<tooltip display='${JSON.stringify(this.style)}' ${direction}>
|
|
||||||
<h3>Gridable #${cell}</h3>
|
|
||||||
</tooltip>`;
|
|
||||||
},
|
|
||||||
getDisplay(cell) {
|
|
||||||
return (this[cell] as GridCell).state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as RawLayer;
|
|
115
src/data/layers/aca/a.tsx
Normal file
115
src/data/layers/aca/a.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import Tooltip from "@/components/system/Tooltip.vue";
|
||||||
|
import { points as mainPoints } from "@/data/mod";
|
||||||
|
import { createAchievement } from "@/features/achievement";
|
||||||
|
import { createGrid } from "@/features/grid";
|
||||||
|
import { createResource } from "@/features/resource";
|
||||||
|
import { createTreeNode } from "@/features/tree";
|
||||||
|
import { createLayer } from "@/game/layers";
|
||||||
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
|
import Decimal from "@/util/bignum";
|
||||||
|
import { render, renderRow } from "@/util/vue";
|
||||||
|
import { points as fPoints } from "./f";
|
||||||
|
|
||||||
|
const id = "a";
|
||||||
|
const color = "yellow";
|
||||||
|
const name = "Achievements";
|
||||||
|
const points = createResource<DecimalSource>(0, "achievement power");
|
||||||
|
|
||||||
|
export const treeNode = createTreeNode({
|
||||||
|
tooltip: "Achievements",
|
||||||
|
onClick() {
|
||||||
|
// TODO open this layer as a modal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ach1 = createAchievement({
|
||||||
|
image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
|
||||||
|
display: "Get me!",
|
||||||
|
tooltip() {
|
||||||
|
if (this.earned.value) {
|
||||||
|
return "You did it!";
|
||||||
|
}
|
||||||
|
return "How did this happen?";
|
||||||
|
},
|
||||||
|
shouldEarn: true
|
||||||
|
});
|
||||||
|
const ach2 = createAchievement({
|
||||||
|
display: "Impossible!",
|
||||||
|
tooltip() {
|
||||||
|
if (this.earned.value) {
|
||||||
|
return "HOW????";
|
||||||
|
}
|
||||||
|
return "Mwahahaha!";
|
||||||
|
},
|
||||||
|
style: { color: "#04e050" }
|
||||||
|
});
|
||||||
|
const ach3 = createAchievement({
|
||||||
|
display: "EIEIO",
|
||||||
|
tooltip:
|
||||||
|
"Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).",
|
||||||
|
shouldEarn: function() {
|
||||||
|
return Decimal.gte(fPoints.value, 1);
|
||||||
|
},
|
||||||
|
onComplete() {
|
||||||
|
console.log("Bork bork bork!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const achievements = [ach1, ach2, ach3];
|
||||||
|
|
||||||
|
const grid = createGrid({
|
||||||
|
rows: 2,
|
||||||
|
cols: 2,
|
||||||
|
getStartState(id) {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
getStyle(id) {
|
||||||
|
return { backgroundColor: `#${(Number(id) * 1234) % 999999}` };
|
||||||
|
},
|
||||||
|
// TODO display should return an object
|
||||||
|
getTitle(id) {
|
||||||
|
let direction;
|
||||||
|
if (id === "101") {
|
||||||
|
direction = "top";
|
||||||
|
} else if (id === "102") {
|
||||||
|
direction = "bottom";
|
||||||
|
} else if (id === "201") {
|
||||||
|
direction = "left";
|
||||||
|
} else if (id === "202") {
|
||||||
|
direction = "right";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip display={JSON.stringify(this.cells[id].style)} {...{ direction }}>
|
||||||
|
<h3>Gridable #{id}</h3>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getDisplay(id) {
|
||||||
|
return String(id);
|
||||||
|
},
|
||||||
|
getCanClick() {
|
||||||
|
return Decimal.eq(mainPoints.value, 10);
|
||||||
|
},
|
||||||
|
onClick(id, state) {
|
||||||
|
this.cells[id].state = Number(state) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const display = (
|
||||||
|
<template>
|
||||||
|
{renderRow(achievements)}
|
||||||
|
{render(grid)}
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
const layer = createLayer({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
points,
|
||||||
|
achievements,
|
||||||
|
grid,
|
||||||
|
treeNode,
|
||||||
|
display
|
||||||
|
});
|
||||||
|
|
||||||
|
export default layer;
|
|
@ -1,573 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
import { Direction } from "@/game/enums";
|
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { DecimalSource } from "@/lib/break_eternity";
|
|
||||||
import { RawLayer } from "@/typings/layer";
|
|
||||||
import Decimal, { format, formatWhole } from "@/util/bignum";
|
|
||||||
import {
|
|
||||||
buyableEffect,
|
|
||||||
challengeCompletions,
|
|
||||||
getBuyableAmount,
|
|
||||||
hasMilestone,
|
|
||||||
hasUpgrade,
|
|
||||||
setBuyableAmount,
|
|
||||||
upgradeEffect
|
|
||||||
} from "@/util/features";
|
|
||||||
import { resetLayer, resetLayerData } from "@/util/layers";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: "c", // This is assigned automatically, both to the layer and all upgrades, etc. Shown here so you know about it
|
|
||||||
name: "Candies", // This is optional, only used in a few places, If absent it just uses the layer id.
|
|
||||||
symbol: "C", // This appears on the layer's node. Default is the id with the first letter capitalized
|
|
||||||
position: 0, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order
|
|
||||||
startData() {
|
|
||||||
return {
|
|
||||||
unlocked: true,
|
|
||||||
points: new Decimal(0),
|
|
||||||
best: new Decimal(0),
|
|
||||||
total: new Decimal(0),
|
|
||||||
beep: false,
|
|
||||||
thingy: "pointy",
|
|
||||||
otherThingy: 10,
|
|
||||||
spentOnBuyables: new Decimal(0)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
minWidth: 800,
|
|
||||||
color: "#4BDC13",
|
|
||||||
requires: new Decimal(10), // Can be a function that takes requirement increases into account
|
|
||||||
resource: "lollipops", // Name of prestige currency
|
|
||||||
baseResource: "points", // Name of resource prestige is based on
|
|
||||||
baseAmount() {
|
|
||||||
return player.points;
|
|
||||||
}, // Get the current amount of baseResource
|
|
||||||
type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
|
|
||||||
exponent: 0.5, // Prestige currency exponent
|
|
||||||
base: 5, // Only needed for static layers, base of the formula (b^(x^exp))
|
|
||||||
roundUpCost: false, // True if the cost needs to be rounded up (use when baseResource is static?)
|
|
||||||
|
|
||||||
// For normal layers, gain beyond [softcap] points is put to the [softcapPower]th power
|
|
||||||
softcap: new Decimal(1e100),
|
|
||||||
softcapPower: new Decimal(0.5),
|
|
||||||
canBuyMax() {}, // Only needed for static layers with buy max
|
|
||||||
gainMult() {
|
|
||||||
// Calculate the multiplier for main currency from bonuses
|
|
||||||
let mult = new Decimal(1);
|
|
||||||
/*
|
|
||||||
if (hasUpgrade(this.layer, 166)) mult = mult.times(2); // These upgrades don't exist
|
|
||||||
if (hasUpgrade(this.layer, 120))
|
|
||||||
mult = mult.times(upgradeEffect(this.layer, 120) as DecimalSource);
|
|
||||||
*/
|
|
||||||
return mult;
|
|
||||||
},
|
|
||||||
gainExp() {
|
|
||||||
// Calculate the exponent on main currency from bonuses
|
|
||||||
return new Decimal(1);
|
|
||||||
},
|
|
||||||
row: 0, // Row the layer is in on the tree (0 is the first row)
|
|
||||||
effect() {
|
|
||||||
return {
|
|
||||||
// Formulas for any boosts inherent to resources in the layer. Can return a single value instead of an object if there is just one effect
|
|
||||||
waffleBoost: Decimal.pow(player.layers[this.layer].points, 0.2),
|
|
||||||
icecreamCap: player.layers[this.layer].points.times(10)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
effectDisplay() {
|
|
||||||
// Optional text to describe the effects
|
|
||||||
const eff = this.effect as { waffleBoost: Decimal; icecreamCap: Decimal };
|
|
||||||
const waffleBoost = eff.waffleBoost.times(
|
|
||||||
(buyableEffect(this.layer, 11) as { first: Decimal, second: Decimal }).first
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
"which are boosting waffles by " +
|
|
||||||
format(waffleBoost) +
|
|
||||||
" and increasing the Ice Cream cap by " +
|
|
||||||
format(eff.icecreamCap)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
infoboxes: {
|
|
||||||
data: {
|
|
||||||
coolInfo: {
|
|
||||||
title: "Lore",
|
|
||||||
titleStyle: { color: "#FE0000" },
|
|
||||||
body: "DEEP LORE!",
|
|
||||||
bodyStyle: { "background-color": "#0000EE" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
milestones: {
|
|
||||||
data: {
|
|
||||||
0: {
|
|
||||||
requirementDisplay: "3 Lollipops",
|
|
||||||
done() {
|
|
||||||
return (player.layers[this.layer].best as Decimal).gte(3);
|
|
||||||
}, // Used to determine when to give the milestone
|
|
||||||
effectDisplay: "Unlock the next milestone"
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
requirementDisplay: "4 Lollipops",
|
|
||||||
unlocked() {
|
|
||||||
return hasMilestone(this.layer, 0);
|
|
||||||
},
|
|
||||||
done() {
|
|
||||||
return (player.layers[this.layer].best as Decimal).gte(4);
|
|
||||||
},
|
|
||||||
effectDisplay: "You can toggle beep and boop (which do nothing)",
|
|
||||||
optionsDisplay: `
|
|
||||||
<div style="display: flex; justify-content: center">
|
|
||||||
<Toggle :value="player.layers.c.beep" @change="value => player.layers.c.beep = value" />
|
|
||||||
<Toggle :value="player.layers.f.boop" @change="value => player.layers.f.boop = value" />
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
style() {
|
|
||||||
if (hasMilestone(this.layer, this.id))
|
|
||||||
return {
|
|
||||||
backgroundColor: "#1111DD"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
challenges: {
|
|
||||||
data: {
|
|
||||||
11: {
|
|
||||||
name: "Fun",
|
|
||||||
completionLimit: 3,
|
|
||||||
challengeDescription() {
|
|
||||||
return (
|
|
||||||
"Makes the game 0% harder<br>" +
|
|
||||||
challengeCompletions(this.layer, this.id) +
|
|
||||||
"/" +
|
|
||||||
this.completionLimit +
|
|
||||||
" completions"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
unlocked() {
|
|
||||||
return (player.layers[this.layer].best as Decimal).gt(0);
|
|
||||||
},
|
|
||||||
goalDescription: "Have 20 points I guess",
|
|
||||||
canComplete() {
|
|
||||||
return player.points.gte(20);
|
|
||||||
},
|
|
||||||
effect() {
|
|
||||||
const ret = player.layers[this.layer].points.add(1).tetrate(0.02);
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
rewardDisplay() {
|
|
||||||
return format(this.effect as Decimal) + "x";
|
|
||||||
},
|
|
||||||
countsAs: [12, 21], // Use this for if a challenge includes the effects of other challenges. Being in this challenge "counts as" being in these.
|
|
||||||
rewardDescription: "Says hi",
|
|
||||||
onComplete() {
|
|
||||||
console.log("hiii");
|
|
||||||
}, // Called when you successfully complete the challenge
|
|
||||||
onEnter() {
|
|
||||||
console.log("So challenging");
|
|
||||||
},
|
|
||||||
onExit() {
|
|
||||||
console.log("Sweet freedom!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
upgrades: {
|
|
||||||
data: {
|
|
||||||
11: {
|
|
||||||
title: "Generator of Genericness",
|
|
||||||
description: "Gain 1 Point every second.",
|
|
||||||
cost: new Decimal(1),
|
|
||||||
unlocked() {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
} // The upgrade is only visible when this is true
|
|
||||||
},
|
|
||||||
12: {
|
|
||||||
description:
|
|
||||||
"Point generation is faster based on your unspent Lollipops.",
|
|
||||||
cost: new Decimal(1),
|
|
||||||
unlocked() {
|
|
||||||
return hasUpgrade(this.layer, 11);
|
|
||||||
},
|
|
||||||
effect() {
|
|
||||||
// Calculate bonuses from the upgrade. Can return a single value or an object with multiple values
|
|
||||||
let ret = player.layers[this.layer].points
|
|
||||||
.add(1)
|
|
||||||
.pow(
|
|
||||||
player.layers[this.layer].upgrades!.includes(24)
|
|
||||||
? 1.1
|
|
||||||
: player.layers[this.layer].upgrades!.includes(14)
|
|
||||||
? 0.75
|
|
||||||
: 0.5
|
|
||||||
);
|
|
||||||
if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000");
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
effectDisplay() {
|
|
||||||
return format(this.effect as Decimal) + "x";
|
|
||||||
} // Add formatting to the effect
|
|
||||||
},
|
|
||||||
13: {
|
|
||||||
unlocked() {
|
|
||||||
return hasUpgrade(this.layer, 12);
|
|
||||||
},
|
|
||||||
onPurchase() {
|
|
||||||
// This function triggers when the upgrade is purchased
|
|
||||||
player.layers[this.layer].unlockOrder = 0;
|
|
||||||
},
|
|
||||||
style() {
|
|
||||||
if (hasUpgrade(this.layer, this.id))
|
|
||||||
return {
|
|
||||||
"background-color": "#1111dd"
|
|
||||||
};
|
|
||||||
else if (!this.canAfford) {
|
|
||||||
return {
|
|
||||||
"background-color": "#dd1111"
|
|
||||||
};
|
|
||||||
} // Otherwise use the default
|
|
||||||
},
|
|
||||||
canAfford() {
|
|
||||||
return player.points.lte(7);
|
|
||||||
},
|
|
||||||
pay() {
|
|
||||||
player.points = player.points.add(7);
|
|
||||||
},
|
|
||||||
fullDisplay:
|
|
||||||
"Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab."
|
|
||||||
},
|
|
||||||
22: {
|
|
||||||
title: "This upgrade doesn't exist",
|
|
||||||
description: "Or does it?.",
|
|
||||||
currencyLocation() {
|
|
||||||
return player.layers[this.layer].buyables;
|
|
||||||
}, // The object in player data that the currency is contained in
|
|
||||||
currencyDisplayName: "exhancers", // Use if using a nonstandard currency
|
|
||||||
currencyInternalName: 11, // Use if using a nonstandard currency
|
|
||||||
|
|
||||||
cost: new Decimal(3),
|
|
||||||
unlocked() {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
} // The upgrade is only visible when this is true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
buyables: {
|
|
||||||
showBRespecButton: true,
|
|
||||||
respec() {
|
|
||||||
// Optional, reset things and give back your currency. Having this function makes a respec button appear
|
|
||||||
player.layers[this.layer].points = player.layers[this.layer].points.add(
|
|
||||||
player.layers[this.layer].spentOnBuyables as Decimal
|
|
||||||
); // A built-in thing to keep track of this but only keeps a single value
|
|
||||||
this.reset();
|
|
||||||
resetLayer(this.layer, true); // Force a reset
|
|
||||||
},
|
|
||||||
respecButtonDisplay: "Respec Thingies", // Text on Respec button, optional
|
|
||||||
respecWarningDisplay:
|
|
||||||
"Are you sure? Respeccing these doesn't accomplish much.",
|
|
||||||
data: {
|
|
||||||
11: {
|
|
||||||
title: "Exhancers", // Optional, displayed at the top in a larger font
|
|
||||||
cost() {
|
|
||||||
// cost for buying xth buyable, can be an object if there are multiple currencies
|
|
||||||
let x = this.amount;
|
|
||||||
if (x.gte(25)) x = x.pow(2).div(25);
|
|
||||||
const cost = Decimal.pow(2, x.pow(1.5));
|
|
||||||
return cost.floor();
|
|
||||||
},
|
|
||||||
effect() {
|
|
||||||
// Effects of owning x of the items, x is a decimal
|
|
||||||
const x = this.amount;
|
|
||||||
const eff = {} as { first?: Decimal; second?: Decimal };
|
|
||||||
if (x.gte(0)) eff.first = Decimal.pow(25, x.pow(1.1));
|
|
||||||
else eff.first = Decimal.pow(1 / 25, x.times(-1).pow(1.1));
|
|
||||||
|
|
||||||
if (x.gte(0)) eff.second = x.pow(0.8);
|
|
||||||
else
|
|
||||||
eff.second = x
|
|
||||||
.times(-1)
|
|
||||||
.pow(0.8)
|
|
||||||
.times(-1);
|
|
||||||
return eff;
|
|
||||||
},
|
|
||||||
display() {
|
|
||||||
// Everything else displayed in the buyable button after the title
|
|
||||||
return (
|
|
||||||
"Cost: " +
|
|
||||||
format(this.cost!) +
|
|
||||||
" lollipops\n\
|
|
||||||
Amount: " +
|
|
||||||
player.layers[this.layer].buyables![this.id] +
|
|
||||||
"/4\n\
|
|
||||||
Adds + " +
|
|
||||||
format((this.effect as { first: Decimal; second: Decimal }).first) +
|
|
||||||
" things and multiplies stuff by " +
|
|
||||||
format((this.effect as { first: Decimal; second: Decimal }).second)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
unlocked() {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
},
|
|
||||||
canAfford() {
|
|
||||||
return player.layers[this.layer].points.gte(this.cost!);
|
|
||||||
},
|
|
||||||
buy() {
|
|
||||||
const cost = this.cost!;
|
|
||||||
player.layers[this.layer].points = player.layers[
|
|
||||||
this.layer
|
|
||||||
].points.sub(cost);
|
|
||||||
player.layers[this.layer].buyables![this.id] = player.layers[
|
|
||||||
this.layer
|
|
||||||
].buyables![this.id].add(1);
|
|
||||||
player.layers[this.layer].spentOnBuyables = (player.layers[
|
|
||||||
this.layer
|
|
||||||
].spentOnBuyables as Decimal).add(cost); // This is a built-in system that you can use for respeccing but it only works with a single Decimal value
|
|
||||||
},
|
|
||||||
buyMax() {}, // You'll have to handle this yourself if you want
|
|
||||||
style: { height: "222px" },
|
|
||||||
purchaseLimit: new Decimal(4),
|
|
||||||
sellOne() {
|
|
||||||
const amount = getBuyableAmount(this.layer, this.id)!;
|
|
||||||
if (amount.lte(0)) return; // Only sell one if there is at least one
|
|
||||||
setBuyableAmount(this.layer, this.id, amount.sub(1));
|
|
||||||
player.layers[this.layer].points = player.layers[
|
|
||||||
this.layer
|
|
||||||
].points.add(this.cost!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onReset(resettingLayer: string) {
|
|
||||||
// Triggers when this layer is being reset, along with the layer doing the resetting. Not triggered by lower layers resetting, but is by layers on the same row.
|
|
||||||
if (
|
|
||||||
layers[resettingLayer].row != undefined &&
|
|
||||||
this.row != undefined &&
|
|
||||||
layers[resettingLayer].row! > this.row!
|
|
||||||
)
|
|
||||||
resetLayerData(this.layer, ["points"]); // This is actually the default behavior
|
|
||||||
},
|
|
||||||
automate() {}, // Do any automation inherent to this layer if appropriate
|
|
||||||
resetsNothing() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onPrestige() {
|
|
||||||
return;
|
|
||||||
}, // Useful for if you gain secondary resources or have other interesting things happen to this layer when you reset it. You gain the currency after this function ends.
|
|
||||||
|
|
||||||
hotkeys: [
|
|
||||||
{
|
|
||||||
key: "c",
|
|
||||||
description: "reset for lollipops or whatever",
|
|
||||||
press() {
|
|
||||||
if (layers[this.layer].canReset) resetLayer(this.layer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "ctrl+c",
|
|
||||||
description: "respec things",
|
|
||||||
press() {
|
|
||||||
layers[this.layer].buyables!.respec!();
|
|
||||||
},
|
|
||||||
unlocked() {
|
|
||||||
return hasUpgrade("c", "22");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
increaseUnlockOrder: [], // Array of layer names to have their order increased when this one is first unlocked
|
|
||||||
|
|
||||||
microtabs: {
|
|
||||||
stuff: {
|
|
||||||
data: {
|
|
||||||
first: {
|
|
||||||
display: `
|
|
||||||
<upgrades />
|
|
||||||
<div>confirmed</div>`
|
|
||||||
},
|
|
||||||
second: {
|
|
||||||
embedLayer: "f"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
otherStuff: {
|
|
||||||
// There could be another set of microtabs here
|
|
||||||
data: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
bars: {
|
|
||||||
data: {
|
|
||||||
longBoi: {
|
|
||||||
fillStyle: { "background-color": "#FFFFFF" },
|
|
||||||
baseStyle: { "background-color": "#696969" },
|
|
||||||
textStyle: { color: "#04e050" },
|
|
||||||
|
|
||||||
borderStyle() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
direction: Direction.Right,
|
|
||||||
width: 300,
|
|
||||||
height: 30,
|
|
||||||
progress() {
|
|
||||||
return player.points
|
|
||||||
.add(1)
|
|
||||||
.log(10)
|
|
||||||
.div(10)
|
|
||||||
.toNumber();
|
|
||||||
},
|
|
||||||
display() {
|
|
||||||
return format(player.points) + " / 1e10 points";
|
|
||||||
},
|
|
||||||
unlocked: true
|
|
||||||
},
|
|
||||||
tallBoi: {
|
|
||||||
fillStyle: { "background-color": "#4BEC13" },
|
|
||||||
baseStyle: { "background-color": "#000000" },
|
|
||||||
textStyle: { "text-shadow": "0px 0px 2px #000000" },
|
|
||||||
|
|
||||||
borderStyle() {
|
|
||||||
return { "border-width": "7px" };
|
|
||||||
},
|
|
||||||
direction: Direction.Up,
|
|
||||||
width: 50,
|
|
||||||
height: 200,
|
|
||||||
progress() {
|
|
||||||
return player.points.div(100);
|
|
||||||
},
|
|
||||||
display() {
|
|
||||||
return formatWhole(player.points.div(1).min(100)) + "%";
|
|
||||||
},
|
|
||||||
unlocked: true
|
|
||||||
},
|
|
||||||
flatBoi: {
|
|
||||||
fillStyle: { "background-color": "#FE0102" },
|
|
||||||
baseStyle: { "background-color": "#222222" },
|
|
||||||
textStyle: { "text-shadow": "0px 0px 2px #000000" },
|
|
||||||
|
|
||||||
borderStyle() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
direction: Direction.Up,
|
|
||||||
width: 100,
|
|
||||||
height: 30,
|
|
||||||
progress() {
|
|
||||||
return player.layers.c.points.div(50);
|
|
||||||
},
|
|
||||||
unlocked: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Optional, lets you format the tab yourself by listing components. You can create your own components in v.js.
|
|
||||||
subtabs: {
|
|
||||||
"main tab": {
|
|
||||||
buttonStyle() {
|
|
||||||
return { color: "orange" };
|
|
||||||
},
|
|
||||||
notify: true,
|
|
||||||
display: `
|
|
||||||
<main-display />
|
|
||||||
<sticky><prestige-button /></sticky>
|
|
||||||
<resource-display />
|
|
||||||
<spacer height="5px" />
|
|
||||||
<button onclick='console.log("yeet")'>'HI'</button>
|
|
||||||
<div>Name your points!</div>
|
|
||||||
<TextField :value="player.layers.c.thingy" @change="value => player.layers.c.thingy = value" :field="false" />
|
|
||||||
<sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">I have {{ format(player.points) }} {{ player.layers.c.thingy }} points!</sticky>
|
|
||||||
<hr />
|
|
||||||
<milestones />
|
|
||||||
<spacer />
|
|
||||||
<upgrades />
|
|
||||||
<challenges />`,
|
|
||||||
glowColor: "blue"
|
|
||||||
},
|
|
||||||
thingies: {
|
|
||||||
resetNotify: true,
|
|
||||||
style() {
|
|
||||||
return { "background-color": "#222222", "--background": "#222222" };
|
|
||||||
},
|
|
||||||
buttonStyle() {
|
|
||||||
return { "border-color": "orange" };
|
|
||||||
},
|
|
||||||
display: `
|
|
||||||
<buyables />
|
|
||||||
<spacer />
|
|
||||||
<row style="width: 600px; height: 350px; background-color: green; border-style: solid;">
|
|
||||||
<Toggle :value="player.layers.c.beep" @change="value => player.layers.c.beep = value" />
|
|
||||||
<spacer width="30px" height="10px" />
|
|
||||||
<div>Beep</div>
|
|
||||||
<spacer />
|
|
||||||
<vr height="200px"/>
|
|
||||||
<column>
|
|
||||||
<prestige-button style="width: 150px; height: 80px" />
|
|
||||||
<prestige-button style="width: 100px; height: 150px" />
|
|
||||||
</column>
|
|
||||||
</row>
|
|
||||||
<spacer />
|
|
||||||
<img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" />`
|
|
||||||
},
|
|
||||||
jail: {
|
|
||||||
display: `
|
|
||||||
<infobox id="coolInfo" />
|
|
||||||
<bar id="longBoi" />
|
|
||||||
<spacer />
|
|
||||||
<row>
|
|
||||||
<column style="background-color: #555555; padding: 15px">
|
|
||||||
<div style="color: teal">Sugar level:</div><spacer /><bar id="tallBoi" />
|
|
||||||
</column>
|
|
||||||
<spacer />
|
|
||||||
<column>
|
|
||||||
<div>idk</div>
|
|
||||||
<spacer width="0" height="50px" />
|
|
||||||
<bar id="flatBoi" />
|
|
||||||
</column>
|
|
||||||
</row>
|
|
||||||
<spacer />
|
|
||||||
<div>It's jail because "bars"! So funny! Ha ha!</div>
|
|
||||||
<tree :nodes="[['f', 'c'], ['g', 'spook', 'h']]" />`
|
|
||||||
},
|
|
||||||
illuminati: {
|
|
||||||
unlocked() {
|
|
||||||
return hasUpgrade("c", 13);
|
|
||||||
},
|
|
||||||
display: `
|
|
||||||
<h1> C O N F I R M E D </h1>
|
|
||||||
<spacer />
|
|
||||||
<microtab family="stuff" style="width: 660px; height: 370px; background-color: brown; --background: brown; border: solid white; margin: auto" />
|
|
||||||
<div>Adjust how many points H gives you!</div>
|
|
||||||
<Slider :value="player.layers.c.otherThingy" @change="value => player.layers.c.otherThingy = value" :min="1" :max="30" />`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style() {
|
|
||||||
return {
|
|
||||||
//'background-color': '#3325CC'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
nodeStyle() {
|
|
||||||
return {
|
|
||||||
// Style on the layer node
|
|
||||||
color: "#3325CC",
|
|
||||||
"text-decoration": "underline"
|
|
||||||
};
|
|
||||||
},
|
|
||||||
glowColor: "orange", // If the node is highlighted, it will be this color (default is red)
|
|
||||||
componentStyles: {
|
|
||||||
challenge() {
|
|
||||||
return { height: "200px" };
|
|
||||||
},
|
|
||||||
"prestige-button"() {
|
|
||||||
return { color: "#AA66AA" };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip() {
|
|
||||||
// Optional, tooltip displays when the layer is unlocked
|
|
||||||
let tooltip = "{{ formatWhole(player.layers.c.points) }} {{ layers.c.resource }}";
|
|
||||||
if (player.layers[this.layer].buyables![11].gt(0))
|
|
||||||
tooltip +=
|
|
||||||
"<br><i><br><br><br>{{ formatWhole(player.layers.c.buyables![11]) }} Exhancers</i>";
|
|
||||||
return tooltip;
|
|
||||||
},
|
|
||||||
shouldNotify() {
|
|
||||||
// Optional, layer will be highlighted on the tree if true.
|
|
||||||
// Layer will automatically highlight if an upgrade is purchasable.
|
|
||||||
return player.layers.c.buyables![11] == new Decimal(1);
|
|
||||||
},
|
|
||||||
mark: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
|
|
||||||
resetDescription: "Melt your points into "
|
|
||||||
} as RawLayer;
|
|
610
src/data/layers/aca/c.tsx
Normal file
610
src/data/layers/aca/c.tsx
Normal file
|
@ -0,0 +1,610 @@
|
||||||
|
import MainDisplay from "@/components/features/MainDisplay.vue";
|
||||||
|
import Slider from "@/components/fields/Slider.vue";
|
||||||
|
import Text from "@/components/fields/Text.vue";
|
||||||
|
import Toggle from "@/components/fields/Toggle.vue";
|
||||||
|
import Column from "@/components/system/Column.vue";
|
||||||
|
import Resource from "@/components/system/Resource.vue";
|
||||||
|
import Row from "@/components/system/Row.vue";
|
||||||
|
import Spacer from "@/components/system/Spacer.vue";
|
||||||
|
import Sticky from "@/components/system/Sticky.vue";
|
||||||
|
import VerticalRule from "@/components/system/VerticalRule.vue";
|
||||||
|
import { createLayerTreeNode, createResetButton } from "@/data/common";
|
||||||
|
import { points as mainPoints, tree as mainTree } from "@/data/mod";
|
||||||
|
import { createBar, Direction } from "@/features/bar";
|
||||||
|
import { createBuyable } from "@/features/buyable";
|
||||||
|
import { createChallenge } from "@/features/challenge";
|
||||||
|
import { createClickable } from "@/features/clickable";
|
||||||
|
import { createCumulativeConversion, createExponentialScaling } from "@/features/conversion";
|
||||||
|
import { persistent, showIf } from "@/features/feature";
|
||||||
|
import { createHotkey } from "@/features/hotkey";
|
||||||
|
import { createInfobox } from "@/features/infobox";
|
||||||
|
import { createMilestone } from "@/features/milestone";
|
||||||
|
import { createReset } from "@/features/reset";
|
||||||
|
import { addSoftcap, createResource, displayResource, trackBest } from "@/features/resource";
|
||||||
|
import { createTab } from "@/features/tab";
|
||||||
|
import { createTabButton, createTabFamily } from "@/features/tabFamily";
|
||||||
|
import { createTree, createTreeNode } from "@/features/tree";
|
||||||
|
import { createUpgrade } from "@/features/upgrade";
|
||||||
|
import { createLayer, getLayer } from "@/game/layers";
|
||||||
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
|
import Decimal, { format, formatWhole } from "@/util/bignum";
|
||||||
|
import { render, renderCol, renderRow } from "@/util/vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { boop, tab as fTab, treeNode as fNode } from "./f";
|
||||||
|
|
||||||
|
const c = getLayer("c");
|
||||||
|
|
||||||
|
const id = "c";
|
||||||
|
const color = "#4BDC13";
|
||||||
|
const name = "Candies";
|
||||||
|
const points = addSoftcap(createResource<DecimalSource>(0, "lollipops"), 1e100, 0.5);
|
||||||
|
const best = trackBest(points);
|
||||||
|
const beep = persistent<boolean>(false);
|
||||||
|
const thingy = persistent<string>("pointy");
|
||||||
|
export const otherThingy = persistent<number>(10);
|
||||||
|
const spentOnBuyables = persistent(new Decimal(10));
|
||||||
|
|
||||||
|
const waffleBoost = computed(() => Decimal.pow(points.value, 0.2));
|
||||||
|
const icecreamCap = computed(() => Decimal.times(points.value, 10));
|
||||||
|
|
||||||
|
const coolInfo = createInfobox({
|
||||||
|
title: "Lore",
|
||||||
|
titleStyle: { color: "#FE0000" },
|
||||||
|
display: "DEEP LORE!",
|
||||||
|
bodyStyle: { backgroundColor: "#0000EE" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const lollipopMilestone3 = createMilestone({
|
||||||
|
shouldEarn() {
|
||||||
|
return Decimal.gte(best.value, 3);
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
requirement: "3 Lollipops",
|
||||||
|
effectDisplay: "Unlock the next milestone"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lollipopMilestone4 = createMilestone({
|
||||||
|
visibility() {
|
||||||
|
return showIf(lollipopMilestone3.earned.value);
|
||||||
|
},
|
||||||
|
shouldEarn() {
|
||||||
|
return Decimal.gte(best.value, 4);
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
requirement: "4 Lollipops",
|
||||||
|
effectDisplay: "You can toggle beep and boop (which do nothing)",
|
||||||
|
optionsDisplay() {
|
||||||
|
return (
|
||||||
|
<div style="display: flex; justify-content: center">
|
||||||
|
<Toggle title="beep" v-model={beep} />
|
||||||
|
<Toggle title="boop" v-model={boop} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style() {
|
||||||
|
if (this.earned) {
|
||||||
|
return { backgroundColor: "#1111DD" };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lollipopMilestones = [lollipopMilestone3, lollipopMilestone4];
|
||||||
|
|
||||||
|
const funChallenge = createChallenge({
|
||||||
|
title: "Fun",
|
||||||
|
completionLimit: 3,
|
||||||
|
display: {
|
||||||
|
description() {
|
||||||
|
return `Makes the game 0% harder<br>${this.completions}/${this.completionLimit} completions`;
|
||||||
|
},
|
||||||
|
goal: "Have 20 points I guess",
|
||||||
|
reward: "Says hi",
|
||||||
|
effectDisplay() {
|
||||||
|
return format(funEffect.value) + "x";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visibility() {
|
||||||
|
return showIf(Decimal.gt(best.value, 0));
|
||||||
|
},
|
||||||
|
goal: 20,
|
||||||
|
resource: mainPoints,
|
||||||
|
onComplete() {
|
||||||
|
console.log("hiii");
|
||||||
|
},
|
||||||
|
onEnter() {
|
||||||
|
console.log("So challenging");
|
||||||
|
},
|
||||||
|
onExit() {
|
||||||
|
console.log("Sweet freedom!");
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
height: "200px"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const funEffect = computed(() => Decimal.add(points.value, 1).tetrate(0.02));
|
||||||
|
|
||||||
|
export const generatorUpgrade = createUpgrade({
|
||||||
|
title: "Generator of Genericness",
|
||||||
|
display: "Gain 1 point every second",
|
||||||
|
cost: 1,
|
||||||
|
resource: points
|
||||||
|
});
|
||||||
|
export const lollipopMultiplierUpgrade = createUpgrade({
|
||||||
|
display: () =>
|
||||||
|
`Point generation is faster based on your unspent Lollipops<br>Currently: ${format(
|
||||||
|
lollipopMultiplierEffect.value
|
||||||
|
)}x`,
|
||||||
|
cost: 1,
|
||||||
|
resource: points,
|
||||||
|
visibility: () => showIf(generatorUpgrade.bought.value)
|
||||||
|
});
|
||||||
|
export const lollipopMultiplierEffect = computed(() => {
|
||||||
|
let ret = Decimal.add(points.value, 1).pow(0.5);
|
||||||
|
if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000");
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
export const unlockIlluminatiUpgrade = createUpgrade({
|
||||||
|
visibility() {
|
||||||
|
return showIf(lollipopMultiplierUpgrade.bought.value);
|
||||||
|
},
|
||||||
|
canPurchase() {
|
||||||
|
return Decimal.lt(mainPoints.value, 7);
|
||||||
|
},
|
||||||
|
onPurchase() {
|
||||||
|
mainPoints.value = Decimal.add(mainPoints.value, 7);
|
||||||
|
},
|
||||||
|
display: "Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab.",
|
||||||
|
style() {
|
||||||
|
if (this.bought) {
|
||||||
|
return { backgroundColor: "#1111dd" };
|
||||||
|
}
|
||||||
|
if (!this.canAfford) {
|
||||||
|
return { backgroundColor: "#dd1111" };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const upgrades = [generatorUpgrade, lollipopMultiplierUpgrade, unlockIlluminatiUpgrade];
|
||||||
|
|
||||||
|
const exhancers = createBuyable({
|
||||||
|
resource: points,
|
||||||
|
cost() {
|
||||||
|
let x = new Decimal(this.amount.value);
|
||||||
|
if (x.gte(25)) {
|
||||||
|
x = x.pow(2).div(25);
|
||||||
|
}
|
||||||
|
const cost = Decimal.pow(2, x.pow(1.5));
|
||||||
|
return cost.floor();
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
title: "Exhancers",
|
||||||
|
description() {
|
||||||
|
return `Adds ${format(
|
||||||
|
exhancersFirstEffect.value
|
||||||
|
)} things and multiplies stuff by ${format(exhancersSecondEffect.value)}.`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPurchase(cost) {
|
||||||
|
spentOnBuyables.value = Decimal.add(spentOnBuyables.value, cost);
|
||||||
|
},
|
||||||
|
style: { height: "222px" },
|
||||||
|
purchaseLimit: 4
|
||||||
|
});
|
||||||
|
const exhancersFirstEffect = computed(() => {
|
||||||
|
if (Decimal.gte(exhancers.amount.value, 0)) {
|
||||||
|
return Decimal.pow(25, Decimal.pow(exhancers.amount.value, 1.1));
|
||||||
|
}
|
||||||
|
return Decimal.pow(1 / 25, Decimal.times(exhancers.amount.value, -1).pow(1.1));
|
||||||
|
});
|
||||||
|
const exhancersSecondEffect = computed(() => {
|
||||||
|
if (Decimal.gte(exhancers.amount.value, 0)) {
|
||||||
|
return Decimal.pow(25, Decimal.pow(exhancers.amount.value, 1.1));
|
||||||
|
}
|
||||||
|
return Decimal.pow(1 / 25, Decimal.times(exhancers.amount.value, -1).pow(1.1));
|
||||||
|
});
|
||||||
|
const confirmRespec = persistent<boolean>(false);
|
||||||
|
const respecBuyables = createClickable({
|
||||||
|
small: true,
|
||||||
|
display: "Respec Thingies",
|
||||||
|
onClick() {
|
||||||
|
if (
|
||||||
|
confirmRespec.value &&
|
||||||
|
!confirm("Are you sure? Respeccing these doesn't accomplish much.")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
points.value = Decimal.add(points.value, spentOnBuyables.value);
|
||||||
|
mainTree.reset(treeNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sellExhancer = createClickable({
|
||||||
|
small: true,
|
||||||
|
display: "Sell One",
|
||||||
|
onClick() {
|
||||||
|
if (Decimal.lte(exhancers.amount.value, 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exhancers.amount.value = Decimal.sub(exhancers.amount.value, 1);
|
||||||
|
points.value = Decimal.add(points.value, exhancers.cost.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const buyablesDisplay = (
|
||||||
|
<Column>
|
||||||
|
<Row>
|
||||||
|
<Toggle title="Confirm" v-model={confirmRespec} />
|
||||||
|
{render(respecBuyables)}
|
||||||
|
</Row>
|
||||||
|
{render(exhancers)}
|
||||||
|
{render(sellExhancer)}
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
|
||||||
|
const longBoi = createBar({
|
||||||
|
fillStyle: { backgroundColor: "#FFFFFF" },
|
||||||
|
baseStyle: { backgroundColor: "#696969" },
|
||||||
|
textStyle: { color: "#04e050" },
|
||||||
|
direction: Direction.Right,
|
||||||
|
width: 300,
|
||||||
|
height: 30,
|
||||||
|
progress() {
|
||||||
|
return Decimal.add(mainPoints.value, 1)
|
||||||
|
.log(10)
|
||||||
|
.div(10)
|
||||||
|
.toNumber();
|
||||||
|
},
|
||||||
|
display() {
|
||||||
|
return format(mainPoints.value) + " / 1e10 points";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tallBoi = createBar({
|
||||||
|
fillStyle: { backgroundColor: "#4BEC13" },
|
||||||
|
baseStyle: { backgroundColor: "#000000" },
|
||||||
|
textStyle: { textShadow: "0px 0px 2px #000000" },
|
||||||
|
borderStyle: { borderWidth: "7px" },
|
||||||
|
direction: Direction.Up,
|
||||||
|
width: 50,
|
||||||
|
height: 200,
|
||||||
|
progress() {
|
||||||
|
return Decimal.div(mainPoints.value, 100);
|
||||||
|
},
|
||||||
|
display() {
|
||||||
|
return formatWhole(Decimal.div(mainPoints.value, 1).min(100)) + "%";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const flatBoi = createBar({
|
||||||
|
fillStyle: { backgroundColor: "#FE0102" },
|
||||||
|
baseStyle: { backgroundColor: "#222222" },
|
||||||
|
textStyle: { textShadow: "0px 0px 2px #000000" },
|
||||||
|
direction: Direction.Up,
|
||||||
|
width: 100,
|
||||||
|
height: 30,
|
||||||
|
progress() {
|
||||||
|
return Decimal.div(points.value, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversion = createCumulativeConversion({
|
||||||
|
scaling: createExponentialScaling(10, 5, 0.5),
|
||||||
|
baseResource: mainPoints,
|
||||||
|
gainResource: points,
|
||||||
|
roundUpCost: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = createReset({
|
||||||
|
thingsToReset: () => [c()]
|
||||||
|
});
|
||||||
|
|
||||||
|
const hotkeys = [
|
||||||
|
createHotkey({
|
||||||
|
key: "c",
|
||||||
|
description: "reset for lollipops or whatever",
|
||||||
|
onPress() {
|
||||||
|
if (resetButton.canClick) {
|
||||||
|
reset.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createHotkey({
|
||||||
|
key: "ctrl+c",
|
||||||
|
description: "respec things",
|
||||||
|
onPress() {
|
||||||
|
respecBuyables.onClick();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
export const treeNode = createLayerTreeNode({
|
||||||
|
layerID: id,
|
||||||
|
color,
|
||||||
|
reset,
|
||||||
|
mark: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
|
||||||
|
tooltip() {
|
||||||
|
let tooltip = displayResource(points);
|
||||||
|
if (Decimal.gt(exhancers.amount.value, 0)) {
|
||||||
|
tooltip += `<br><i><br><br><br>${formatWhole(exhancers.amount.value)} Exhancers</i>`;
|
||||||
|
}
|
||||||
|
return tooltip;
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
color: "#3325CC",
|
||||||
|
textDecoration: "underline"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetButton = createResetButton({
|
||||||
|
conversion,
|
||||||
|
tree: mainTree,
|
||||||
|
treeNode,
|
||||||
|
style: {
|
||||||
|
color: "#AA66AA"
|
||||||
|
},
|
||||||
|
resetDescription: "Melt your points into "
|
||||||
|
});
|
||||||
|
|
||||||
|
export const g = createTreeNode({
|
||||||
|
display: "TH",
|
||||||
|
color: "#6d3678",
|
||||||
|
canClick() {
|
||||||
|
return Decimal.gte(points.value, 10);
|
||||||
|
},
|
||||||
|
tooltip: "Thanos your points",
|
||||||
|
onClick() {
|
||||||
|
points.value = Decimal.div(points.value, 2);
|
||||||
|
console.log("Thanos'd");
|
||||||
|
},
|
||||||
|
glowColor() {
|
||||||
|
if (Decimal.eq(exhancers.amount.value, 1)) {
|
||||||
|
return "orange";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export const h = createTreeNode({
|
||||||
|
id: "h",
|
||||||
|
branches: [
|
||||||
|
"g",
|
||||||
|
() => ({
|
||||||
|
target: "flatBoi",
|
||||||
|
featureType: "bar",
|
||||||
|
endOffset: {
|
||||||
|
x: -50 + 100 * flatBoi.progress.value.toNumber()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
tooltip() {
|
||||||
|
return `Restore your points to ${format(otherThingy.value)}`;
|
||||||
|
},
|
||||||
|
canClick() {
|
||||||
|
return Decimal.lt(mainPoints.value, otherThingy.value);
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
mainPoints.value = otherThingy.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
export const spook = createTreeNode({});
|
||||||
|
const tree = createTree({
|
||||||
|
nodes() {
|
||||||
|
return [
|
||||||
|
[fNode, treeNode],
|
||||||
|
[g, spook, h]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
branches: [
|
||||||
|
{
|
||||||
|
from: fNode,
|
||||||
|
to: treeNode,
|
||||||
|
style: {
|
||||||
|
strokeWidth: "25px",
|
||||||
|
stroke: "blue",
|
||||||
|
filter: "blur(5px)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ from: treeNode, to: g },
|
||||||
|
{ from: g, to: h }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const illuminatiTabs = createTabFamily({
|
||||||
|
tabs: {
|
||||||
|
first: createTabButton({
|
||||||
|
tab: (
|
||||||
|
<template>
|
||||||
|
{renderRow(upgrades)}
|
||||||
|
<div>confirmed</div>
|
||||||
|
</template>
|
||||||
|
),
|
||||||
|
display: "first"
|
||||||
|
}),
|
||||||
|
second: createTabButton({
|
||||||
|
tab: fTab,
|
||||||
|
display: "second"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: "660px",
|
||||||
|
height: "370px",
|
||||||
|
backgroundColor: "brown",
|
||||||
|
"--background": "brown",
|
||||||
|
border: "solid white",
|
||||||
|
margin: "auto"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = createTabFamily({
|
||||||
|
tabs: {
|
||||||
|
mainTab: createTabButton({
|
||||||
|
tab: createTab({
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
<template>
|
||||||
|
<MainDisplay
|
||||||
|
resource={points}
|
||||||
|
color={color}
|
||||||
|
effectDisplay={`which are boosting waffles by ${format(
|
||||||
|
waffleBoost.value
|
||||||
|
)} and increasing the Ice Cream cap by ${format(
|
||||||
|
icecreamCap.value
|
||||||
|
)}`}
|
||||||
|
/>
|
||||||
|
<Sticky>{render(resetButton)}</Sticky>
|
||||||
|
<Resource resource={points} color={color} />
|
||||||
|
<Spacer height="5px" />
|
||||||
|
<button onClick={() => console.log("yeet")}>'HI'</button>
|
||||||
|
<div>Name your points!</div>
|
||||||
|
<Text v-model={thingy} />
|
||||||
|
<Sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">
|
||||||
|
I have {displayResource(mainPoints)}!
|
||||||
|
</Sticky>
|
||||||
|
<hr />
|
||||||
|
{renderCol(lollipopMilestones)}
|
||||||
|
<Spacer />
|
||||||
|
{renderRow(upgrades)}
|
||||||
|
{render(funChallenge)}
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#3325CC"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
display: "main tab",
|
||||||
|
glowColor() {
|
||||||
|
if (
|
||||||
|
generatorUpgrade.canPurchase.value ||
|
||||||
|
lollipopMultiplierUpgrade.canPurchase.value ||
|
||||||
|
unlockIlluminatiUpgrade.canPurchase.value ||
|
||||||
|
funChallenge.canComplete.value
|
||||||
|
) {
|
||||||
|
return "blue";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
style: { color: "orange" }
|
||||||
|
}),
|
||||||
|
thingies: createTabButton({
|
||||||
|
tab: createTab({
|
||||||
|
glowColor: "white",
|
||||||
|
style() {
|
||||||
|
return { backgroundColor: "#222222", "--background": "#222222" };
|
||||||
|
},
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
<template>
|
||||||
|
{buyablesDisplay}
|
||||||
|
<Spacer />
|
||||||
|
<Row style="width: 600px; height: 350px; background-color: green; border-style: solid;">
|
||||||
|
<Toggle v-model={beep} />
|
||||||
|
<Spacer width="30px" height="10px" />
|
||||||
|
<div>Beep</div>
|
||||||
|
<Spacer />
|
||||||
|
<VerticalRule height="200px" />
|
||||||
|
</Row>
|
||||||
|
<Spacer />
|
||||||
|
<img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" />
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
display: "thingies",
|
||||||
|
style: { borderColor: "orange" }
|
||||||
|
}),
|
||||||
|
jail: createTabButton({
|
||||||
|
tab: createTab({
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
<template>
|
||||||
|
{render(coolInfo)}
|
||||||
|
{render(longBoi)}
|
||||||
|
<Spacer />
|
||||||
|
<Row>
|
||||||
|
<Column style="background-color: #555555; padding: 15px">
|
||||||
|
<div style="color: teal">Sugar level:</div>
|
||||||
|
<Spacer />
|
||||||
|
{render(tallBoi)}
|
||||||
|
</Column>
|
||||||
|
<Spacer />
|
||||||
|
<Column>
|
||||||
|
<div>idk</div>
|
||||||
|
<Spacer width="0" height="50px" />
|
||||||
|
{render(flatBoi)}
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
<Spacer />
|
||||||
|
<div>It's jail because "bars"! So funny! Ha ha!</div>
|
||||||
|
{render(tree)}
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#3325CC"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
display: "jail"
|
||||||
|
}),
|
||||||
|
illuminati: createTabButton({
|
||||||
|
tab: createTab({
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
<template>
|
||||||
|
<h1> C O N F I R M E D </h1>
|
||||||
|
<Spacer />
|
||||||
|
{render(illuminatiTabs)}
|
||||||
|
<div>Adjust how many points H gives you!</div>
|
||||||
|
<Slider v-model={otherThingy} min={1} max={30} />
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
backgroundColor: "#3325CC"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
visibility() {
|
||||||
|
return showIf(unlockIlluminatiUpgrade.bought.value);
|
||||||
|
},
|
||||||
|
display: "illuminati"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const layer = createLayer({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
links: tree.links,
|
||||||
|
points,
|
||||||
|
beep,
|
||||||
|
thingy,
|
||||||
|
otherThingy,
|
||||||
|
spentOnBuyables,
|
||||||
|
waffleBoost,
|
||||||
|
icecreamCap,
|
||||||
|
coolInfo,
|
||||||
|
lollipopMilestones,
|
||||||
|
funChallenge,
|
||||||
|
funEffect,
|
||||||
|
generatorUpgrade,
|
||||||
|
lollipopMultiplierUpgrade,
|
||||||
|
lollipopMultiplierEffect,
|
||||||
|
unlockIlluminatiUpgrade,
|
||||||
|
exhancers,
|
||||||
|
exhancersFirstEffect,
|
||||||
|
exhancersSecondEffect,
|
||||||
|
respecBuyables,
|
||||||
|
sellExhancer,
|
||||||
|
bars: { tallBoi, longBoi, flatBoi },
|
||||||
|
tree,
|
||||||
|
g,
|
||||||
|
h,
|
||||||
|
spook,
|
||||||
|
conversion,
|
||||||
|
reset,
|
||||||
|
hotkeys,
|
||||||
|
treeNode,
|
||||||
|
resetButton,
|
||||||
|
minWidth: 800,
|
||||||
|
display: tabs
|
||||||
|
});
|
||||||
|
|
||||||
|
export default layer;
|
|
@ -1,151 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
import { layers as tmp } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { RawLayer } from "@/typings/layer";
|
|
||||||
import Decimal, { formatWhole } from "@/util/bignum";
|
|
||||||
import { getClickableState } from "@/util/features";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: "f",
|
|
||||||
infoboxes: {
|
|
||||||
data: {
|
|
||||||
coolInfo: {
|
|
||||||
title: "Lore",
|
|
||||||
titleStyle: { color: "#FE0000" },
|
|
||||||
body: "DEEP LORE!",
|
|
||||||
bodyStyle: { "background-color": "#0000EE" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startData() {
|
|
||||||
return {
|
|
||||||
unlocked: false,
|
|
||||||
points: new Decimal(0),
|
|
||||||
boop: false,
|
|
||||||
clickables: { [11]: "Start" } // Optional default Clickable state
|
|
||||||
};
|
|
||||||
},
|
|
||||||
color: "#FE0102",
|
|
||||||
requires() {
|
|
||||||
return new Decimal(10);
|
|
||||||
},
|
|
||||||
resource: "farm points",
|
|
||||||
baseResource: "points",
|
|
||||||
baseAmount() {
|
|
||||||
return player.points;
|
|
||||||
},
|
|
||||||
type: "static",
|
|
||||||
exponent: 0.5,
|
|
||||||
base: 3,
|
|
||||||
roundUpCost: true,
|
|
||||||
canBuyMax() {
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
name: "Farms",
|
|
||||||
//directMult() {return new Decimal(player.layers.c.otherThingy)},
|
|
||||||
|
|
||||||
row: 1,
|
|
||||||
branches: [
|
|
||||||
{
|
|
||||||
target: "c",
|
|
||||||
"stroke-width": "25px",
|
|
||||||
stroke: "blue",
|
|
||||||
style: "filter: blur(5px)"
|
|
||||||
}
|
|
||||||
], // When this layer appears, a branch will appear from this layer to any layers here. Each entry can be a pair consisting of a layer id and a color.
|
|
||||||
|
|
||||||
tooltipLocked() {
|
|
||||||
// Optional, tooltip displays when the layer is locked
|
|
||||||
return "This weird farmer dinosaur will only see you if you have at least {{layers.f.requires}} points. You only have {{ formatWhole(player.points) }}";
|
|
||||||
},
|
|
||||||
midsection:
|
|
||||||
'<div><br/><img src="https://images.beano.com/store/24ab3094eb95e5373bca1ccd6f330d4406db8d1f517fc4170b32e146f80d?auto=compress%2Cformat&dpr=1&w=390" /><div>Bork Bork!</div></div>',
|
|
||||||
// The following are only currently used for "custom" Prestige type:
|
|
||||||
prestigeButtonDisplay() {
|
|
||||||
//Is secretly HTML
|
|
||||||
if (!this.canBuyMax)
|
|
||||||
return (
|
|
||||||
"Hi! I'm a <u>weird dinosaur</u> and I'll give you a Farm Point in exchange for all of your points and lollipops! (At least " +
|
|
||||||
formatWhole(tmp[this.layer].nextAt) +
|
|
||||||
" points)"
|
|
||||||
);
|
|
||||||
if (this.canBuyMax)
|
|
||||||
return (
|
|
||||||
"Hi! I'm a <u>weird dinosaur</u> and I'll give you <b>" +
|
|
||||||
formatWhole(tmp[this.layer].resetGain) +
|
|
||||||
"</b> Farm Points in exchange for all of your points and lollipops! (You'll get another one at " +
|
|
||||||
formatWhole(tmp[this.layer].nextAt) +
|
|
||||||
" points)"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
canReset() {
|
|
||||||
return Decimal.gte(tmp[this.layer].baseAmount!, tmp[this.layer].nextAt);
|
|
||||||
},
|
|
||||||
// This is also non minimal, a Clickable!
|
|
||||||
clickables: {
|
|
||||||
masterButtonClick() {
|
|
||||||
if (getClickableState(this.layer, 11) == "Borkened...")
|
|
||||||
player.layers[this.layer].clickables![11] = "Start";
|
|
||||||
},
|
|
||||||
masterButtonDisplay() {
|
|
||||||
return getClickableState(this.layer, 11) == "Borkened..."
|
|
||||||
? "Fix the clickable!"
|
|
||||||
: "Does nothing";
|
|
||||||
}, // Text on Respec button, optional
|
|
||||||
data: {
|
|
||||||
11: {
|
|
||||||
title: "Clicky clicky!", // Optional, displayed at the top in a larger font
|
|
||||||
display() {
|
|
||||||
// Everything else displayed in the buyable button after the title
|
|
||||||
const data = getClickableState(this.layer, this.id);
|
|
||||||
return "Current state:<br>" + data;
|
|
||||||
},
|
|
||||||
unlocked() {
|
|
||||||
return player.layers[this.layer].unlocked;
|
|
||||||
},
|
|
||||||
canClick() {
|
|
||||||
return getClickableState(this.layer, this.id) !== "Borkened...";
|
|
||||||
},
|
|
||||||
click() {
|
|
||||||
switch (getClickableState(this.layer, this.id)) {
|
|
||||||
case "Start":
|
|
||||||
player.layers[this.layer].clickables![this.id] = "A new state!";
|
|
||||||
break;
|
|
||||||
case "A new state!":
|
|
||||||
player.layers[this.layer].clickables![this.id] = "Keep going!";
|
|
||||||
break;
|
|
||||||
case "Keep going!":
|
|
||||||
player.layers[this.layer].clickables![this.id] =
|
|
||||||
"Maybe that's a bit too far...";
|
|
||||||
break;
|
|
||||||
case "Maybe that's a bit too far...":
|
|
||||||
//makeParticles(coolParticle, 4)
|
|
||||||
player.layers[this.layer].clickables![this.id] = "Borkened...";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
player.layers[this.layer].clickables![this.id] = "Start";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hold() {
|
|
||||||
console.log("Clickkkkk...");
|
|
||||||
},
|
|
||||||
style() {
|
|
||||||
switch (getClickableState(this.layer, this.id)) {
|
|
||||||
case "Start":
|
|
||||||
return { "background-color": "green" };
|
|
||||||
case "A new state!":
|
|
||||||
return { "background-color": "yellow" };
|
|
||||||
case "Keep going!":
|
|
||||||
return { "background-color": "orange" };
|
|
||||||
case "Maybe that's a bit too far...":
|
|
||||||
return { "background-color": "red" };
|
|
||||||
default:
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as RawLayer;
|
|
178
src/data/layers/aca/f.tsx
Normal file
178
src/data/layers/aca/f.tsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import MainDisplay from "@/components/features/MainDisplay.vue";
|
||||||
|
import { createLayerTreeNode, createResetButton } from "@/data/common";
|
||||||
|
import { points as mainPoints, tree as mainTree } from "@/data/mod";
|
||||||
|
import { createClickable } from "@/features/clickable";
|
||||||
|
import { createExponentialScaling, createIndependentConversion } from "@/features/conversion";
|
||||||
|
import { persistent } from "@/features/feature";
|
||||||
|
import { createInfobox } from "@/features/infobox";
|
||||||
|
import { createReset } from "@/features/reset";
|
||||||
|
import { createResource, displayResource } from "@/features/resource";
|
||||||
|
import { createLayer, getLayer } from "@/game/layers";
|
||||||
|
import Decimal, { DecimalSource, formatWhole } from "@/util/bignum";
|
||||||
|
import { render } from "@/util/vue";
|
||||||
|
import { otherThingy } from "./c";
|
||||||
|
|
||||||
|
const f = getLayer("f");
|
||||||
|
|
||||||
|
const id = "f";
|
||||||
|
const color = "#FE0102";
|
||||||
|
const name = "Farms";
|
||||||
|
export const points = createResource<DecimalSource>(0, "farm points");
|
||||||
|
export const boop = persistent<boolean>(false);
|
||||||
|
|
||||||
|
const coolInfo = createInfobox({
|
||||||
|
title: "Lore",
|
||||||
|
titleStyle: { color: "#FE0000" },
|
||||||
|
display: "DEEP LORE!",
|
||||||
|
bodyStyle: { backgroundColor: "#0000EE" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const clickableState = persistent<string>("Start");
|
||||||
|
const clickable = createClickable({
|
||||||
|
display: {
|
||||||
|
title: "Clicky clicky!",
|
||||||
|
description() {
|
||||||
|
return "Current state:<br>" + clickableState.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialState: "Start",
|
||||||
|
canClick() {
|
||||||
|
return clickableState.value !== "Borkened...";
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
switch (clickableState.value) {
|
||||||
|
case "Start":
|
||||||
|
clickableState.value = "A new state!";
|
||||||
|
break;
|
||||||
|
case "A new state!":
|
||||||
|
clickableState.value = "Keep going!";
|
||||||
|
break;
|
||||||
|
case "Keep going!":
|
||||||
|
clickableState.value = "Maybe that's a bit too far...";
|
||||||
|
break;
|
||||||
|
case "Maybe that's a bit too far...":
|
||||||
|
//makeParticles(coolParticle, 4)
|
||||||
|
clickableState.value = "Borkened...";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
clickableState.value = "Start";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHold() {
|
||||||
|
console.log("Clickkkkk...");
|
||||||
|
},
|
||||||
|
style() {
|
||||||
|
switch (clickableState.value) {
|
||||||
|
case "Start":
|
||||||
|
return { "background-color": "green" };
|
||||||
|
case "A new state!":
|
||||||
|
return { "background-color": "yellow" };
|
||||||
|
case "Keep going!":
|
||||||
|
return { "background-color": "orange" };
|
||||||
|
case "Maybe that's a bit too far...":
|
||||||
|
return { "background-color": "red" };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetClickable = createClickable({
|
||||||
|
onClick() {
|
||||||
|
if (clickableState.value == "Borkened...") {
|
||||||
|
clickableState.value = "Start";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
display() {
|
||||||
|
return clickableState.value == "Borkened..." ? "Fix the clickable!" : "Does nothing";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = createReset({
|
||||||
|
thingsToReset: () => [f()]
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversion = createIndependentConversion({
|
||||||
|
scaling: createExponentialScaling(10, 3, 0.5),
|
||||||
|
baseResource: mainPoints,
|
||||||
|
gainResource: points,
|
||||||
|
modifyGainAmount: gain => Decimal.times(gain, otherThingy.value)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const treeNode = createLayerTreeNode({
|
||||||
|
layerID: id,
|
||||||
|
color,
|
||||||
|
reset,
|
||||||
|
tooltip() {
|
||||||
|
if (treeNode.canClick.value) {
|
||||||
|
return `${displayResource(points)} ${points.displayName}`;
|
||||||
|
}
|
||||||
|
return `This weird farmer dinosaur will only see you if you have at least 10 points. You only have ${displayResource(
|
||||||
|
mainPoints
|
||||||
|
)}`;
|
||||||
|
},
|
||||||
|
canClick() {
|
||||||
|
return Decimal.gte(mainPoints.value, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetButton = createResetButton({
|
||||||
|
conversion,
|
||||||
|
tree: mainTree,
|
||||||
|
treeNode,
|
||||||
|
display() {
|
||||||
|
if (this.conversion.buyMax) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Hi! I'm a <u>weird dinosaur</u> and I'll give you{" "}
|
||||||
|
<b>{formatWhole(this.conversion.currentGain.value)}</b> Farm Points in exchange
|
||||||
|
for all of your points and lollipops! (You'll get another one at{" "}
|
||||||
|
{formatWhole(this.conversion.nextAt.value)} points)
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Hi! I'm a <u>weird dinosaur</u> and I'll give you a Farm Point in exchange for
|
||||||
|
all of your points and lollipops! (At least{" "}
|
||||||
|
{formatWhole(this.conversion.nextAt.value)} points)
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tab = (): JSX.Element => (
|
||||||
|
<template>
|
||||||
|
{render(coolInfo)}
|
||||||
|
<MainDisplay resource={points} color={color} />
|
||||||
|
{render(resetButton)}
|
||||||
|
<div>You have {formatWhole(conversion.baseResource.value)} points</div>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<img src="https://images.beano.com/store/24ab3094eb95e5373bca1ccd6f330d4406db8d1f517fc4170b32e146f80d?auto=compress%2Cformat&dpr=1&w=390" />
|
||||||
|
<div>Bork Bork!</div>
|
||||||
|
</div>
|
||||||
|
{render(clickable)}
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
||||||
|
const layer = createLayer({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
name,
|
||||||
|
points,
|
||||||
|
boop,
|
||||||
|
coolInfo,
|
||||||
|
clickable,
|
||||||
|
clickableState,
|
||||||
|
resetClickable,
|
||||||
|
reset,
|
||||||
|
conversion,
|
||||||
|
treeNode,
|
||||||
|
resetButton,
|
||||||
|
display: tab
|
||||||
|
});
|
||||||
|
|
||||||
|
export default layer;
|
196
src/data/mod.ts
196
src/data/mod.ts
|
@ -1,196 +0,0 @@
|
||||||
import { layers } from "@/game/layers";
|
|
||||||
import player from "@/game/player";
|
|
||||||
import { RawLayer } from "@/typings/layer";
|
|
||||||
import { PlayerData } from "@/typings/player";
|
|
||||||
import Decimal from "@/util/bignum";
|
|
||||||
import {
|
|
||||||
getBuyableAmount,
|
|
||||||
hasMilestone,
|
|
||||||
hasUpgrade,
|
|
||||||
inChallenge,
|
|
||||||
upgradeEffect
|
|
||||||
} from "@/util/features";
|
|
||||||
import { computed } from "vue";
|
|
||||||
import a from "./layers/aca/a";
|
|
||||||
import c from "./layers/aca/c";
|
|
||||||
import f from "./layers/aca/f";
|
|
||||||
import demoLayer from "./layers/demo";
|
|
||||||
import demoInfinityLayer from "./layers/demo-infinity";
|
|
||||||
|
|
||||||
// Import initial layers
|
|
||||||
|
|
||||||
const g = {
|
|
||||||
id: "g",
|
|
||||||
symbol: "TH",
|
|
||||||
branches: ["c"],
|
|
||||||
color: "#6d3678",
|
|
||||||
shown: true,
|
|
||||||
canClick() {
|
|
||||||
return player.points.gte(10);
|
|
||||||
},
|
|
||||||
tooltip: "Thanos your points",
|
|
||||||
click() {
|
|
||||||
player.points = player.points.div(2);
|
|
||||||
console.log(this.layer);
|
|
||||||
}
|
|
||||||
} as RawLayer;
|
|
||||||
const h = {
|
|
||||||
id: "h",
|
|
||||||
branches: [
|
|
||||||
"g",
|
|
||||||
() => ({
|
|
||||||
target: "flatBoi",
|
|
||||||
featureType: "bar",
|
|
||||||
endOffset: {
|
|
||||||
x:
|
|
||||||
-50 +
|
|
||||||
100 *
|
|
||||||
(layers.c.bars!.data.flatBoi.progress instanceof Number
|
|
||||||
? (layers.c.bars!.data.flatBoi.progress as number)
|
|
||||||
: (layers.c.bars!.data.flatBoi.progress as Decimal).toNumber())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
tooltip() {
|
|
||||||
return "Restore your points to {{ player.layers.c.otherThingy }}";
|
|
||||||
},
|
|
||||||
row: "side",
|
|
||||||
position: 3,
|
|
||||||
canClick() {
|
|
||||||
return player.points.lt(player.layers.c.otherThingy as Decimal);
|
|
||||||
},
|
|
||||||
click() {
|
|
||||||
player.points = new Decimal(player.layers.c.otherThingy as Decimal);
|
|
||||||
}
|
|
||||||
} as RawLayer;
|
|
||||||
const spook = {
|
|
||||||
id: "spook",
|
|
||||||
row: 1,
|
|
||||||
layerShown: "ghost"
|
|
||||||
} as RawLayer;
|
|
||||||
|
|
||||||
const main = {
|
|
||||||
id: "main",
|
|
||||||
display: `
|
|
||||||
<div v-if="player.devSpeed === 0">Game Paused</div>
|
|
||||||
<div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
|
|
||||||
<div v-if="player.offTime != undefined">Offline Time: {{ formatTime(player.offTime.remain) }}</div>
|
|
||||||
<div>
|
|
||||||
<span v-if="player.points.lt('1e1000')">You have </span>
|
|
||||||
<h2>{{ format(player.points) }}</h2>
|
|
||||||
<span v-if="player.points.lt('1e1e6')"> points</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="Decimal.gt(pointGain, 0)">
|
|
||||||
({{ state.oompsMag != 0 ? format(state.oomps) + " OOM" + (state.oompsMag < 0 ? "^OOM" : state.oompsMag > 1 ? "^" + state.oompsMag : "") + "s" : formatSmall(pointGain) }}/sec)
|
|
||||||
</div>
|
|
||||||
<spacer />
|
|
||||||
<modal :show="false">
|
|
||||||
<svg style="height: 80vmin; width: 80vmin;">
|
|
||||||
<path d="M 32 222 Q 128 222, 128 0 Q 128 222, 224 222 L 224 224 L 32 224"/>
|
|
||||||
|
|
||||||
<circle cx="64" cy="128" r="64" fill="#8da8b0"/>
|
|
||||||
<circle cx="128" cy="64" r="64" fill="#71368a"/>
|
|
||||||
<circle cx="192" cy="128" r="64" fill="#fa8508"/>
|
|
||||||
</svg>
|
|
||||||
</modal>
|
|
||||||
<tree :append="true" />`,
|
|
||||||
name: "Tree"
|
|
||||||
} as RawLayer;
|
|
||||||
|
|
||||||
export const getInitialLayers = (
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
||||||
playerData: Partial<PlayerData>
|
|
||||||
): Array<RawLayer> => [main, f, c, a, g, h, spook, demoLayer, demoInfinityLayer];
|
|
||||||
|
|
||||||
export function getStartingData(): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
points: new Decimal(10)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasWon = computed(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const pointGain = computed(() => {
|
|
||||||
if (!hasUpgrade("c", 11)) return new Decimal(0);
|
|
||||||
let gain = new Decimal(3.19);
|
|
||||||
if (hasUpgrade("c", 12)) gain = gain.times(upgradeEffect("c", 12) as Decimal);
|
|
||||||
if (hasMilestone("p", 0)) gain = gain.plus(0.01);
|
|
||||||
if (hasMilestone("p", 4)) {
|
|
||||||
if (hasUpgrade("p", 12)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 13)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 14)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 21)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 22)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 23)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 31)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 32)) gain = gain.plus(0.1);
|
|
||||||
if (hasUpgrade("p", 33)) gain = gain.plus(0.1);
|
|
||||||
}
|
|
||||||
if (hasUpgrade("p", 11))
|
|
||||||
gain = gain.plus(
|
|
||||||
hasUpgrade("p", 34)
|
|
||||||
? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
|
|
||||||
: 1
|
|
||||||
);
|
|
||||||
if (hasUpgrade("p", 12))
|
|
||||||
gain = gain.times(
|
|
||||||
hasUpgrade("p", 34)
|
|
||||||
? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
|
|
||||||
: 1
|
|
||||||
);
|
|
||||||
if (hasUpgrade("p", 13))
|
|
||||||
gain = gain.pow(
|
|
||||||
hasUpgrade("p", 34)
|
|
||||||
? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
|
|
||||||
: 1
|
|
||||||
);
|
|
||||||
if (hasUpgrade("p", 14))
|
|
||||||
gain = gain.tetrate(
|
|
||||||
hasUpgrade("p", 34)
|
|
||||||
? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal).toNumber()
|
|
||||||
: 1
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasUpgrade("p", 71)) gain = gain.plus(1.1);
|
|
||||||
if (hasUpgrade("p", 72)) gain = gain.times(1.1);
|
|
||||||
if (hasUpgrade("p", 73)) gain = gain.pow(1.1);
|
|
||||||
if (hasUpgrade("p", 74)) gain = gain.tetrate(1.1);
|
|
||||||
if (hasMilestone("p", 5) && !inChallenge("p", 22)) {
|
|
||||||
const asdf = hasUpgrade("p", 132)
|
|
||||||
? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(2))
|
|
||||||
: hasUpgrade("p", 101)
|
|
||||||
? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(3))
|
|
||||||
: hasUpgrade("p", 93)
|
|
||||||
? (player.layers.p.gp as Decimal).plus(1).pow(0.2)
|
|
||||||
: (player.layers.p.gp as Decimal).plus(1).log10();
|
|
||||||
gain = gain.plus(asdf);
|
|
||||||
if (hasUpgrade("p", 213)) gain = gain.mul(asdf.plus(1));
|
|
||||||
}
|
|
||||||
if (hasUpgrade("p", 104)) gain = gain.times(player.layers.p.points.plus(1).pow(0.5));
|
|
||||||
if (hasUpgrade("p", 142)) gain = gain.times(5);
|
|
||||||
if (player.layers.i.unlocked)
|
|
||||||
gain = gain.times(player.layers.i.points.plus(1).pow(hasUpgrade("p", 235) ? 6.942 : 1));
|
|
||||||
if (inChallenge("p", 11) || inChallenge("p", 21))
|
|
||||||
gain = new Decimal(10).pow(gain.log10().pow(0.75));
|
|
||||||
if (inChallenge("p", 12) || inChallenge("p", 21))
|
|
||||||
gain = gain.pow(new Decimal(1).sub(new Decimal(1).div(getBuyableAmount("p", 11)!.plus(1))));
|
|
||||||
if (hasUpgrade("p", 211)) gain = gain.times(getBuyableAmount("p", 21)!.plus(1));
|
|
||||||
if (hasMilestone("p", 13)) gain = gain.times(layers.p.buyables!.data[31].effect as Decimal);
|
|
||||||
if (hasMilestone("p", 13)) gain = gain.pow(layers.p.buyables!.data[42].effect as Decimal);
|
|
||||||
return gain;
|
|
||||||
});
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
export function update(delta: Decimal): void {}
|
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
||||||
export function fixOldSave(
|
|
||||||
oldVersion: string | undefined,
|
|
||||||
playerData: Partial<PlayerData>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
): void {}
|
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
112
src/data/mod.tsx
Normal file
112
src/data/mod.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { createResource, trackBest, trackOOMPS, trackTotal } from "@/features/resource";
|
||||||
|
import { createTree, GenericTree } from "@/features/tree";
|
||||||
|
import { globalBus } from "@/game/events";
|
||||||
|
import { createLayer, GenericLayer } from "@/game/layers";
|
||||||
|
import player, { PlayerData } from "@/game/player";
|
||||||
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
|
import Decimal, { format, formatSmall, formatTime } from "@/util/bignum";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import a from "./layers/aca/a";
|
||||||
|
import c, {
|
||||||
|
generatorUpgrade,
|
||||||
|
lollipopMultiplierEffect,
|
||||||
|
lollipopMultiplierUpgrade
|
||||||
|
} from "./layers/aca/c";
|
||||||
|
import f from "./layers/aca/f";
|
||||||
|
|
||||||
|
export const points = createResource<DecimalSource>(0);
|
||||||
|
const best = trackBest(points);
|
||||||
|
const total = trackTotal(points);
|
||||||
|
const oomps = trackOOMPS(points);
|
||||||
|
|
||||||
|
const pointGain = computed(() => {
|
||||||
|
if (!generatorUpgrade.bought) return new Decimal(0);
|
||||||
|
let gain = new Decimal(3.19);
|
||||||
|
if (lollipopMultiplierUpgrade.bought) gain = gain.times(lollipopMultiplierEffect.value);
|
||||||
|
return gain;
|
||||||
|
});
|
||||||
|
globalBus.on("update", diff => {
|
||||||
|
points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Casting as generic tree to avoid recursive type definitions
|
||||||
|
export const tree = createTree({
|
||||||
|
nodes: () => [[c.treeNode], [f.treeNode, c.spook]],
|
||||||
|
leftSideNodes: [a.treeNode, c.h],
|
||||||
|
branches: [
|
||||||
|
{
|
||||||
|
startNode: f.treeNode,
|
||||||
|
endNode: c.treeNode,
|
||||||
|
stroke: "blue",
|
||||||
|
"stroke-width": "25px",
|
||||||
|
style: {
|
||||||
|
filter: "blur(5px)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ startNode: c.treeNode, endNode: c.g }
|
||||||
|
]
|
||||||
|
}) as GenericTree;
|
||||||
|
|
||||||
|
// Note: layers don't _need_ a reference to everything, but I'd recommend it over trying to remember
|
||||||
|
// what does and doesn't need to be included. Officially all you need are anything with persistency
|
||||||
|
export const main = createLayer({
|
||||||
|
id: "main",
|
||||||
|
name: "Tree",
|
||||||
|
links: tree.links,
|
||||||
|
display() {
|
||||||
|
return (
|
||||||
|
<template>
|
||||||
|
<div v-if={player.devSpeed === 0}>Game Paused</div>
|
||||||
|
<div v-else-if={player.devSpeed && player.devSpeed !== 1}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Dev Speed: {format(player.devSpeed!)}x
|
||||||
|
</div>
|
||||||
|
<div v-if={player.offlineTime != undefined}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||||
|
Offline Time: {formatTime(player.offlineTime!)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-if={Decimal.lt(points.value, "1e1000")}>You have </span>
|
||||||
|
<h2>{format(points.value)}</h2>
|
||||||
|
<span v-if={Decimal.lt(points.value, "1e1e6")}> points</span>
|
||||||
|
</div>
|
||||||
|
<div v-if={Decimal.gt(pointGain.value, 0)}>
|
||||||
|
({oomps.value === "" ? formatSmall(pointGain.value) : oomps.value}/sec)
|
||||||
|
</div>
|
||||||
|
<spacer />
|
||||||
|
<modal show={false}>
|
||||||
|
<svg style="height: 80vmin; width: 80vmin;">
|
||||||
|
<path d="M 32 222 Q 128 222, 128 0 Q 128 222, 224 222 L 224 224 L 32 224" />
|
||||||
|
|
||||||
|
<circle cx="64" cy="128" r="64" fill="#8da8b0" />
|
||||||
|
<circle cx="128" cy="64" r="64" fill="#71368a" />
|
||||||
|
<circle cx="192" cy="128" r="64" fill="#fa8508" />
|
||||||
|
</svg>
|
||||||
|
</modal>
|
||||||
|
<tree {...tree} />
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
points,
|
||||||
|
best,
|
||||||
|
total,
|
||||||
|
oomps,
|
||||||
|
tree
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getInitialLayers = (
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||||
|
player: Partial<PlayerData>
|
||||||
|
): Array<GenericLayer> => [main, f, c, a];
|
||||||
|
|
||||||
|
export const hasWon = computed(() => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
export function fixOldSave(
|
||||||
|
oldVersion: string | undefined,
|
||||||
|
player: Partial<PlayerData>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
): void {}
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
@ -1,4 +1,35 @@
|
||||||
import { Theme } from "@/typings/theme";
|
interface ThemeVars {
|
||||||
|
"--foreground": string;
|
||||||
|
"--background": string;
|
||||||
|
"--feature-foreground": string;
|
||||||
|
"--tooltip-background": string;
|
||||||
|
"--raised-background": string;
|
||||||
|
"--points": string;
|
||||||
|
"--locked": string;
|
||||||
|
"--highlighted": string;
|
||||||
|
"--bought": string;
|
||||||
|
"--danger": string;
|
||||||
|
"--link": string;
|
||||||
|
"--outline": string;
|
||||||
|
"--accent1": string;
|
||||||
|
"--accent2": string;
|
||||||
|
"--accent3": string;
|
||||||
|
"--border-radius": string;
|
||||||
|
"--modal-border": string;
|
||||||
|
"--feature-margin": string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Theme {
|
||||||
|
variables: ThemeVars;
|
||||||
|
stackedInfoboxes: boolean;
|
||||||
|
floatingTabs: boolean;
|
||||||
|
showSingleTab: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@vue/runtime-dom" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface CSSProperties extends Partial<ThemeVars> {}
|
||||||
|
}
|
||||||
|
|
||||||
const defaultTheme: Theme = {
|
const defaultTheme: Theme = {
|
||||||
variables: {
|
variables: {
|
||||||
|
|
137
src/features/achievement.tsx
Normal file
137
src/features/achievement.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import AchievementComponent from "@/components/features/Achievement.vue";
|
||||||
|
import {
|
||||||
|
CoercableComponent,
|
||||||
|
Component,
|
||||||
|
findFeatures,
|
||||||
|
getUniqueID,
|
||||||
|
makePersistent,
|
||||||
|
Persistent,
|
||||||
|
Replace,
|
||||||
|
setDefault,
|
||||||
|
StyleValue,
|
||||||
|
Visibility
|
||||||
|
} from "@/features/feature";
|
||||||
|
import { globalBus } from "@/game/events";
|
||||||
|
import "@/game/notifications";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { createProxy } from "@/util/proxies";
|
||||||
|
import { coerceComponent } from "@/util/vue";
|
||||||
|
import { Unsubscribe } from "nanoevents";
|
||||||
|
import { Ref, unref } from "vue";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
|
||||||
|
export const AchievementType = Symbol("Achievement");
|
||||||
|
|
||||||
|
export interface AchievementOptions {
|
||||||
|
visibility?: Computable<Visibility>;
|
||||||
|
shouldEarn?: Computable<boolean>;
|
||||||
|
display?: Computable<CoercableComponent>;
|
||||||
|
mark?: Computable<boolean | string>;
|
||||||
|
image?: Computable<string>;
|
||||||
|
style?: Computable<StyleValue>;
|
||||||
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
tooltip?: Computable<CoercableComponent>;
|
||||||
|
onComplete?: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseAchievement extends Persistent<boolean> {
|
||||||
|
id: string;
|
||||||
|
earned: Ref<boolean>;
|
||||||
|
complete: VoidFunction;
|
||||||
|
type: typeof AchievementType;
|
||||||
|
[Component]: typeof AchievementComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Achievement<T extends AchievementOptions> = Replace<
|
||||||
|
T & BaseAchievement,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
shouldEarn: GetComputableType<T["shouldEarn"]>;
|
||||||
|
display: GetComputableType<T["display"]>;
|
||||||
|
mark: GetComputableType<T["mark"]>;
|
||||||
|
image: GetComputableType<T["image"]>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
tooltip: GetComputableTypeWithDefault<T["tooltip"], GetComputableType<T["display"]>>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericAchievement = Replace<
|
||||||
|
Achievement<AchievementOptions>,
|
||||||
|
{
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createAchievement<T extends AchievementOptions>(
|
||||||
|
options: T & ThisType<Achievement<T>>
|
||||||
|
): Achievement<T> {
|
||||||
|
const achievement: T & Partial<BaseAchievement> = options;
|
||||||
|
makePersistent<boolean>(achievement, false);
|
||||||
|
achievement.id = getUniqueID("achievement-");
|
||||||
|
achievement.type = AchievementType;
|
||||||
|
achievement[Component] = AchievementComponent;
|
||||||
|
|
||||||
|
achievement.earned = achievement.state;
|
||||||
|
achievement.complete = function() {
|
||||||
|
proxy.state.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
processComputable(achievement as T, "visibility");
|
||||||
|
setDefault(achievement, "visibility", Visibility.Visible);
|
||||||
|
processComputable(achievement as T, "shouldEarn");
|
||||||
|
processComputable(achievement as T, "display");
|
||||||
|
processComputable(achievement as T, "mark");
|
||||||
|
processComputable(achievement as T, "image");
|
||||||
|
processComputable(achievement as T, "style");
|
||||||
|
processComputable(achievement as T, "classes");
|
||||||
|
processComputable(achievement as T, "tooltip");
|
||||||
|
setDefault(achievement, "tooltip", achievement.display);
|
||||||
|
|
||||||
|
const proxy = createProxy((achievement as unknown) as Achievement<T>);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const listeners: Record<string, Unsubscribe> = {};
|
||||||
|
globalBus.on("addLayer", layer => {
|
||||||
|
const achievements: GenericAchievement[] = (findFeatures(
|
||||||
|
layer,
|
||||||
|
AchievementType
|
||||||
|
) as GenericAchievement[]).filter(ach => ach.shouldEarn != null);
|
||||||
|
if (achievements.length) {
|
||||||
|
listeners[layer.id] = layer.on("postUpdate", () => {
|
||||||
|
achievements.forEach(achievement => {
|
||||||
|
if (
|
||||||
|
unref(achievement.visibility) === Visibility.Visible &&
|
||||||
|
!unref(achievement.earned) &&
|
||||||
|
unref(achievement.shouldEarn)
|
||||||
|
) {
|
||||||
|
achievement.state.value = true;
|
||||||
|
achievement.onComplete?.();
|
||||||
|
if (achievement.display) {
|
||||||
|
const display = unref(achievement.display);
|
||||||
|
toast.info(
|
||||||
|
<template>
|
||||||
|
<h2>Milestone earned!</h2>
|
||||||
|
<div>{coerceComponent(display)}</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
globalBus.on("removeLayer", layer => {
|
||||||
|
// unsubscribe from postUpdate
|
||||||
|
listeners[layer.id]?.();
|
||||||
|
delete listeners[layer.id];
|
||||||
|
});
|
102
src/features/bar.ts
Normal file
102
src/features/bar.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import BarComponent from "@/components/features/Bar.vue";
|
||||||
|
import {
|
||||||
|
CoercableComponent,
|
||||||
|
Component,
|
||||||
|
getUniqueID,
|
||||||
|
Replace,
|
||||||
|
setDefault,
|
||||||
|
StyleValue,
|
||||||
|
Visibility
|
||||||
|
} from "@/features/feature";
|
||||||
|
import { DecimalSource } from "@/lib/break_eternity";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { createProxy } from "@/util/proxies";
|
||||||
|
|
||||||
|
export const BarType = Symbol("Bar");
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
Up = "Up",
|
||||||
|
Down = "Down",
|
||||||
|
Left = "Left",
|
||||||
|
Right = "Right",
|
||||||
|
Default = "Up"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BarOptions {
|
||||||
|
visibility?: Computable<Visibility>;
|
||||||
|
width: Computable<number>;
|
||||||
|
height: Computable<number>;
|
||||||
|
direction: Computable<Direction>;
|
||||||
|
style?: Computable<StyleValue>;
|
||||||
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
borderStyle?: Computable<StyleValue>;
|
||||||
|
baseStyle?: Computable<StyleValue>;
|
||||||
|
textStyle?: Computable<StyleValue>;
|
||||||
|
fillStyle?: Computable<StyleValue>;
|
||||||
|
progress: Computable<DecimalSource>;
|
||||||
|
display?: Computable<CoercableComponent>;
|
||||||
|
mark?: Computable<boolean | string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseBar {
|
||||||
|
id: string;
|
||||||
|
type: typeof BarType;
|
||||||
|
[Component]: typeof BarComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Bar<T extends BarOptions> = Replace<
|
||||||
|
T & BaseBar,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
width: GetComputableType<T["width"]>;
|
||||||
|
height: GetComputableType<T["height"]>;
|
||||||
|
direction: GetComputableType<T["direction"]>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
borderStyle: GetComputableType<T["borderStyle"]>;
|
||||||
|
baseStyle: GetComputableType<T["baseStyle"]>;
|
||||||
|
textStyle: GetComputableType<T["textStyle"]>;
|
||||||
|
fillStyle: GetComputableType<T["fillStyle"]>;
|
||||||
|
progress: GetComputableType<T["progress"]>;
|
||||||
|
display: GetComputableType<T["display"]>;
|
||||||
|
mark: GetComputableType<T["mark"]>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericBar = Replace<
|
||||||
|
Bar<BarOptions>,
|
||||||
|
{
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createBar<T extends BarOptions>(options: T & ThisType<Bar<T>>): Bar<T> {
|
||||||
|
const bar: T & Partial<BaseBar> = options;
|
||||||
|
bar.id = getUniqueID("bar-");
|
||||||
|
bar.type = BarType;
|
||||||
|
bar[Component] = BarComponent;
|
||||||
|
|
||||||
|
processComputable(bar as T, "visibility");
|
||||||
|
setDefault(bar, "visibility", Visibility.Visible);
|
||||||
|
processComputable(bar as T, "width");
|
||||||
|
processComputable(bar as T, "height");
|
||||||
|
processComputable(bar as T, "direction");
|
||||||
|
processComputable(bar as T, "style");
|
||||||
|
processComputable(bar as T, "classes");
|
||||||
|
processComputable(bar as T, "borderStyle");
|
||||||
|
processComputable(bar as T, "baseStyle");
|
||||||
|
processComputable(bar as T, "textStyle");
|
||||||
|
processComputable(bar as T, "fillStyle");
|
||||||
|
processComputable(bar as T, "progress");
|
||||||
|
processComputable(bar as T, "display");
|
||||||
|
processComputable(bar as T, "mark");
|
||||||
|
|
||||||
|
const proxy = createProxy((bar as unknown) as Bar<T>);
|
||||||
|
return proxy;
|
||||||
|
}
|
318
src/features/board.ts
Normal file
318
src/features/board.ts
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
import BoardComponent from "@/components/features/board/Board.vue";
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
findFeatures,
|
||||||
|
getUniqueID,
|
||||||
|
makePersistent,
|
||||||
|
Persistent,
|
||||||
|
Replace,
|
||||||
|
setDefault,
|
||||||
|
State,
|
||||||
|
StyleValue,
|
||||||
|
Visibility
|
||||||
|
} from "@/features/feature";
|
||||||
|
import { globalBus } from "@/game/events";
|
||||||
|
import Decimal, { DecimalSource } from "@/lib/break_eternity";
|
||||||
|
import { isFunction } from "@/util/common";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { createProxy } from "@/util/proxies";
|
||||||
|
import { Unsubscribe } from "nanoevents";
|
||||||
|
import { computed, Ref, unref } from "vue";
|
||||||
|
import { Link } from "./links";
|
||||||
|
|
||||||
|
export const BoardType = Symbol("Board");
|
||||||
|
|
||||||
|
export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
|
||||||
|
|
||||||
|
export enum ProgressDisplay {
|
||||||
|
Outline = "Outline",
|
||||||
|
Fill = "Fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Shape {
|
||||||
|
Circle = "Circle",
|
||||||
|
Diamond = "Triangle"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoardNode {
|
||||||
|
id: number;
|
||||||
|
position: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
type: string;
|
||||||
|
state?: State;
|
||||||
|
pinned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> {
|
||||||
|
startNode: BoardNode;
|
||||||
|
endNode: BoardNode;
|
||||||
|
pulsing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeLabel {
|
||||||
|
text: string;
|
||||||
|
color?: string;
|
||||||
|
pulsing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoardData = {
|
||||||
|
nodes: BoardNode[];
|
||||||
|
selectedNode: number | null;
|
||||||
|
selectedAction: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NodeTypeOptions {
|
||||||
|
title: NodeComputable<string>;
|
||||||
|
label?: NodeComputable<NodeLabel | null>;
|
||||||
|
size: NodeComputable<number>;
|
||||||
|
draggable?: NodeComputable<boolean>;
|
||||||
|
shape: NodeComputable<Shape>;
|
||||||
|
canAccept?: boolean | Ref<boolean> | ((node: BoardNode, otherNode: BoardNode) => boolean);
|
||||||
|
progress?: NodeComputable<number>;
|
||||||
|
progressDisplay?: NodeComputable<ProgressDisplay>;
|
||||||
|
progressColor?: NodeComputable<string>;
|
||||||
|
fillColor?: NodeComputable<string>;
|
||||||
|
outlineColor?: NodeComputable<string>;
|
||||||
|
titleColor?: NodeComputable<string>;
|
||||||
|
actions?: BoardNodeActionOptions[];
|
||||||
|
actionDistance?: NodeComputable<number>;
|
||||||
|
onClick?: (node: BoardNode) => void;
|
||||||
|
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
|
||||||
|
update?: (node: BoardNode, diff: DecimalSource) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseNodeType {
|
||||||
|
nodes: Ref<BoardNode[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeType<T extends NodeTypeOptions> = Replace<
|
||||||
|
T & BaseNodeType,
|
||||||
|
{
|
||||||
|
title: GetComputableType<T["title"]>;
|
||||||
|
label: GetComputableType<T["label"]>;
|
||||||
|
size: GetComputableTypeWithDefault<T["size"], 50>;
|
||||||
|
draggable: GetComputableTypeWithDefault<T["draggable"], false>;
|
||||||
|
shape: GetComputableTypeWithDefault<T["shape"], Shape.Circle>;
|
||||||
|
canAccept: GetComputableTypeWithDefault<T["canAccept"], false>;
|
||||||
|
progress: GetComputableType<T["progress"]>;
|
||||||
|
progressDisplay: GetComputableTypeWithDefault<T["progressDisplay"], ProgressDisplay.Fill>;
|
||||||
|
progressColor: GetComputableTypeWithDefault<T["progressColor"], "none">;
|
||||||
|
fillColor: GetComputableType<T["fillColor"]>;
|
||||||
|
outlineColor: GetComputableType<T["outlineColor"]>;
|
||||||
|
titleColor: GetComputableType<T["titleColor"]>;
|
||||||
|
actions?: GenericBoardNodeAction[];
|
||||||
|
actionDistance: GetComputableTypeWithDefault<T["actionDistance"], number>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericNodeType = Replace<
|
||||||
|
NodeType<NodeTypeOptions>,
|
||||||
|
{
|
||||||
|
size: NodeComputable<number>;
|
||||||
|
draggable: NodeComputable<boolean>;
|
||||||
|
shape: NodeComputable<Shape>;
|
||||||
|
canAccept: NodeComputable<boolean>;
|
||||||
|
progressDisplay: NodeComputable<ProgressDisplay>;
|
||||||
|
progressColor: NodeComputable<string>;
|
||||||
|
actionDistance: NodeComputable<number>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface BoardNodeActionOptions {
|
||||||
|
id: string;
|
||||||
|
visibility?: NodeComputable<Visibility>;
|
||||||
|
icon: NodeComputable<string>;
|
||||||
|
fillColor?: NodeComputable<string>;
|
||||||
|
tooltip: NodeComputable<string>;
|
||||||
|
links?: NodeComputable<BoardNodeLink[]>;
|
||||||
|
onClick: (node: BoardNode) => boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseBoardNodeAction {
|
||||||
|
links?: Ref<BoardNodeLink[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
||||||
|
T & BaseBoardNodeAction,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
icon: GetComputableType<T["icon"]>;
|
||||||
|
fillColor: GetComputableType<T["fillColor"]>;
|
||||||
|
tooltip: GetComputableType<T["tooltip"]>;
|
||||||
|
links: GetComputableType<T["links"]>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericBoardNodeAction = Replace<
|
||||||
|
BoardNodeAction<BoardNodeActionOptions>,
|
||||||
|
{
|
||||||
|
visibility: NodeComputable<Visibility>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface BoardOptions {
|
||||||
|
visibility?: Computable<Visibility>;
|
||||||
|
height?: Computable<string>;
|
||||||
|
width?: Computable<string>;
|
||||||
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
style?: Computable<StyleValue>;
|
||||||
|
startNodes: () => Omit<BoardNode, "id">[];
|
||||||
|
types: Record<string, NodeTypeOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseBoard extends Persistent<BoardData> {
|
||||||
|
id: string;
|
||||||
|
links: Ref<BoardNodeLink[] | null>;
|
||||||
|
nodes: Ref<BoardNode[]>;
|
||||||
|
selectedNode: Ref<BoardNode | null>;
|
||||||
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
|
type: typeof BoardType;
|
||||||
|
[Component]: typeof BoardComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Board<T extends BoardOptions> = Replace<
|
||||||
|
T & BaseBoard,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
types: Record<string, GenericNodeType>;
|
||||||
|
height: GetComputableType<T["height"]>;
|
||||||
|
width: GetComputableType<T["width"]>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericBoard = Replace<
|
||||||
|
Board<BoardOptions>,
|
||||||
|
{
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<T>>): Board<T> {
|
||||||
|
const board: T & Partial<BaseBoard> = options;
|
||||||
|
makePersistent<BoardData>(board, {
|
||||||
|
nodes: [],
|
||||||
|
selectedNode: null,
|
||||||
|
selectedAction: null
|
||||||
|
});
|
||||||
|
board.id = getUniqueID("board-");
|
||||||
|
board.type = BoardType;
|
||||||
|
board[Component] = BoardComponent;
|
||||||
|
|
||||||
|
board.nodes = computed(() => proxy.state.value.nodes);
|
||||||
|
board.selectedNode = computed(
|
||||||
|
() => proxy.nodes.value.find(node => node.id === proxy.state.value.selectedNode) || null
|
||||||
|
);
|
||||||
|
board.selectedAction = computed(() => {
|
||||||
|
if (proxy.selectedNode.value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const type = proxy.types[proxy.selectedNode.value.type];
|
||||||
|
if (type.actions == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return type.actions.find(action => action.id === proxy.state.value.selectedAction) || null;
|
||||||
|
});
|
||||||
|
board.links = computed(() => {
|
||||||
|
if (proxy.selectedAction.value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (proxy.selectedAction.value.links && proxy.selectedNode.value) {
|
||||||
|
return getNodeProperty(proxy.selectedAction.value.links, proxy.selectedNode.value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
processComputable(board as T, "visibility");
|
||||||
|
setDefault(board, "visibility", Visibility.Visible);
|
||||||
|
processComputable(board as T, "width");
|
||||||
|
setDefault(board, "width", "100%");
|
||||||
|
processComputable(board as T, "height");
|
||||||
|
setDefault(board, "height", "400px");
|
||||||
|
processComputable(board as T, "classes");
|
||||||
|
processComputable(board as T, "style");
|
||||||
|
|
||||||
|
for (const type in board.types) {
|
||||||
|
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
|
||||||
|
|
||||||
|
processComputable(nodeType, "title");
|
||||||
|
processComputable(nodeType, "label");
|
||||||
|
processComputable(nodeType, "size");
|
||||||
|
setDefault(nodeType, "size", 50);
|
||||||
|
processComputable(nodeType, "draggable");
|
||||||
|
setDefault(nodeType, "draggable", false);
|
||||||
|
processComputable(nodeType, "shape");
|
||||||
|
setDefault(nodeType, "shape", Shape.Circle);
|
||||||
|
processComputable(nodeType, "canAccept");
|
||||||
|
setDefault(nodeType, "canAccept", false);
|
||||||
|
processComputable(nodeType, "progress");
|
||||||
|
processComputable(nodeType, "progressDisplay");
|
||||||
|
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
|
||||||
|
processComputable(nodeType, "progressColor");
|
||||||
|
setDefault(nodeType, "progressColor", "none");
|
||||||
|
processComputable(nodeType, "fillColor");
|
||||||
|
processComputable(nodeType, "outlineColor");
|
||||||
|
processComputable(nodeType, "titleColor");
|
||||||
|
processComputable(nodeType, "actionDistance");
|
||||||
|
setDefault(nodeType, "actionDistance", Math.PI / 6);
|
||||||
|
nodeType.nodes = computed(() => proxy.state.value.nodes.filter(node => node.type === type));
|
||||||
|
setDefault(nodeType, "onClick", function(node: BoardNode) {
|
||||||
|
proxy.state.value.selectedNode = node.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nodeType.actions) {
|
||||||
|
for (const action of nodeType.actions) {
|
||||||
|
processComputable(action, "visibility");
|
||||||
|
setDefault(action, "visibility", Visibility.Visible);
|
||||||
|
processComputable(action, "icon");
|
||||||
|
processComputable(action, "fillColor");
|
||||||
|
processComputable(action, "tooltip");
|
||||||
|
processComputable(action, "links");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
board.types[type] = createProxy((nodeType as unknown) as GenericNodeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = createProxy((board as unknown) as Board<T>);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
|
||||||
|
return isFunction(property) ? property(node) : unref(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUniqueNodeID(board: GenericBoard): number {
|
||||||
|
let id = 0;
|
||||||
|
board.nodes.value.forEach(node => {
|
||||||
|
if (node.id >= id) {
|
||||||
|
id = node.id + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Record<string, Unsubscribe> = {};
|
||||||
|
globalBus.on("addLayer", layer => {
|
||||||
|
const boards: GenericBoard[] = findFeatures(layer, BoardType) as GenericBoard[];
|
||||||
|
listeners[layer.id] = layer.on("postUpdate", (diff: Decimal) => {
|
||||||
|
boards.forEach(board => {
|
||||||
|
Object.values(board.types).forEach(type =>
|
||||||
|
type.nodes.value.forEach(node => type.update?.(node, diff))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
globalBus.on("removeLayer", layer => {
|
||||||
|
// unsubscribe from postUpdate
|
||||||
|
listeners[layer.id]();
|
||||||
|
delete listeners[layer.id];
|
||||||
|
});
|
182
src/features/buyable.tsx
Normal file
182
src/features/buyable.tsx
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import ClickableComponent from "@/components/features/Clickable.vue";
|
||||||
|
import { Resource } from "@/features/resource";
|
||||||
|
import Decimal, { DecimalSource, format } from "@/util/bignum";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { createProxy } from "@/util/proxies";
|
||||||
|
import { isCoercableComponent } from "@/util/vue";
|
||||||
|
import { computed, Ref, unref } from "vue";
|
||||||
|
import {
|
||||||
|
CoercableComponent,
|
||||||
|
Component,
|
||||||
|
getUniqueID,
|
||||||
|
makePersistent,
|
||||||
|
Persistent,
|
||||||
|
Replace,
|
||||||
|
setDefault,
|
||||||
|
StyleValue,
|
||||||
|
Visibility
|
||||||
|
} from "./feature";
|
||||||
|
|
||||||
|
export const BuyableType = Symbol("Buyable");
|
||||||
|
|
||||||
|
type BuyableDisplay =
|
||||||
|
| CoercableComponent
|
||||||
|
| {
|
||||||
|
title?: CoercableComponent;
|
||||||
|
description: CoercableComponent;
|
||||||
|
effectDisplay?: CoercableComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BuyableOptions {
|
||||||
|
visibility?: Computable<Visibility>;
|
||||||
|
cost?: Computable<DecimalSource>;
|
||||||
|
resource?: Computable<Resource>;
|
||||||
|
canPurchase?: Computable<boolean>;
|
||||||
|
purchaseLimit?: Computable<DecimalSource>;
|
||||||
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
style?: Computable<StyleValue>;
|
||||||
|
mark?: Computable<boolean | string>;
|
||||||
|
small?: Computable<boolean>;
|
||||||
|
display?: Computable<BuyableDisplay>;
|
||||||
|
onPurchase?: (cost: DecimalSource) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseBuyable extends Persistent<DecimalSource> {
|
||||||
|
id: string;
|
||||||
|
amount: Ref<DecimalSource>;
|
||||||
|
bought: Ref<boolean>;
|
||||||
|
canAfford: Ref<boolean>;
|
||||||
|
canClick: ProcessedComputable<boolean>;
|
||||||
|
onClick: VoidFunction;
|
||||||
|
purchase: VoidFunction;
|
||||||
|
type: typeof BuyableType;
|
||||||
|
[Component]: typeof ClickableComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Buyable<T extends BuyableOptions> = Replace<
|
||||||
|
T & BaseBuyable,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
cost: GetComputableType<T["cost"]>;
|
||||||
|
resource: GetComputableType<T["resource"]>;
|
||||||
|
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
|
||||||
|
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], 1>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
mark: GetComputableType<T["mark"]>;
|
||||||
|
small: GetComputableType<T["small"]>;
|
||||||
|
display: Ref<CoercableComponent>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericBuyable = Replace<
|
||||||
|
Buyable<BuyableOptions>,
|
||||||
|
{
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
canPurchase: ProcessedComputable<boolean>;
|
||||||
|
purchaseLimit: ProcessedComputable<DecimalSource>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createBuyable<T extends BuyableOptions>(
|
||||||
|
options: T & ThisType<Buyable<T>>
|
||||||
|
): Buyable<T> {
|
||||||
|
if (options.canPurchase == null && (options.resource == null || options.cost == null)) {
|
||||||
|
console.warn(
|
||||||
|
"Cannot create buyable without a canPurchase property or a resource and cost property",
|
||||||
|
options
|
||||||
|
);
|
||||||
|
throw "Cannot create buyable without a canPurchase property or a resource and cost property";
|
||||||
|
}
|
||||||
|
|
||||||
|
const buyable: T & Partial<BaseBuyable> = options;
|
||||||
|
makePersistent<DecimalSource>(buyable, 0);
|
||||||
|
buyable.id = getUniqueID("buyable-");
|
||||||
|
buyable.type = BuyableType;
|
||||||
|
buyable[Component] = ClickableComponent;
|
||||||
|
|
||||||
|
buyable.amount = buyable.state;
|
||||||
|
buyable.bought = computed(() => Decimal.gt(proxy.amount.value, 0));
|
||||||
|
buyable.canAfford = computed(
|
||||||
|
() =>
|
||||||
|
proxy.resource != null &&
|
||||||
|
proxy.cost != null &&
|
||||||
|
Decimal.gte(unref<Resource>(proxy.resource).value, unref(proxy.cost))
|
||||||
|
);
|
||||||
|
if (buyable.canPurchase == null) {
|
||||||
|
buyable.canPurchase = computed(
|
||||||
|
() =>
|
||||||
|
proxy.purchaseLimit != null &&
|
||||||
|
proxy.canAfford &&
|
||||||
|
Decimal.lt(proxy.amount.value, unref(proxy.purchaseLimit))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
processComputable(buyable as T, "canPurchase");
|
||||||
|
// TODO once processComputable typing works, this can be replaced
|
||||||
|
//buyable.canClick = buyable.canPurchase;
|
||||||
|
buyable.canClick = computed(() => unref(proxy.canPurchase));
|
||||||
|
buyable.onClick = buyable.purchase = function() {
|
||||||
|
if (!unref(proxy.canPurchase) || proxy.cost == null || proxy.resource == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cost = unref(proxy.cost);
|
||||||
|
unref<Resource>(proxy.resource).value = Decimal.sub(
|
||||||
|
unref<Resource>(proxy.resource).value,
|
||||||
|
cost
|
||||||
|
);
|
||||||
|
proxy.amount.value = Decimal.add(proxy.amount.value, 1);
|
||||||
|
this.onPurchase?.(cost);
|
||||||
|
};
|
||||||
|
processComputable(buyable as T, "display");
|
||||||
|
const display = buyable.display;
|
||||||
|
buyable.display = computed(() => {
|
||||||
|
// TODO once processComputable types correctly, remove this "as X"
|
||||||
|
const currDisplay = unref(display) as BuyableDisplay;
|
||||||
|
if (
|
||||||
|
currDisplay != null &&
|
||||||
|
!isCoercableComponent(currDisplay) &&
|
||||||
|
proxy.cost != null &&
|
||||||
|
proxy.resource != null
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<div v-if={currDisplay.title}>
|
||||||
|
<component v-is={currDisplay.title} />
|
||||||
|
</div>
|
||||||
|
<component v-is={currDisplay.description} />
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
Amount: {format(proxy.amount.value)} / {format(unref(proxy.purchaseLimit))}
|
||||||
|
</div>
|
||||||
|
<div v-if={currDisplay.effectDisplay}>
|
||||||
|
<br />
|
||||||
|
Currently: <component v-is={currDisplay.effectDisplay} />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
Cost: {format(unref(proxy.cost))} {unref<Resource>(proxy.resource).displayName}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
processComputable(buyable as T, "visibility");
|
||||||
|
setDefault(buyable, "visibility", Visibility.Visible);
|
||||||
|
processComputable(buyable as T, "cost");
|
||||||
|
processComputable(buyable as T, "resource");
|
||||||
|
processComputable(buyable as T, "purchaseLimit");
|
||||||
|
setDefault(buyable, "purchaseLimit", 1);
|
||||||
|
processComputable(buyable as T, "classes");
|
||||||
|
processComputable(buyable as T, "style");
|
||||||
|
processComputable(buyable as T, "mark");
|
||||||
|
processComputable(buyable as T, "small");
|
||||||
|
|
||||||
|
const proxy = createProxy((buyable as unknown) as Buyable<T>);
|
||||||
|
return proxy;
|
||||||
|
}
|
203
src/features/challenge.ts
Normal file
203
src/features/challenge.ts
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
import ChallengeComponent from "@/components/features/Challenge.vue";
|
||||||
|
import {
|
||||||
|
CoercableComponent,
|
||||||
|
Component,
|
||||||
|
getUniqueID,
|
||||||
|
persistent,
|
||||||
|
PersistentRef,
|
||||||
|
Replace,
|
||||||
|
setDefault,
|
||||||
|
StyleValue,
|
||||||
|
Visibility
|
||||||
|
} from "@/features/feature";
|
||||||
|
import { Resource } from "@/features/resource";
|
||||||
|
import { globalBus } from "@/game/events";
|
||||||
|
import settings from "@/game/settings";
|
||||||
|
import Decimal, { DecimalSource } from "@/util/bignum";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
GetComputableTypeWithDefault,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "@/util/computed";
|
||||||
|
import { createProxy } from "@/util/proxies";
|
||||||
|
import { computed, Ref, unref } from "vue";
|
||||||
|
import { GenericReset } from "./reset";
|
||||||
|
|
||||||
|
export const ChallengeType = Symbol("ChallengeType");
|
||||||
|
|
||||||
|
export interface ChallengeOptions {
|
||||||
|
visibility?: Computable<Visibility>;
|
||||||
|
canStart?: Computable<boolean>;
|
||||||
|
reset?: GenericReset;
|
||||||
|
canComplete?: Computable<boolean | DecimalSource>;
|
||||||
|
completionLimit?: Computable<DecimalSource>;
|
||||||
|
mark?: Computable<boolean | string>;
|
||||||
|
resource?: Computable<Resource>;
|
||||||
|
goal?: Computable<DecimalSource>;
|
||||||
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
style?: Computable<StyleValue>;
|
||||||
|
display?: Computable<
|
||||||
|
| CoercableComponent
|
||||||
|
| {
|
||||||
|
title?: CoercableComponent;
|
||||||
|
description: CoercableComponent;
|
||||||
|
goal?: CoercableComponent;
|
||||||
|
reward?: CoercableComponent;
|
||||||
|
effectDisplay?: CoercableComponent;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
onComplete?: VoidFunction;
|
||||||
|
onExit?: VoidFunction;
|
||||||
|
onEnter?: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseChallenge {
|
||||||
|
id: string;
|
||||||
|
completions: PersistentRef<DecimalSource>;
|
||||||
|
completed: Ref<boolean>;
|
||||||
|
maxed: Ref<boolean>;
|
||||||
|
active: PersistentRef<boolean>;
|
||||||
|
toggle: VoidFunction;
|
||||||
|
type: typeof ChallengeType;
|
||||||
|
[Component]: typeof ChallengeComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Challenge<T extends ChallengeOptions> = Replace<
|
||||||
|
T & BaseChallenge,
|
||||||
|
{
|
||||||
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
|
canStart: GetComputableTypeWithDefault<T["canStart"], Ref<boolean>>;
|
||||||
|
canComplete: GetComputableTypeWithDefault<T["canComplete"], Ref<boolean>>;
|
||||||
|
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
||||||
|
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
||||||
|
resource: GetComputableType<T["resource"]>;
|
||||||
|
goal: GetComputableType<T["goal"]>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
display: GetComputableType<T["display"]>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericChallenge = Replace<
|
||||||
|
Challenge<ChallengeOptions>,
|
||||||
|
{
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
canStart: ProcessedComputable<boolean>;
|
||||||
|
canComplete: ProcessedComputable<boolean>;
|
||||||
|
completionLimit: ProcessedComputable<DecimalSource>;
|
||||||
|
mark: ProcessedComputable<boolean>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function createActiveChallenge(
|
||||||
|
challenges: GenericChallenge[]
|
||||||
|
): Ref<GenericChallenge | undefined> {
|
||||||
|
return computed(() => challenges.find(challenge => challenge.active.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createChallenge<T extends ChallengeOptions>(
|
||||||
|
options: T & ThisType<Challenge<T>>
|
||||||
|
): Challenge<T> {
|
||||||
|
if (options.canComplete == null && (options.resource == null || options.goal == null)) {
|
||||||
|
console.warn(
|
||||||
|
"Cannot create challenge without a canComplete property or a resource and goal property",
|
||||||
|
options
|
||||||
|
);
|
||||||
|
throw "Cannot create challenge without a canComplete property or a resource and goal property";
|
||||||
|
}
|
||||||
|
|
||||||
|
const challenge: T & Partial<BaseChallenge> = options;
|
||||||
|
challenge.id = getUniqueID("challenge-");
|
||||||
|
challenge.type = ChallengeType;
|
||||||
|
challenge[Component] = ChallengeComponent;
|
||||||
|
|
||||||
|
challenge.completions = persistent(0);
|
||||||
|
challenge.active = persistent(false);
|
||||||
|
challenge.completed = computed(() => Decimal.gt(proxy.completions.value, 0));
|
||||||
|
challenge.maxed = computed(() =>
|
||||||
|
Decimal.gte(proxy.completions.value, unref(proxy.completionLimit))
|
||||||
|
);
|
||||||
|
challenge.toggle = function() {
|
||||||
|
if (proxy.active.value) {
|
||||||
|
if (proxy.canComplete && unref(proxy.canComplete) && !proxy.maxed.value) {
|
||||||
|
let completions: boolean | DecimalSource = unref(proxy.canComplete);
|
||||||
|
if (typeof completions === "boolean") {
|
||||||
|
completions = 1;
|
||||||
|
}
|
||||||
|
proxy.completions.value = Decimal.min(
|
||||||
|
Decimal.add(proxy.completions.value, completions),
|
||||||
|
unref(proxy.completionLimit)
|
||||||
|
);
|
||||||
|
proxy.onComplete?.();
|
||||||
|
}
|
||||||
|
proxy.active.value = false;
|
||||||
|
proxy.onExit?.();
|
||||||
|
proxy.reset?.reset();
|
||||||
|
} else if (unref(proxy.canStart)) {
|
||||||
|
proxy.reset?.reset();
|
||||||
|
proxy.active.value = true;
|
||||||
|
proxy.onEnter?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
processComputable(challenge as T, "visibility");
|
||||||
|
setDefault(challenge, "visibility", Visibility.Visible);
|
||||||
|
const visibility = challenge.visibility as ProcessedComputable<Visibility>;
|
||||||
|
challenge.visibility = computed(() => {
|
||||||
|
if (settings.hideChallenges === true && unref(proxy.maxed)) {
|
||||||
|
return Visibility.None;
|
||||||
|
}
|
||||||
|
return unref(visibility);
|
||||||
|
});
|
||||||
|
if (challenge.canStart == null) {
|
||||||
|
challenge.canStart = computed(() =>
|
||||||
|
Decimal.lt(proxy.completions.value, unref(proxy.completionLimit))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (challenge.canComplete == null) {
|
||||||
|
challenge.canComplete = computed(() => {
|
||||||
|
if (!proxy.active.value || proxy.resource == null || proxy.goal == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Decimal.gte(unref<Resource>(proxy.resource).value, unref(proxy.goal));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (challenge.mark == null) {
|
||||||
|
challenge.mark = computed(
|
||||||
|
() => Decimal.gt(unref(proxy.completionLimit), 1) && unref(proxy.maxed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
processComputable(challenge as T, "canStart");
|
||||||
|
processComputable(challenge as T, "canComplete");
|
||||||
|
processComputable(challenge as T, "completionLimit");
|
||||||
|
setDefault(challenge, "completionLimit", 1);
|
||||||
|
processComputable(challenge as T, "mark");
|
||||||
|
processComputable(challenge as T, "resource");
|
||||||
|
processComputable(challenge as T, "goal");
|
||||||
|
processComputable(challenge as T, "classes");
|
||||||
|
processComputable(challenge as T, "style");
|
||||||
|
processComputable(challenge as T, "display");
|
||||||
|
|
||||||
|
if (challenge.reset != null) {
|
||||||
|
globalBus.on("reset", currentReset => {
|
||||||
|
if (currentReset === challenge.reset && (challenge.active as Ref<boolean>).value) {
|
||||||
|
(challenge.toggle as VoidFunction)();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = createProxy((challenge as unknown) as Challenge<T>);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@/game/settings" {
|
||||||
|
interface Settings {
|
||||||
|
hideChallenges: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalBus.on("loadSettings", settings => {
|
||||||
|
setDefault(settings, "hideChallenges", false);
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue