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 = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
'vue/setup-compiler-macros': true
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
|
@ -15,6 +16,8 @@ module.exports = {
|
|||
},
|
||||
rules: {
|
||||
"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
|
||||
}
|
||||
]
|
||||
],
|
||||
"@vue/babel-plugin-jsx"
|
||||
]
|
||||
};
|
||||
|
|
21781
package-lock.json
generated
21781
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": {
|
||||
"core-js": "^3.6.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"vue": "^3.2.2",
|
||||
"vue-class-component": "^8.0.0-rc.1",
|
||||
"vue-next-select": "^2.9.0",
|
||||
"nanoevents": "^6.0.2",
|
||||
"vue": "^3.2.26",
|
||||
"vue-next-select": "^2.10.2",
|
||||
"vue-panzoom": "^1.1.6",
|
||||
"vue-sortable": "github:Netbel/vue-sortable#master-fix",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
|
@ -23,25 +23,26 @@
|
|||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"@jetblack/operator-overloading": "^0.2.0",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "^4.18.0",
|
||||
"@typescript-eslint/parser": "^4.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~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-typescript": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint": "^8.6.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",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.36.0",
|
||||
"sass-loader": "^10.2.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.1",
|
||||
"typescript": "~4.1.5"
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
|
35
src/App.vue
35
src/App.vue
|
@ -2,38 +2,31 @@
|
|||
<div id="modal-root" :style="theme" />
|
||||
<div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
|
||||
<Nav v-if="useHeader" />
|
||||
<Tabs />
|
||||
<Game />
|
||||
<TPS v-if="showTPS" />
|
||||
<GameOverScreen />
|
||||
<NaNScreen />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
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 themes from "./data/themes";
|
||||
import settings from "./game/settings";
|
||||
import "./main.css";
|
||||
import { mapSettings } from "./util/vue";
|
||||
|
||||
export default defineComponent({
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
function updateMouse(/* event */) {
|
||||
// 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>
|
||||
|
||||
<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 button {
|
||||
background-color: var(--layer-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,65 +1,40 @@
|
|||
<template>
|
||||
<tooltip v-if="achievement.unlocked" :display="tooltip">
|
||||
<Tooltip
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
:display="tooltip"
|
||||
>
|
||||
<div
|
||||
:style="style"
|
||||
:style="[{ backgroundImage: (earned && image && `url(${image})`) || '' }, style ?? []]"
|
||||
:class="{
|
||||
[layer]: true,
|
||||
feature: true,
|
||||
achievement: true,
|
||||
locked: !achievement.earned,
|
||||
bought: achievement.earned
|
||||
locked: !earned,
|
||||
bought: earned,
|
||||
...classes
|
||||
}"
|
||||
>
|
||||
<component v-if="display" :is="display" />
|
||||
<branch-node :branches="achievement.branches" :id="id" featureType="achievement" />
|
||||
<component v-if="component" :is="component" />
|
||||
<MarkNode :mark="mark" />
|
||||
<LinkNode :id="id" />
|
||||
</div>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { CoercableComponent } from "@/typings/component";
|
||||
import { Achievement } from "@/typings/features/achievement";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { GenericAchievement } from "@/features/achievement";
|
||||
import { FeatureComponent } from "@/features/feature";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
import MarkNode from "./MarkNode.vue";
|
||||
import { Visibility } from "@/features/feature";
|
||||
|
||||
export default defineComponent({
|
||||
name: "achievement",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
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";
|
||||
}
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericAchievement>>());
|
||||
|
||||
const component = computed(() => {
|
||||
const display = props.display.value;
|
||||
return display && coerceComponent(display);
|
||||
});
|
||||
</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>
|
||||
<div v-if="bar.unlocked" :style="style" :class="{ [layer]: true, bar: true }">
|
||||
<div class="overlayTextContainer border" :style="borderStyle">
|
||||
<component class="overlayText" :style="textStyle" :is="display" />
|
||||
<div
|
||||
v-if="visibility !== Visibility.None"
|
||||
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 class="border" :style="backgroundStyle">
|
||||
<div class="fill" :style="fillStyle" />
|
||||
<div
|
||||
class="border"
|
||||
:style="[
|
||||
{ width: width + 'px', height: height + 'px' },
|
||||
style ?? {},
|
||||
baseStyle ?? {},
|
||||
borderStyle ?? {}
|
||||
]"
|
||||
>
|
||||
<div class="fill" :style="[barStyle, style ?? {}, fillStyle ?? {}]" />
|
||||
</div>
|
||||
<branch-node :branches="bar.branches" :id="id" featureType="bar" />
|
||||
<MarkNode :mark="mark" />
|
||||
<LinkNode :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Direction } from "@/game/enums";
|
||||
import { layers } from "@/game/layers";
|
||||
import { Bar } from "@/typings/features/bar";
|
||||
<script setup lang="ts">
|
||||
import { Direction, GenericBar } from "@/features/bar";
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import Decimal from "@/util/bignum";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { computed, CSSProperties, toRefs, unref } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
import MarkNode from "./MarkNode.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "bar",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bar(): Bar {
|
||||
return layers[this.layer].bars!.data[this.id];
|
||||
},
|
||||
progress(): number {
|
||||
let progress =
|
||||
this.bar.progress instanceof Decimal
|
||||
? this.bar.progress.toNumber()
|
||||
: (this.bar.progress as number);
|
||||
return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
|
||||
},
|
||||
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
|
||||
return [
|
||||
{ width: this.bar.width + "px", height: this.bar.height + "px" },
|
||||
layers[this.layer].componentStyles?.bar,
|
||||
this.bar.style
|
||||
];
|
||||
},
|
||||
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) {
|
||||
case Direction.Up:
|
||||
fillStyle.clipPath = `inset(${this.progress}% 0% 0% 0%)`;
|
||||
fillStyle.width = this.bar.width + 1 + "px";
|
||||
break;
|
||||
case Direction.Down:
|
||||
fillStyle.clipPath = `inset(0% 0% ${this.progress}% 0%)`;
|
||||
fillStyle.width = this.bar.width + 1 + "px";
|
||||
break;
|
||||
case Direction.Right:
|
||||
fillStyle.clipPath = `inset(0% ${this.progress}% 0% 0%)`;
|
||||
break;
|
||||
case Direction.Left:
|
||||
fillStyle.clipPath = `inset(0% 0% 0% ${this.progress} + '%)`;
|
||||
break;
|
||||
case Direction.Default:
|
||||
fillStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||
break;
|
||||
}
|
||||
return [fillStyle, this.bar.style, this.bar.fillStyle];
|
||||
},
|
||||
display(): Component | string {
|
||||
return coerceComponent(this.bar.display);
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericBar>>());
|
||||
|
||||
const normalizedProgress = computed(() => {
|
||||
let progress =
|
||||
props.progress.value instanceof Decimal
|
||||
? props.progress.value.toNumber()
|
||||
: Number(props.progress.value);
|
||||
return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
|
||||
});
|
||||
|
||||
const barStyle = computed(() => {
|
||||
const barStyle: Partial<CSSProperties> = {
|
||||
width: unref(props.width) + 0.5 + "px",
|
||||
height: unref(props.height) + 0.5 + "px"
|
||||
};
|
||||
switch (unref(props.direction)) {
|
||||
case Direction.Up:
|
||||
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
|
||||
barStyle.width = unref(props.width) + 1 + "px";
|
||||
break;
|
||||
case Direction.Down:
|
||||
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
|
||||
barStyle.width = unref(props.width) + 1 + "px";
|
||||
break;
|
||||
case Direction.Right:
|
||||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||
break;
|
||||
case Direction.Left:
|
||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
|
||||
break;
|
||||
case Direction.Default:
|
||||
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||
break;
|
||||
}
|
||||
return barStyle;
|
||||
});
|
||||
|
||||
const component = computed(() => {
|
||||
const display = props.display.value;
|
||||
return display && coerceComponent(display);
|
||||
});
|
||||
</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>
|
||||
<div
|
||||
v-if="challenge.shown"
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
:style="style"
|
||||
:class="{
|
||||
feature: true,
|
||||
[layer]: true,
|
||||
challenge: true,
|
||||
resetNotify: challenge.active,
|
||||
notify: challenge.active && challenge.canComplete,
|
||||
done: challenge.completed,
|
||||
canStart: challenge.canStart,
|
||||
maxed: challenge.maxed
|
||||
resetNotify: active,
|
||||
notify: active && canComplete,
|
||||
done: completed,
|
||||
canStart,
|
||||
maxed,
|
||||
...classes
|
||||
}"
|
||||
>
|
||||
<div v-if="title"><component :is="title" /></div>
|
||||
<button
|
||||
:style="{ backgroundColor: challenge.canStart ? buttonColor : null }"
|
||||
@click="toggle"
|
||||
>
|
||||
<button class="toggleChallenge" @click="toggle">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
<component v-if="fullDisplay" :is="fullDisplay" />
|
||||
<component v-if="component" :is="component" />
|
||||
<default-challenge-display v-else :id="id" />
|
||||
<mark-node :mark="challenge.mark" />
|
||||
<branch-node :branches="challenge.branches" :id="id" featureType="challenge" />
|
||||
<MarkNode :mark="mark" />
|
||||
<LinkNode :id="id" />
|
||||
</div>
|
||||
</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";
|
||||
<script setup lang="tsx">
|
||||
import { GenericChallenge } from "@/features/challenge";
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "challenge",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
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 "Start";
|
||||
},
|
||||
fullDisplay(): Component | string | null {
|
||||
if (this.challenge.fullDisplay) {
|
||||
return coerceComponent(this.challenge.fullDisplay, "div");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.challenge.toggle();
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericChallenge>>());
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (props.active.value) {
|
||||
return props.canComplete.value ? "Finish" : "Exit Early";
|
||||
}
|
||||
if (props.maxed.value) {
|
||||
return "Completed";
|
||||
}
|
||||
return "Start";
|
||||
});
|
||||
|
||||
const component = computed(() => {
|
||||
const display = props.display.value;
|
||||
if (display == null) {
|
||||
return null;
|
||||
}
|
||||
if (isCoercableComponent(display)) {
|
||||
return coerceComponent(display);
|
||||
}
|
||||
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>
|
||||
|
||||
|
@ -111,5 +102,6 @@ export default defineComponent({
|
|||
|
||||
.challenge.canStart button {
|
||||
cursor: pointer;
|
||||
background-color: var(--layer-color);
|
||||
}
|
||||
</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>
|
||||
<div v-if="clickable.unlocked">
|
||||
<div v-if="visibility !== Visibility.None" v-show="visibility === Visibility.Visible">
|
||||
<button
|
||||
:style="style"
|
||||
@click="clickable.click"
|
||||
@click="onClick"
|
||||
@mousedown="start"
|
||||
@mouseleave="stop"
|
||||
@mouseup="stop"
|
||||
@touchstart="start"
|
||||
@touchend="stop"
|
||||
@touchcancel="stop"
|
||||
:disabled="!clickable.canClick"
|
||||
:disabled="!canClick"
|
||||
:class="{
|
||||
feature: true,
|
||||
[layer]: true,
|
||||
clickable: true,
|
||||
can: clickable.canClick,
|
||||
locked: !clickable.canClick
|
||||
can: props.canClick,
|
||||
locked: !canClick,
|
||||
small,
|
||||
...classes
|
||||
}"
|
||||
>
|
||||
<div v-if="title">
|
||||
<component :is="title" />
|
||||
</div>
|
||||
<component :is="display" style="white-space: pre-line;" />
|
||||
<mark-node :mark="clickable.mark" />
|
||||
<branch-node :branches="clickable.branches" :id="id" featureType="clickable" />
|
||||
<component v-if="component" :is="component" />
|
||||
<MarkNode :mark="mark" />
|
||||
<LinkNode :id="id" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { Clickable } from "@/typings/features/clickable";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="tsx">
|
||||
import { GenericClickable } from "@/features/clickable";
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { coerceComponent, isCoercableComponent, setupHoldToClick } from "@/util/vue";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
import MarkNode from "./MarkNode.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "clickable",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
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;
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericClickable>>());
|
||||
|
||||
const component = computed(() => {
|
||||
const display = unref(props.display);
|
||||
if (display == null) {
|
||||
return null;
|
||||
}
|
||||
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>
|
||||
|
||||
<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>
|
||||
<div v-if="grid" class="table">
|
||||
<div v-for="row in grid.rows" class="row" :key="row">
|
||||
<div v-for="col in grid.cols" :key="col">
|
||||
<grid-cell class="align" :id="id" :cell="row * 100 + col" />
|
||||
<div
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { Grid } from "@/typings/features/grid";
|
||||
import { InjectLayerMixin } from "@/util/vue";
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { GenericGrid } from "@/features/grid";
|
||||
import GridCell from "./GridCell.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "grid",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
grid(): Grid {
|
||||
return layers[this.layer].grids!.data[this.id];
|
||||
}
|
||||
}
|
||||
});
|
||||
defineProps<FeatureComponent<GenericGrid>>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="gridCell.unlocked"
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
:class="{ feature: true, tile: true, can: canClick, locked: !canClick }"
|
||||
:style="style"
|
||||
@click="gridCell.click"
|
||||
@click="onClick"
|
||||
@mousedown="start"
|
||||
@mouseleave="stop"
|
||||
@mouseup="stop"
|
||||
|
@ -12,80 +13,28 @@
|
|||
@touchcancel="stop"
|
||||
:disabled="!canClick"
|
||||
>
|
||||
<div v-if="title"><component :is="title" /></div>
|
||||
<component :is="display" style="white-space: pre-line;" />
|
||||
<branch-node :branches="gridCell.branches" :id="id" featureType="gridCell" />
|
||||
<div v-if="title"><component :is="titleComponent" /></div>
|
||||
<component :is="component" style="white-space: pre-line;" />
|
||||
<LinkNode :id="id" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { GridCell } from "@/typings/features/grid";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { Visibility } from "@/features/feature";
|
||||
import { GridCell } from "@/features/grid";
|
||||
import { coerceComponent, setupHoldToClick } from "@/util/vue";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "grid-cell",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
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 props = toRefs(defineProps<GridCell>());
|
||||
|
||||
const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
|
||||
|
||||
const titleComponent = computed(() => {
|
||||
const title = unref(props.title);
|
||||
return title && coerceComponent(title);
|
||||
});
|
||||
const component = computed(() => coerceComponent(unref(props.display)));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -93,5 +42,6 @@ export default defineComponent({
|
|||
min-height: 80px;
|
||||
width: 80px;
|
||||
font-size: 10px;
|
||||
background-color: var(--layer-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,84 +1,43 @@
|
|||
<template>
|
||||
<div class="infobox" v-if="infobox.unlocked" :style="style" :class="{ collapsed, stacked }">
|
||||
<button class="title" :style="titleStyle" @click="toggle">
|
||||
<div
|
||||
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>
|
||||
<component :is="title" />
|
||||
<component :is="titleComponent" />
|
||||
</button>
|
||||
<collapse-transition>
|
||||
<div v-if="!collapsed" class="body" :style="{ backgroundColor: borderColor }">
|
||||
<component :is="body" :style="bodyStyle" />
|
||||
<CollapseTransition>
|
||||
<div v-if="!collapsed" class="body" :style="{ backgroundColor: color }">
|
||||
<component :is="bodyComponent" :style="bodyStyle" />
|
||||
</div>
|
||||
</collapse-transition>
|
||||
<branch-node :branches="infobox.branches" :id="id" featureType="infobox" />
|
||||
</CollapseTransition>
|
||||
<LinkNode :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import themes from "@/data/themes";
|
||||
import { layers } from "@/game/layers";
|
||||
import player from "@/game/player";
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { GenericInfobox } from "@/features/infobox";
|
||||
import settings from "@/game/settings";
|
||||
import { Infobox } from "@/typings/features/infobox";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "infobox",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericInfobox>>());
|
||||
|
||||
const titleComponent = computed(() => coerceComponent(unref(props.title)));
|
||||
const bodyComponent = computed(() => coerceComponent(unref(props.display)));
|
||||
const stacked = computed(() => themes[settings.theme].stackedInfoboxes);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,52 +1,39 @@
|
|||
<template>
|
||||
<div>
|
||||
<span v-if="showPrefix">You have </span>
|
||||
<resource :amount="amount" :color="color" />
|
||||
<ResourceVue :resource="resource" :color="color || 'white'" />
|
||||
{{ resource
|
||||
}}<!-- remove whitespace -->
|
||||
<span v-if="effectDisplay">, <component :is="effectDisplay"/></span>
|
||||
<span v-if="effectComponent">, <component :is="effectComponent"/></span>
|
||||
<br /><br />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import player from "@/game/player";
|
||||
import { format, formatWhole } from "@/util/bignum";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { CoercableComponent } from "@/features/feature";
|
||||
import { Resource } from "@/features/resource";
|
||||
import Decimal from "@/util/bignum";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { computed, StyleValue, toRefs } from "vue";
|
||||
import ResourceVue from "../system/Resource.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "main-display",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
precision: Number
|
||||
},
|
||||
computed: {
|
||||
style(): Partial<CSSStyleDeclaration> | undefined {
|
||||
return layers[this.layer].componentStyles?.["main-display"];
|
||||
},
|
||||
resource(): string {
|
||||
return layers[this.layer].resource;
|
||||
},
|
||||
effectDisplay(): Component | string | undefined {
|
||||
return (
|
||||
layers[this.layer].effectDisplay &&
|
||||
coerceComponent(layers[this.layer].effectDisplay!)
|
||||
);
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
resource: Resource;
|
||||
color?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: StyleValue;
|
||||
effectDisplay?: CoercableComponent;
|
||||
}>()
|
||||
);
|
||||
|
||||
const effectComponent = computed(() => {
|
||||
const effectDisplay = props.effectDisplay?.value;
|
||||
return effectDisplay && coerceComponent(effectDisplay);
|
||||
});
|
||||
|
||||
const showPrefix = computed(() => {
|
||||
return Decimal.lt(props.resource.value, "1e1000");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -5,15 +5,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "mark-node",
|
||||
props: {
|
||||
mark: [Boolean, String]
|
||||
}
|
||||
});
|
||||
<script setup lang="ts">
|
||||
defineProps<{ mark: boolean | string | undefined }>();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<div
|
||||
v-if="milestone.shown"
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
: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>
|
||||
<div v-if="effectDisplay"><component :is="effectDisplay" /></div>
|
||||
<component v-if="optionsDisplay" :is="optionsDisplay" />
|
||||
<branch-node :branches="milestone.branches" :id="id" featureType="milestone" />
|
||||
<component v-if="component" :is="component" />
|
||||
<LinkNode :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { Milestone } from "@/typings/features/milestone";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="tsx">
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { GenericMilestone } from "@/features/milestone";
|
||||
import { coerceComponent, isCoercableComponent } from "@/util/vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
import LinkNode from "../system/LinkNode.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "milestone",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
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;
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericMilestone>>());
|
||||
|
||||
const component = computed(() => {
|
||||
const display = props.display.value;
|
||||
if (display == 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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
<button
|
||||
v-if="upgrade.unlocked"
|
||||
v-if="visibility !== Visibility.None"
|
||||
v-show="visibility === Visibility.Visible"
|
||||
:style="style"
|
||||
@click="buy"
|
||||
@click="purchase"
|
||||
:class="{
|
||||
feature: true,
|
||||
[layer]: true,
|
||||
upgrade: true,
|
||||
can: upgrade.canAfford && !upgrade.bought,
|
||||
locked: !upgrade.canAfford && !upgrade.bought,
|
||||
bought: upgrade.bought
|
||||
can: canPurchase && !bought,
|
||||
locked: !canPurchase && !bought,
|
||||
bought,
|
||||
...classes
|
||||
}"
|
||||
:disabled="!upgrade.canAfford && !upgrade.bought"
|
||||
:disabled="!canPurchase && !bought"
|
||||
>
|
||||
<component v-if="fullDisplay" :is="fullDisplay" />
|
||||
<default-upgrade-display v-else :id="id" />
|
||||
<branch-node :branches="upgrade.branches" :id="id" featureType="upgrade" />
|
||||
<component v-if="component" :is="component" />
|
||||
<MarkNode :mark="mark" />
|
||||
<LinkNode :id="id" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { layers } from "@/game/layers";
|
||||
import { Upgrade } from "@/typings/features/upgrade";
|
||||
import { coerceComponent, InjectLayerMixin } from "@/util/vue";
|
||||
import { Component, defineComponent } from "vue";
|
||||
<script setup lang="tsx">
|
||||
import { FeatureComponent, Visibility } from "@/features/feature";
|
||||
import { displayResource } from "@/features/resource";
|
||||
import { GenericUpgrade } from "@/features/upgrade";
|
||||
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({
|
||||
name: "upgrade",
|
||||
mixins: [InjectLayerMixin],
|
||||
props: {
|
||||
id: {
|
||||
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;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buy() {
|
||||
this.upgrade.buy();
|
||||
}
|
||||
const props = toRefs(defineProps<FeatureComponent<GenericUpgrade>>());
|
||||
|
||||
const component = computed(() => {
|
||||
const display = unref(props.display);
|
||||
if (display == null) {
|
||||
return null;
|
||||
}
|
||||
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")} />
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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})`"
|
||||
>
|
||||
<transition name="actions" appear>
|
||||
<g v-if="selected && actions">
|
||||
<g v-if="isSelected && actions">
|
||||
<!-- TODO move to separate file -->
|
||||
<g
|
||||
v-for="(action, index) in actions"
|
||||
|
@ -27,19 +27,13 @@
|
|||
@touchend.stop="e => actionMouseUp(e, action)"
|
||||
>
|
||||
<circle
|
||||
:fill="
|
||||
action.fillColor
|
||||
? typeof action.fillColor === 'function'
|
||||
? action.fillColor(node)
|
||||
: action.fillColor
|
||||
: fillColor
|
||||
"
|
||||
:fill="getNodeProperty(action.fillColor, node)"
|
||||
r="20"
|
||||
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
||||
:stroke="outlineColor"
|
||||
/>
|
||||
<text :fill="titleColor" class="material-icons">{{
|
||||
typeof action.icon === "function" ? action.icon(node) : action.icon
|
||||
getNodeProperty(action.icon, node)
|
||||
}}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
@ -47,8 +41,8 @@
|
|||
|
||||
<g
|
||||
class="node-container"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
@mouseenter="isHovering = true"
|
||||
@mouseleave="isHovering = false"
|
||||
@mousedown="mouseDown"
|
||||
@touchstart="mouseDown"
|
||||
@mouseup="mouseUp"
|
||||
|
@ -163,7 +157,7 @@
|
|||
|
||||
<transition name="fade" appear>
|
||||
<text
|
||||
v-if="selected && selectedAction"
|
||||
v-if="isSelected && selectedAction"
|
||||
:fill="titleColor"
|
||||
class="node-title"
|
||||
:y="size + 75"
|
||||
|
@ -173,192 +167,140 @@
|
|||
</g>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import themes from "@/data/themes";
|
||||
import { ProgressDisplay, Shape } from "@/game/enums";
|
||||
import { layers } from "@/game/layers";
|
||||
import player from "@/game/player";
|
||||
import {
|
||||
BoardNode,
|
||||
GenericBoardNodeAction,
|
||||
GenericNodeType,
|
||||
getNodeProperty,
|
||||
ProgressDisplay,
|
||||
Shape
|
||||
} from "@/features/board";
|
||||
import { Visibility } from "@/features/feature";
|
||||
import settings from "@/game/settings";
|
||||
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
|
||||
import { getNodeTypeProperty } from "@/util/features";
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import { computed, ref, toRefs, unref, watch } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BoardNode",
|
||||
data() {
|
||||
return {
|
||||
ProgressDisplay,
|
||||
Shape,
|
||||
hovering: false,
|
||||
sqrtTwo: Math.sqrt(2)
|
||||
const sqrtTwo = Math.sqrt(2);
|
||||
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
node: BoardNode;
|
||||
nodeType: GenericNodeType;
|
||||
dragging?: BoardNode;
|
||||
dragged?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
},
|
||||
emits: ["mouseDown", "endDragging"],
|
||||
props: {
|
||||
node: {
|
||||
type: Object as PropType<BoardNode>,
|
||||
required: true
|
||||
},
|
||||
nodeType: {
|
||||
type: Object as PropType<NodeType>,
|
||||
required: true
|
||||
},
|
||||
dragging: {
|
||||
type: Object as PropType<BoardNode>
|
||||
},
|
||||
dragged: {
|
||||
type: Object as PropType<{ x: number; y: number }>,
|
||||
required: true
|
||||
},
|
||||
hasDragged: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
hasDragged?: boolean;
|
||||
receivingNode?: boolean;
|
||||
selectedNode?: BoardNode | null;
|
||||
selectedAction?: GenericBoardNodeAction | null;
|
||||
}>()
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "mouseDown", event: MouseEvent | TouchEvent, node: number, isDraggable: boolean): void;
|
||||
(e: "endDragging", node: number): void;
|
||||
}>();
|
||||
|
||||
const isHovering = ref(false);
|
||||
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
|
||||
const isDraggable = computed(() =>
|
||||
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
||||
);
|
||||
|
||||
watch(isDraggable, value => {
|
||||
const node = unref(props.node);
|
||||
if (unref(props.dragging) === node && !value) {
|
||||
emit("endDragging", 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>
|
||||
|
||||
<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>
|
||||
<span class="container" :class="{ confirming }">
|
||||
<span v-if="confirming">Are you sure?</span>
|
||||
<span class="container" :class="{ confirming: isConfirming }">
|
||||
<span v-if="isConfirming">Are you sure?</span>
|
||||
<button @click.stop="click" class="button danger" :disabled="disabled">
|
||||
<span v-if="confirming">Yes</span>
|
||||
<span v-if="isConfirming">Yes</span>
|
||||
<slot v-else />
|
||||
</button>
|
||||
<button v-if="confirming" class="button" @click.stop="cancel">No</button>
|
||||
<button v-if="isConfirming" class="button" @click.stop="cancel">No</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { ref, toRefs, unref, watch } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "danger-button",
|
||||
data() {
|
||||
return {
|
||||
confirming: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
skipConfirm: Boolean
|
||||
},
|
||||
emits: ["click", "confirmingChanged"],
|
||||
watch: {
|
||||
confirming(newValue) {
|
||||
this.$emit("confirmingChanged", newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
if (this.skipConfirm) {
|
||||
this.$emit("click");
|
||||
return;
|
||||
}
|
||||
if (this.confirming) {
|
||||
this.$emit("click");
|
||||
}
|
||||
this.confirming = !this.confirming;
|
||||
},
|
||||
cancel() {
|
||||
this.confirming = false;
|
||||
}
|
||||
}
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
skipConfirm?: boolean;
|
||||
}>()
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
(e: "confirmingChanged", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const isConfirming = ref(false);
|
||||
|
||||
watch(isConfirming, isConfirming => {
|
||||
emit("confirmingChanged", isConfirming);
|
||||
});
|
||||
|
||||
function click() {
|
||||
if (unref(props.skipConfirm)) {
|
||||
emit("click");
|
||||
return;
|
||||
}
|
||||
if (isConfirming.value) {
|
||||
emit("click");
|
||||
}
|
||||
isConfirming.value = !isConfirming.value;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
isConfirming.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -4,40 +4,34 @@
|
|||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, toRefs } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "feedback-button",
|
||||
data() {
|
||||
return {
|
||||
activated: false,
|
||||
activatedTimeout: null
|
||||
} as {
|
||||
activated: boolean;
|
||||
activatedTimeout: number | null;
|
||||
};
|
||||
},
|
||||
props: {
|
||||
left: Boolean
|
||||
},
|
||||
emits: ["click"],
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit("click");
|
||||
toRefs(
|
||||
defineProps<{
|
||||
left?: boolean;
|
||||
}>()
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
|
||||
// Give feedback to user
|
||||
if (this.activatedTimeout) {
|
||||
clearTimeout(this.activatedTimeout);
|
||||
}
|
||||
this.activated = false;
|
||||
this.$nextTick(() => {
|
||||
this.activated = true;
|
||||
this.activatedTimeout = setTimeout(() => (this.activated = false), 500);
|
||||
});
|
||||
}
|
||||
const activated = ref(false);
|
||||
const activatedTimeout = ref<number | null>(null);
|
||||
|
||||
function click() {
|
||||
emit("click");
|
||||
|
||||
// Give feedback to user
|
||||
if (activatedTimeout.value) {
|
||||
clearTimeout(activatedTimeout.value);
|
||||
}
|
||||
});
|
||||
activated.value = false;
|
||||
nextTick(() => {
|
||||
activated.value = true;
|
||||
activatedTimeout.value = setTimeout(() => (activated.value = false), 500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,40 +1,49 @@
|
|||
<template>
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="title">{{ title }}</span>
|
||||
<vue-select
|
||||
<span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
|
||||
<VueNextSelect
|
||||
:options="options"
|
||||
:model-value="value"
|
||||
@update:modelValue="setSelected"
|
||||
v-model="value"
|
||||
label-by="label"
|
||||
:value-by="getValue"
|
||||
:reduce="(option: SelectOption) => option.value"
|
||||
:placeholder="placeholder"
|
||||
:close-on-select="closeOnSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
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({
|
||||
name: "Select",
|
||||
props: {
|
||||
title: String,
|
||||
options: Array, // https://vue-select.org/guide/options.html#options-prop
|
||||
value: [String, Object],
|
||||
default: [String, Object],
|
||||
placeholder: String,
|
||||
closeOnSelect: Boolean
|
||||
export type SelectOption = { label: string; value: unknown };
|
||||
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
title?: CoercableComponent;
|
||||
modelValue?: unknown;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
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"],
|
||||
methods: {
|
||||
setSelected(value: any) {
|
||||
value = value || this.default;
|
||||
this.$emit("change", value);
|
||||
},
|
||||
getValue(item?: { value: any }) {
|
||||
return item?.value;
|
||||
}
|
||||
set(value: unknown) {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
<template>
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="title">{{ title }}</span>
|
||||
<tooltip :display="`${value}`" :class="{ fullWidth: !title }">
|
||||
<input
|
||||
type="range"
|
||||
:value="value"
|
||||
@input="e => $emit('change', parseInt(e.target.value))"
|
||||
:min="min"
|
||||
:max="max"
|
||||
/>
|
||||
</tooltip>
|
||||
<Tooltip :display="`${value}`" :class="{ fullWidth: !title }">
|
||||
<input type="range" v-model="value" :min="min" :max="max" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
import Tooltip from "../system/Tooltip.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Slider",
|
||||
props: {
|
||||
title: String,
|
||||
value: Number,
|
||||
min: Number,
|
||||
max: Number
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
title?: string;
|
||||
modelValue?: number;
|
||||
min?: 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>
|
||||
|
||||
|
|
|
@ -1,59 +1,69 @@
|
|||
<template>
|
||||
<form @submit.prevent="$emit('submit')">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="title">{{ title }}</span>
|
||||
<textarea-autosize
|
||||
v-if="textarea"
|
||||
<span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
|
||||
<VueTextareaAutosize
|
||||
v-if="textArea"
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
:value="val"
|
||||
:maxHeight="maxHeight"
|
||||
@input="change"
|
||||
@blur="() => $emit('blur')"
|
||||
@blur="submit"
|
||||
ref="field"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
:value="val"
|
||||
@input="e => change(e.target.value)"
|
||||
@blur="() => $emit('blur')"
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
ref="field"
|
||||
:class="{ fullWidth: !title }"
|
||||
@blur="submit"
|
||||
ref="field"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
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({
|
||||
name: "TextField",
|
||||
props: {
|
||||
title: String,
|
||||
value: String,
|
||||
modelValue: String,
|
||||
textarea: Boolean,
|
||||
placeholder: String,
|
||||
maxHeight: Number
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
title?: CoercableComponent;
|
||||
modelValue?: string;
|
||||
textArea?: boolean;
|
||||
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"],
|
||||
mounted() {
|
||||
(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);
|
||||
}
|
||||
set(value: string) {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
emit("submit");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
<template>
|
||||
<label class="field">
|
||||
<input type="checkbox" class="toggle" :checked="value" @input="handleInput" />
|
||||
<component :is="display" />
|
||||
<input type="checkbox" class="toggle" v-model="value" />
|
||||
<component :is="component" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { CoercableComponent } from "@/features/feature";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { defineComponent } from "vue";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
|
||||
// Reference: https://codepen.io/finnhvman/pen/pOeyjE
|
||||
export default defineComponent({
|
||||
name: "Toggle",
|
||||
props: {
|
||||
title: String,
|
||||
value: Boolean
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
title?: CoercableComponent;
|
||||
modelValue?: 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"],
|
||||
computed: {
|
||||
display() {
|
||||
return coerceComponent(this.title || "", "span");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInput(e: InputEvent) {
|
||||
this.$emit("change", (e.target as HTMLInputElement).checked);
|
||||
}
|
||||
set(value: boolean) {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
});
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "column"
|
||||
});
|
||||
</script>
|
||||
<script setup lang="ts"></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>
|
||||
<Modal :show="show">
|
||||
<Modal :model-value="isOpen">
|
||||
<template v-slot: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">
|
||||
<h2>Congratulations!</h2>
|
||||
<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
|
||||
|
@ -35,43 +35,27 @@
|
|||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/system/Modal.vue";
|
||||
import { hasWon } from "@/data/mod";
|
||||
import modInfo from "@/data/modInfo.json";
|
||||
import player from "@/game/player";
|
||||
import { formatTime } from "@/util/bignum";
|
||||
import { defineComponent } from "vue";
|
||||
import { loadSave, newSave } from "@/util/save";
|
||||
import { computed } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "GameOverScreen",
|
||||
data() {
|
||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
||||
return {
|
||||
title,
|
||||
logo,
|
||||
discordName,
|
||||
discordLink,
|
||||
versionNumber,
|
||||
versionTitle
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timePlayed() {
|
||||
return formatTime(player.timePlayed);
|
||||
},
|
||||
show() {
|
||||
return hasWon.value && !player.keepGoing;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
keepGoing() {
|
||||
player.keepGoing = true;
|
||||
},
|
||||
playAgain() {
|
||||
console.warn("Not yet implemented!");
|
||||
}
|
||||
}
|
||||
});
|
||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
||||
|
||||
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||
const isOpen = computed(() => hasWon.value && !player.keepGoing);
|
||||
|
||||
function keepGoing() {
|
||||
player.keepGoing = true;
|
||||
}
|
||||
|
||||
function playAgain() {
|
||||
loadSave(newSave());
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Modal :show="show" @close="$emit('closeDialog', 'Info')">
|
||||
<Modal v-model="isOpen">
|
||||
<template v-slot:header>
|
||||
<div class="info-modal-header">
|
||||
<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
|
||||
|
@ -17,7 +17,7 @@
|
|||
Aarex
|
||||
</div>
|
||||
<br />
|
||||
<div class="link" @click="$emit('openDialog', 'Changelog')">
|
||||
<div class="link" @click="openChangelog">
|
||||
Changelog
|
||||
</div>
|
||||
<br />
|
||||
|
@ -51,60 +51,36 @@
|
|||
</div>
|
||||
<br />
|
||||
<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>
|
||||
</template>
|
||||
</Modal>
|
||||
</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 { hotkeys } from "@/game/layers";
|
||||
import player from "@/game/player";
|
||||
import { formatTime } from "@/util/bignum";
|
||||
import { defineComponent } from "vue";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Info",
|
||||
data() {
|
||||
const {
|
||||
title,
|
||||
logo,
|
||||
author,
|
||||
discordName,
|
||||
discordLink,
|
||||
versionNumber,
|
||||
versionTitle
|
||||
} = 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);
|
||||
}
|
||||
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = modInfo;
|
||||
|
||||
const props = toRefs(defineProps<{ changelog: typeof Changelog | null }>());
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
function openChangelog() {
|
||||
unref(props.changelog)?.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<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
|
||||
class="modal-mask"
|
||||
v-show="show"
|
||||
v-on:pointerdown.self="$emit('close')"
|
||||
v-show="modelValue"
|
||||
v-on:pointerdown.self="close"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<slot name="header" :shown="isVisible">
|
||||
<slot name="header" :shown="isOpen">
|
||||
default header
|
||||
</slot>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<branches>
|
||||
<slot name="body" :shown="isVisible">
|
||||
<slot name="body" :shown="isOpen">
|
||||
default body
|
||||
</slot>
|
||||
</branches>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer" :shown="isVisible">
|
||||
<slot name="footer" :shown="isOpen">
|
||||
<div class="modal-default-footer">
|
||||
<div class="modal-default-flex-grow"></div>
|
||||
<button
|
||||
class="button modal-default-button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<button class="button modal-default-button" @click="close">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
@ -45,31 +42,25 @@
|
|||
</teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Modal",
|
||||
data() {
|
||||
return {
|
||||
isAnimating: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
show: Boolean
|
||||
},
|
||||
emits: ["close"],
|
||||
computed: {
|
||||
isVisible(): boolean {
|
||||
return this.show || this.isAnimating;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setAnimating(isAnimating: boolean) {
|
||||
this.isAnimating = isAnimating;
|
||||
}
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const isOpen = computed(() => props.modelValue || isAnimating.value);
|
||||
function close() {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
const isAnimating = ref(false);
|
||||
function setAnimating(value: boolean) {
|
||||
isAnimating.value = value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Modal :show="hasNaN" v-bind="$attrs">
|
||||
<Modal v-model="hasNaN" v-bind="$attrs">
|
||||
<template v-slot:header>
|
||||
<div class="nan-modal-header">
|
||||
<h2>NaN value detected!</h2>
|
||||
|
@ -7,9 +7,10 @@
|
|||
</template>
|
||||
<template v-slot:body>
|
||||
<div>
|
||||
Attempted to assign "{{ path }}" to NaN (previously {{ format(previous) }}).
|
||||
Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console for
|
||||
more details, and consider sharing it with the developers on discord.
|
||||
Attempted to assign "{{ path }}" to NaN<span v-if="previous">
|
||||
{{ " " }}(previously {{ format(previous) }})</span
|
||||
>. Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console
|
||||
for more details, and consider sharing it with the developers on discord.
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
|
@ -19,20 +20,20 @@
|
|||
</a>
|
||||
</div>
|
||||
<br />
|
||||
<Toggle title="Autosave" :value="autosave" @change="setAutosave" />
|
||||
<Toggle title="Pause game" :value="paused" @change="togglePaused" />
|
||||
<Toggle title="Autosave" v-model="autosave" />
|
||||
<Toggle title="Pause game" v-model="isPaused" />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="nan-footer">
|
||||
<button @click="toggleSavesManager" class="button">
|
||||
<button @click="savesManager?.open()" class="button">
|
||||
Open Saves Manager
|
||||
</button>
|
||||
<button @click="setZero" class="button">Set to 0</button>
|
||||
<button @click="setOne" class="button">Set to 1</button>
|
||||
<button
|
||||
@click="setPrev"
|
||||
@click="hasNaN = false"
|
||||
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
|
||||
</button>
|
||||
|
@ -40,75 +41,61 @@
|
|||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<SavesManager :show="showSaves" @closeDialog="toggleSavesManager" />
|
||||
<SavesManager ref="savesManager" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/system/Modal.vue";
|
||||
import modInfo from "@/data/modInfo.json";
|
||||
import player from "@/game/player";
|
||||
import state from "@/game/state";
|
||||
import Decimal, { format } from "@/util/bignum";
|
||||
import { mapPlayer, mapState } from "@/util/vue";
|
||||
import { defineComponent } from "vue";
|
||||
import Decimal, { DecimalSource, format } from "@/util/bignum";
|
||||
import { computed, ref, toRef } from "vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NaNScreen",
|
||||
data() {
|
||||
const { discordName, discordLink } = modInfo;
|
||||
return { discordName, discordLink, format, showSaves: false };
|
||||
const { discordName, discordLink } = modInfo;
|
||||
const autosave = toRef(player, "autosave");
|
||||
const hasNaN = toRef(state, "hasNaN");
|
||||
const savesManager = ref<typeof SavesManager | null>(null);
|
||||
|
||||
const path = computed(() => state.NaNPath?.join("."));
|
||||
const property = computed(() => state.NaNPath?.slice(-1)[0]);
|
||||
const previous = computed<DecimalSource | null>(() => {
|
||||
if (state.NaNReceiver && property.value) {
|
||||
return state.NaNReceiver[property.value] as DecimalSource;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const isPaused = computed({
|
||||
get() {
|
||||
return player.devSpeed === 0;
|
||||
},
|
||||
computed: {
|
||||
...mapPlayer(["autosave"]),
|
||||
...mapState(["hasNaN"]),
|
||||
path(): string | undefined {
|
||||
return state.NaNPath?.join(".");
|
||||
},
|
||||
previous(): unknown {
|
||||
if (state.NaNReceiver && this.property) {
|
||||
return state.NaNReceiver[this.property];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
paused() {
|
||||
return player.devSpeed === 0;
|
||||
},
|
||||
property(): string | undefined {
|
||||
return state.NaNPath?.slice(-1)[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;
|
||||
}
|
||||
set(value: boolean) {
|
||||
player.devSpeed = value ? null : 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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<div class="nav" v-if="useHeader" v-bind="$attrs">
|
||||
<img v-if="banner" :src="banner" height="100%" :alt="title" />
|
||||
<div v-else class="title">{{ title }}</div>
|
||||
<div @click="openDialog('Changelog')" class="version-container">
|
||||
<tooltip display="<span>Changelog</span>" bottom class="version"
|
||||
><span>v{{ version }}</span></tooltip
|
||||
<div @click="changelog?.open()" class="version-container">
|
||||
<Tooltip display="<span>Changelog</span>" bottom class="version"
|
||||
><span>v{{ versionNumber }}</span></Tooltip
|
||||
>
|
||||
</div>
|
||||
<div style="flex-grow: 1; cursor: unset;"></div>
|
||||
<div class="discord">
|
||||
<span @click="openDialog('Info')" class="material-icons">discord</span>
|
||||
<span @click="openDiscord" class="material-icons">discord</span>
|
||||
<ul class="discord-links">
|
||||
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
|
||||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
|
@ -29,57 +29,57 @@
|
|||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</div>
|
||||
<div @click="openDialog('Info')">
|
||||
<tooltip display="Info" bottom class="info">
|
||||
<div @click="info?.open()">
|
||||
<Tooltip display="Info" bottom class="info">
|
||||
<span class="material-icons">info</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="openDialog('Saves')">
|
||||
<tooltip display="Saves" bottom xoffset="-20px">
|
||||
<div @click="savesManager?.open()">
|
||||
<Tooltip display="Saves" bottom xoffset="-20px">
|
||||
<span class="material-icons">library_books</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="openDialog('Options')">
|
||||
<tooltip display="Options" bottom xoffset="-66px">
|
||||
<div @click="options?.open()">
|
||||
<Tooltip display="Options" bottom xoffset="-66px">
|
||||
<span class="material-icons">settings</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="overlay-nav" v-bind="$attrs">
|
||||
<div @click="openDialog('Changelog')" class="version-container">
|
||||
<tooltip display="Changelog" right xoffset="25%" class="version">
|
||||
<span>v{{ version }}</span>
|
||||
<div @click="changelog?.open()" class="version-container">
|
||||
<Tooltip display="Changelog" right xoffset="25%" class="version">
|
||||
<span>v{{ versionNumber }}</span>
|
||||
</tooltip>
|
||||
</div>
|
||||
<div @click="openDialog('Saves')">
|
||||
<tooltip display="Saves" right>
|
||||
<div @click="savesManager?.open()">
|
||||
<Tooltip display="Saves" right>
|
||||
<span class="material-icons">library_books</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="openDialog('Options')">
|
||||
<tooltip display="Options" right>
|
||||
<div @click="options?.open()">
|
||||
<Tooltip display="Options" right>
|
||||
<span class="material-icons">settings</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="openDialog('Info')">
|
||||
<tooltip display="Info" right>
|
||||
<div @click="info?.open()">
|
||||
<Tooltip display="Info" right>
|
||||
<span class="material-icons">info</span>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</tooltip>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</div>
|
||||
<div class="discord">
|
||||
<span @click="openDialog('Info')" class="material-icons">discord</span>
|
||||
<span @click="openDiscord" class="material-icons">discord</span>
|
||||
<ul class="discord-links">
|
||||
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
|
||||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
|
@ -98,47 +98,33 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
|
||||
<SavesManager :show="showSaves" @closeDialog="closeDialog" />
|
||||
<Options :show="showOptions" @closeDialog="closeDialog" />
|
||||
<Changelog :show="showChangelog" @closeDialog="closeDialog" />
|
||||
<Info ref="info" :changelog="changelog" />
|
||||
<SavesManager ref="savesManager" />
|
||||
<Options ref="options" />
|
||||
<Changelog ref="changelog" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import Changelog from "@/data/Changelog.vue";
|
||||
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";
|
||||
type showModals = "showInfo" | "showSaves" | "showOptions" | "showChangelog";
|
||||
const info = ref<typeof Info | null>(null);
|
||||
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({
|
||||
name: "Nav",
|
||||
data() {
|
||||
return {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
const { useHeader, banner, title, discordName, discordLink, versionNumber } = modInfo;
|
||||
|
||||
function openDiscord() {
|
||||
window.open(discordLink, "mywindow");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,107 +1,70 @@
|
|||
<template>
|
||||
<Modal :show="show" @close="$emit('closeDialog', 'Options')">
|
||||
<Modal v-model="isOpen">
|
||||
<template v-slot:header>
|
||||
<div class="header">
|
||||
<h2>Options</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<Select
|
||||
title="Theme"
|
||||
:options="themes"
|
||||
:value="theme"
|
||||
@change="setTheme"
|
||||
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')"
|
||||
/>
|
||||
<Select title="Theme" :options="themes" v-model="theme" />
|
||||
<Select title="Show Milestones" :options="msDisplayOptions" v-model="msDisplay" />
|
||||
<Toggle title="Show TPS" v-model="showTPS" />
|
||||
<Toggle title="Hide Maxed Challenges" v-model="hideChallenges" />
|
||||
<Toggle title="Unthrottled" v-model="unthrottled" />
|
||||
<Toggle
|
||||
title="Offline Production<tooltip display='Save-specific'>*</tooltip>"
|
||||
:value="offlineProd"
|
||||
@change="togglePlayerOption('offlineProd')"
|
||||
v-model="offlineProd"
|
||||
/>
|
||||
<Toggle
|
||||
title="Autosave<tooltip display='Save-specific'>*</tooltip>"
|
||||
:value="autosave"
|
||||
@change="togglePlayerOption('autosave')"
|
||||
v-model="autosave"
|
||||
/>
|
||||
<Toggle
|
||||
title="Pause game<tooltip display='Save-specific'>*</tooltip>"
|
||||
:value="paused"
|
||||
@change="togglePaused"
|
||||
v-model="isPaused"
|
||||
/>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import themes, { Themes } from "@/data/themes";
|
||||
import { camelToTitle } from "@/util/common";
|
||||
import { mapPlayer, mapSettings } from "@/util/vue";
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/system/Modal.vue";
|
||||
import rawThemes from "@/data/themes";
|
||||
import { MilestoneDisplay } from "@/features/milestone";
|
||||
import player from "@/game/player";
|
||||
import { MilestoneDisplay } from "@/game/enums";
|
||||
import { PlayerData } from "@/typings/player";
|
||||
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({
|
||||
name: "Options",
|
||||
props: {
|
||||
show: Boolean
|
||||
const isOpen = ref(false);
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const themes = Object.keys(rawThemes).map(theme => ({
|
||||
label: camelToTitle(theme),
|
||||
value: theme
|
||||
}));
|
||||
|
||||
// TODO allow features to register options
|
||||
const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
|
||||
label: camelToTitle(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;
|
||||
},
|
||||
emits: ["closeDialog"],
|
||||
data() {
|
||||
return {
|
||||
themes: Object.keys(themes).map(theme => ({
|
||||
label: camelToTitle(theme),
|
||||
value: theme
|
||||
})),
|
||||
msDisplayOptions: Object.values(MilestoneDisplay).map(option => ({
|
||||
label: camelToTitle(option),
|
||||
value: option
|
||||
}))
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapSettings(["showTPS", "hideChallenges", "theme", "msDisplay", "unthrottled"]),
|
||||
...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;
|
||||
}
|
||||
set(value: boolean) {
|
||||
devSpeed.value = value ? null : 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
</h2>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { displayResource, Resource } from "@/features/resource";
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "resource",
|
||||
props: {
|
||||
color: String,
|
||||
amount: String
|
||||
}
|
||||
});
|
||||
const props = toRefs(
|
||||
defineProps<{
|
||||
resource: Resource;
|
||||
color: string;
|
||||
}>()
|
||||
);
|
||||
|
||||
const amount = computed(() => displayResource(props.resource));
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -6,10 +6,4 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "row"
|
||||
});
|
||||
</script>
|
||||
<script lang="ts"></script>
|
||||
|
|
|
@ -1,111 +1,103 @@
|
|||
<template>
|
||||
<div class="save" :class="{ active }">
|
||||
<div class="save" :class="{ active: isActive }">
|
||||
<div class="handle material-icons">drag_handle</div>
|
||||
<div class="actions" v-if="!editing">
|
||||
<feedback-button
|
||||
@click="$emit('export')"
|
||||
<div class="actions" v-if="!isEditing">
|
||||
<FeedbackButton
|
||||
@click="emit('export')"
|
||||
class="button"
|
||||
left
|
||||
v-if="save.error == undefined && !confirming"
|
||||
v-if="save.error == undefined && !isConfirming"
|
||||
>
|
||||
<span class="material-icons">content_paste</span>
|
||||
</feedback-button>
|
||||
</FeedbackButton>
|
||||
<button
|
||||
@click="$emit('duplicate')"
|
||||
@click="emit('duplicate')"
|
||||
class="button"
|
||||
v-if="save.error == undefined && !confirming"
|
||||
v-if="save.error == undefined && !isConfirming"
|
||||
>
|
||||
<span class="material-icons">content_copy</span>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleEditing"
|
||||
@click="isEditing = !isEditing"
|
||||
class="button"
|
||||
v-if="save.error == undefined && !confirming"
|
||||
v-if="save.error == undefined && !isConfirming"
|
||||
>
|
||||
<span class="material-icons">edit</span>
|
||||
</button>
|
||||
<danger-button
|
||||
:disabled="active"
|
||||
@click="$emit('delete')"
|
||||
@confirmingChanged="confirmingChanged"
|
||||
<DangerButton
|
||||
:disabled="isActive"
|
||||
@click="emit('delete')"
|
||||
@confirmingChanged="value => (isConfirming = value)"
|
||||
>
|
||||
<span class="material-icons" style="margin: -2px">delete</span>
|
||||
</danger-button>
|
||||
</DangerButton>
|
||||
</div>
|
||||
<div class="actions" v-else>
|
||||
<button @click="changeName" class="button">
|
||||
<span class="material-icons">check</span>
|
||||
</button>
|
||||
<button @click="toggleEditing" class="button">
|
||||
<button @click="isEditing = !isEditing" class="button">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="details" v-if="save.error == undefined && !editing">
|
||||
<button class="button open" @click="$emit('open')">
|
||||
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||
<button class="button open" @click="emit('open')">
|
||||
<h3>{{ save.name }}</h3>
|
||||
</button>
|
||||
<span class="save-version">v{{ save.modVersion }}</span
|
||||
><br />
|
||||
<div v-if="time">Last played {{ dateFormat.format(time) }}</div>
|
||||
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
|
||||
</div>
|
||||
<div class="details" v-else-if="save.error == undefined && editing">
|
||||
<TextField v-model="newName" class="editname" @submit="changeName" @blur="changeName" />
|
||||
<div class="details" v-else-if="save.error == undefined && isEditing">
|
||||
<Text v-model="newName" class="editname" @submit="changeName" />
|
||||
</div>
|
||||
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import player from "@/game/player";
|
||||
import { PlayerData } from "@/typings/player";
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import { computed, ref, toRefs, unref, watch } 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({
|
||||
name: "save",
|
||||
props: {
|
||||
save: {
|
||||
type: Object as PropType<Partial<PlayerData>>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ["export", "open", "duplicate", "delete", "editSave"],
|
||||
data() {
|
||||
return {
|
||||
dateFormat: new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "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 props = toRefs(
|
||||
defineProps<{
|
||||
save: LoadablePlayerData;
|
||||
}>()
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "export"): void;
|
||||
(e: "open"): void;
|
||||
(e: "duplicate"): void;
|
||||
(e: "delete"): void;
|
||||
(e: "editName", name: string): void;
|
||||
}>();
|
||||
|
||||
const dateFormat = new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<Modal :show="show" @close="$emit('closeDialog', 'Saves')">
|
||||
<Modal v-model="isOpen">
|
||||
<template v-slot:header>
|
||||
<h2>Saves Manager</h2>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div v-sortable="{ update, handle: '.handle' }">
|
||||
<save
|
||||
<Save
|
||||
v-for="(save, index) in saves"
|
||||
:key="index"
|
||||
:save="save"
|
||||
@open="openSave(save.id)"
|
||||
@export="exportSave(save.id)"
|
||||
@editSave="name => editSave(save.id, name)"
|
||||
@editName="name => editSave(save.id, name)"
|
||||
@duplicate="duplicateSave(save.id)"
|
||||
@delete="deleteSave(save.id)"
|
||||
/>
|
||||
|
@ -19,10 +19,8 @@
|
|||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="modal-footer">
|
||||
<TextField
|
||||
:value="saveToImport"
|
||||
@submit="importSave"
|
||||
@change="importSave"
|
||||
<Text
|
||||
v-model="saveToImport"
|
||||
title="Import Save"
|
||||
placeholder="Paste your save here!"
|
||||
:class="{ importingFailed }"
|
||||
|
@ -34,20 +32,17 @@
|
|||
<Select
|
||||
v-if="Object.keys(bank).length > 0"
|
||||
:options="bank"
|
||||
:modelValue="[]"
|
||||
@update:modelValue="preset => newFromPreset(preset as string)"
|
||||
closeOnSelect
|
||||
@change="newFromPreset"
|
||||
placeholder="Select preset"
|
||||
class="presets"
|
||||
:value="[]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div style="flex-grow: 1"></div>
|
||||
<button
|
||||
class="button modal-default-button"
|
||||
@click="$emit('closeDialog', 'Saves')"
|
||||
>
|
||||
<button class="button modal-default-button" @click="isOpen = false">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
@ -56,184 +51,175 @@
|
|||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import player from "@/game/player";
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/system/Modal.vue";
|
||||
import player, { PlayerData } from "@/game/player";
|
||||
import settings from "@/game/settings";
|
||||
import state from "@/game/state";
|
||||
import { PlayerData } from "@/typings/player";
|
||||
import { getUniqueID, loadSave, newSave, save } from "@/util/save";
|
||||
import { defineComponent } from "vue";
|
||||
import { getUniqueID, loadSave, save, newSave as createNewSave } from "@/util/save";
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Text from "../fields/Text.vue";
|
||||
import Save from "./Save.vue";
|
||||
import vSortable from "vue-sortable";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SavesManager",
|
||||
props: {
|
||||
show: Boolean
|
||||
},
|
||||
emits: ["closeDialog"],
|
||||
data() {
|
||||
let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/);
|
||||
let bank = bankContext
|
||||
.keys()
|
||||
.reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
||||
// .slice(2, -4) strips the leading ./ and the trailing .txt
|
||||
acc.push({
|
||||
label: curr.slice(2, -4),
|
||||
value: bankContext(curr).default
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
return {
|
||||
importingFailed: false,
|
||||
saves: {}, // Gets populated when the modal is opened
|
||||
saveToImport: "",
|
||||
bank
|
||||
} as {
|
||||
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 {
|
||||
const save = localStorage.getItem(curr);
|
||||
if (save == null) {
|
||||
acc[curr] = { error: `Save with id "${curr}" doesn't exist`, id: curr };
|
||||
} else {
|
||||
acc[curr] = JSON.parse(decodeURIComponent(escape(atob(save))));
|
||||
acc[curr].id = curr;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Can't load save with id "${curr}"`, error);
|
||||
acc[curr] = { error, id: curr };
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
},
|
||||
exportSave(id: string) {
|
||||
let saveToExport;
|
||||
if (player.id === id) {
|
||||
save();
|
||||
saveToExport = state.saveToExport;
|
||||
} else {
|
||||
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))));
|
||||
}
|
||||
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
|
||||
|
||||
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
||||
const el = document.createElement("textarea");
|
||||
el.value = saveToExport;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
},
|
||||
duplicateSave(id: string) {
|
||||
if (player.id === id) {
|
||||
save();
|
||||
}
|
||||
const isOpen = ref(false);
|
||||
|
||||
const playerData = { ...this.saves[id], id: getUniqueID() };
|
||||
localStorage.setItem(
|
||||
playerData.id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||
);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
this.saves[playerData.id] = playerData;
|
||||
},
|
||||
deleteSave(id: string) {
|
||||
settings.saves = settings.saves.filter((save: string) => save !== id);
|
||||
localStorage.removeItem(id);
|
||||
delete this.saves[id];
|
||||
},
|
||||
openSave(id: string) {
|
||||
this.saves[player.id].time = player.time;
|
||||
save();
|
||||
loadSave(this.saves[id]);
|
||||
},
|
||||
newSave() {
|
||||
const playerData = newSave();
|
||||
this.saves[playerData.id] = playerData;
|
||||
},
|
||||
newFromPreset(preset: string) {
|
||||
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
|
||||
playerData.id = getUniqueID();
|
||||
localStorage.setItem(
|
||||
playerData.id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||
);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
this.saves[playerData.id] = playerData;
|
||||
},
|
||||
editSave(id: string, newName: string) {
|
||||
this.saves[id].name = newName;
|
||||
if (player.id === id) {
|
||||
player.name = newName;
|
||||
save();
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[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);
|
||||
} 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]);
|
||||
}
|
||||
defineExpose({
|
||||
open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
acc.push({
|
||||
label: curr.slice(2, -4),
|
||||
value: bankContext(curr).default
|
||||
});
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
|
||||
const saves = ref<Record<string, LoadablePlayerData>>({});
|
||||
|
||||
function loadSaveData() {
|
||||
saves.value = settings.saves.reduce((acc: Record<string, LoadablePlayerData>, curr: string) => {
|
||||
try {
|
||||
const save = localStorage.getItem(curr);
|
||||
if (save == null) {
|
||||
acc[curr] = { error: `Save with id "${curr}" doesn't exist`, id: curr };
|
||||
} else {
|
||||
acc[curr] = JSON.parse(decodeURIComponent(escape(atob(save))));
|
||||
acc[curr].id = curr;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Can't load save with id "${curr}"`, error);
|
||||
acc[curr] = { error, id: curr };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function exportSave(id: string) {
|
||||
let saveToExport;
|
||||
if (player.id === id) {
|
||||
saveToExport = save();
|
||||
} else {
|
||||
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))));
|
||||
}
|
||||
|
||||
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
||||
const el = document.createElement("textarea");
|
||||
el.value = saveToExport;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
function duplicateSave(id: string) {
|
||||
if (player.id === id) {
|
||||
save();
|
||||
}
|
||||
|
||||
const playerData = { ...saves.value[id], id: getUniqueID() };
|
||||
localStorage.setItem(
|
||||
playerData.id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||
);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
saves.value[playerData.id] = playerData;
|
||||
}
|
||||
|
||||
function deleteSave(id: string) {
|
||||
settings.saves = settings.saves.filter((save: string) => save !== id);
|
||||
localStorage.removeItem(id);
|
||||
delete saves.value[id];
|
||||
}
|
||||
|
||||
function openSave(id: string) {
|
||||
saves.value[player.id].time = player.time;
|
||||
save();
|
||||
loadSave(saves.value[id]);
|
||||
}
|
||||
|
||||
function newSave() {
|
||||
const playerData = createNewSave();
|
||||
saves.value[playerData.id] = playerData;
|
||||
}
|
||||
|
||||
function newFromPreset(preset: string) {
|
||||
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
|
||||
playerData.id = getUniqueID();
|
||||
localStorage.setItem(
|
||||
playerData.id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
|
||||
);
|
||||
|
||||
settings.saves.push(playerData.id);
|
||||
saves.value[playerData.id] = playerData;
|
||||
}
|
||||
|
||||
function editSave(id: string, newName: string) {
|
||||
saves.value[id].name = newName;
|
||||
if (player.id === id) {
|
||||
player.name = newName;
|
||||
save();
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
id,
|
||||
btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function update(e: { newIndex: number; oldIndex: number }) {
|
||||
settings.saves.splice(e.newIndex, 0, settings.saves.splice(e.oldIndex, 1)[0]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
<template>
|
||||
<div :style="{ width: spacingWidth, height: spacingHeight }"></div>
|
||||
<div :style="{ width, height }"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "spacer",
|
||||
props: {
|
||||
width: String,
|
||||
height: String
|
||||
},
|
||||
computed: {
|
||||
spacingWidth(): string {
|
||||
return this.width || "8px";
|
||||
},
|
||||
spacingHeight(): string {
|
||||
return this.height || "17px";
|
||||
}
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
width: string;
|
||||
height: string;
|
||||
}>(),
|
||||
{
|
||||
width: "8px",
|
||||
height: "17px"
|
||||
}
|
||||
});
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -1,54 +1,39 @@
|
|||
<template>
|
||||
<div class="sticky" :style="{ top }" ref="sticky" data-v-sticky>
|
||||
<div class="sticky" :style="{ top }" ref="element" data-v-sticky>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "sticky",
|
||||
data() {
|
||||
return {
|
||||
top: "0",
|
||||
observer: null
|
||||
} 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) {
|
||||
return;
|
||||
}
|
||||
const top = ref("0");
|
||||
const observer = new ResizeObserver(updateTop);
|
||||
const element = ref<HTMLElement | null>(null);
|
||||
|
||||
let top = 0;
|
||||
while (el.previousSibling) {
|
||||
const sibling = el.previousSibling as HTMLElement;
|
||||
if (sibling.dataset && "vSticky" in sibling.dataset) {
|
||||
top += sibling.offsetHeight;
|
||||
}
|
||||
el = sibling;
|
||||
}
|
||||
this.top = top + "px";
|
||||
function updateTop() {
|
||||
let el = element.value;
|
||||
if (el == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newTop = 0;
|
||||
while (el.previousSibling) {
|
||||
const sibling = el.previousSibling as HTMLElement;
|
||||
if (sibling.dataset && "vSticky" in sibling.dataset) {
|
||||
newTop += sibling.offsetHeight;
|
||||
}
|
||||
el = sibling;
|
||||
}
|
||||
top.value = newTop + "px";
|
||||
}
|
||||
|
||||
nextTick(updateTop);
|
||||
|
||||
onMounted(() => {
|
||||
const el = element.value?.parentElement;
|
||||
if (el) {
|
||||
observer.observe(el);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
<template>
|
||||
<div class="tpsDisplay" v-if="tps !== 'NaN'">TPS: {{ tps }}</div>
|
||||
<div class="tpsDisplay" v-if="!tps.isNan">TPS: {{ tps }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import state from "@/game/state";
|
||||
import Decimal, { formatWhole } from "@/util/bignum";
|
||||
import { defineComponent } from "vue";
|
||||
import Decimal from "@/util/bignum";
|
||||
import { computed } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TPS",
|
||||
computed: {
|
||||
tps() {
|
||||
return formatWhole(
|
||||
Decimal.div(
|
||||
state.lastTenTicks.length,
|
||||
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
const tps = computed(() =>
|
||||
Decimal.div(
|
||||
state.lastTenTicks.length,
|
||||
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<div
|
||||
class="tooltip-container"
|
||||
:class="{ shown }"
|
||||
:class="{ shown: isShown }"
|
||||
@mouseenter="setHover(true)"
|
||||
@mouseleave="setHover(false)"
|
||||
>
|
||||
<slot />
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="shown"
|
||||
v-if="isShown"
|
||||
class="tooltip"
|
||||
:class="{ top, left, right, bottom }"
|
||||
:style="{
|
||||
|
@ -16,56 +16,28 @@
|
|||
'--yoffset': yoffset || '0px'
|
||||
}"
|
||||
>
|
||||
<component :is="tooltipDisplay" />
|
||||
<component :is="component" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { CoercableComponent } from "@/typings/component";
|
||||
<script setup lang="ts">
|
||||
import { FeatureComponent } from "@/features/feature";
|
||||
import { Tooltip } from "@/features/tooltip";
|
||||
import { coerceComponent } from "@/util/vue";
|
||||
import { Component, defineComponent, PropType } from "vue";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "tooltip",
|
||||
data() {
|
||||
return {
|
||||
hover: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
force: Boolean,
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
const props = toRefs(defineProps<FeatureComponent<Tooltip>>());
|
||||
|
||||
const isHovered = ref(false);
|
||||
|
||||
function setHover(hover: boolean) {
|
||||
isHovered.value = hover;
|
||||
}
|
||||
|
||||
const isShown = computed(() => unref(props.force) || isHovered.value);
|
||||
const component = computed(() => props.display.value && coerceComponent(unref(props.display)));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -2,15 +2,10 @@
|
|||
<div class="vr" :style="{ height }"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "vr",
|
||||
props: {
|
||||
height: String
|
||||
}
|
||||
});
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
height?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<Modal :show="show" @close="$emit('closeDialog', 'Changelog')">
|
||||
<Modal v-model="isOpen">
|
||||
<template v-slot:header>
|
||||
<h2>Changelog</h2>
|
||||
</template>
|
||||
|
@ -18,15 +18,16 @@
|
|||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
<script setup lang="ts">
|
||||
import Modal from "@/components/system/Modal.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Changelog",
|
||||
props: {
|
||||
show: Boolean
|
||||
},
|
||||
emits: ["closeDialog"]
|
||||
const isOpen = ref(false);
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
</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 = {
|
||||
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