New layer API WIP

This commit is contained in:
thepaperpilot 2022-01-13 22:25:47 -06:00
parent e499447cf5
commit 6f781b33fa
159 changed files with 15366 additions and 26427 deletions

View file

@ -1,7 +1,8 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true node: true,
'vue/setup-compiler-macros': true
}, },
extends: [ extends: [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",
@ -15,6 +16,8 @@ module.exports = {
}, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/script-setup-uses-vars": "error",
"vue/no-mutating-props": "off"
} }
}; };

3
.vs/ProjectSettings.json Normal file
View file

@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

10
.vs/VSWorkspaceState.json Normal file
View file

@ -0,0 +1,10 @@
{
"ExpandedNodes": [
"",
"\\src",
"\\src\\typings",
"\\src\\util"
],
"SelectedNode": "\\src\\App.vue",
"PreviewInSolutionExplorer": false
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

View file

@ -6,6 +6,7 @@ module.exports = {
{ {
enabled: true enabled: true
} }
] ],
"@vue/babel-plugin-jsx"
] ]
}; };

21769
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,9 @@
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"vue": "^3.2.2", "nanoevents": "^6.0.2",
"vue-class-component": "^8.0.0-rc.1", "vue": "^3.2.26",
"vue-next-select": "^2.9.0", "vue-next-select": "^2.10.2",
"vue-panzoom": "^1.1.6", "vue-panzoom": "^1.1.6",
"vue-sortable": "github:Netbel/vue-sortable#master-fix", "vue-sortable": "github:Netbel/vue-sortable#master-fix",
"vue-textarea-autosize": "^1.1.1", "vue-textarea-autosize": "^1.1.1",
@ -23,25 +23,26 @@
"@ivanv/vue-collapse-transition": "^1.0.2", "@ivanv/vue-collapse-transition": "^1.0.2",
"@jetblack/operator-overloading": "^0.2.0", "@jetblack/operator-overloading": "^0.2.0",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^5.9.1",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^5.9.1",
"@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.2.2", "@vue/compiler-sfc": "^3.2.26",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^8.6.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.0.0-alpha.0", "eslint-plugin-vue": "^8.3.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass": "^1.36.0", "sass": "^1.36.0",
"sass-loader": "^10.2.0", "sass-loader": "^10.2.0",
"tsconfig-paths-webpack-plugin": "^3.5.1", "tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "~4.1.5" "typescript": "^4.5.4"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View file

@ -2,38 +2,31 @@
<div id="modal-root" :style="theme" /> <div id="modal-root" :style="theme" />
<div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }"> <div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
<Nav v-if="useHeader" /> <Nav v-if="useHeader" />
<Tabs /> <Game />
<TPS v-if="showTPS" /> <TPS v-if="showTPS" />
<GameOverScreen /> <GameOverScreen />
<NaNScreen /> <NaNScreen />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { computed, toRef } from "vue";
import Game from "./components/system/Game.vue";
import GameOverScreen from "./components/system/GameOverScreen.vue";
import NaNScreen from "./components/system/NaNScreen.vue";
import TPS from "./components/system/TPS.vue";
import modInfo from "./data/modInfo.json"; import modInfo from "./data/modInfo.json";
import themes from "./data/themes"; import themes from "./data/themes";
import settings from "./game/settings"; import settings from "./game/settings";
import "./main.css"; import "./main.css";
import { mapSettings } from "./util/vue";
export default defineComponent({ function updateMouse(/* event */) {
name: "App",
data() {
return { useHeader: modInfo.useHeader };
},
computed: {
...mapSettings(["showTPS"]),
theme() {
return themes[settings.theme].variables;
}
},
methods: {
updateMouse(/* event */) {
// TODO use event to update mouse position for particles // TODO use event to update mouse position for particles
} }
}
}); const useHeader = modInfo.useHeader;
const theme = computed(() => themes[settings.theme].variables);
const showTPS = toRef(settings, "showTPS");
</script> </script>
<style scoped> <style scoped>

View file

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

View file

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

View file

@ -11,6 +11,7 @@
.can, .can,
.can button { .can button {
background-color: var(--layer-color);
cursor: pointer; cursor: pointer;
} }

View file

@ -1,65 +1,40 @@
<template> <template>
<tooltip v-if="achievement.unlocked" :display="tooltip"> <Tooltip
v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:display="tooltip"
>
<div <div
:style="style" :style="[{ backgroundImage: (earned && image && `url(${image})`) || '' }, style ?? []]"
:class="{ :class="{
[layer]: true,
feature: true, feature: true,
achievement: true, achievement: true,
locked: !achievement.earned, locked: !earned,
bought: achievement.earned bought: earned,
...classes
}" }"
> >
<component v-if="display" :is="display" /> <component v-if="component" :is="component" />
<branch-node :branches="achievement.branches" :id="id" featureType="achievement" /> <MarkNode :mark="mark" />
<LinkNode :id="id" />
</div> </div>
</tooltip> </Tooltip>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { layers } from "@/game/layers"; import { GenericAchievement } from "@/features/achievement";
import { CoercableComponent } from "@/typings/component"; import { FeatureComponent } from "@/features/feature";
import { Achievement } from "@/typings/features/achievement"; import { coerceComponent } from "@/util/vue";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { computed, toRefs } from "vue";
import { Component, defineComponent } from "vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue";
import { Visibility } from "@/features/feature";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericAchievement>>());
name: "achievement",
mixins: [InjectLayerMixin], const component = computed(() => {
props: { const display = props.display.value;
id: { return display && coerceComponent(display);
type: [Number, String],
required: true
}
},
computed: {
achievement(): Achievement {
return layers[this.layer].achievements!.data[this.id];
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
layers[this.layer].componentStyles?.achievement,
this.achievement.style,
this.achievement.image && this.achievement.earned
? {
backgroundImage: `url(${this.achievement.image}`
}
: undefined
];
},
display(): Component | string {
if (this.achievement.display) {
return coerceComponent(this.achievement.display, "h3");
}
return coerceComponent(this.achievement.name!, "h3");
},
tooltip(): CoercableComponent {
if (this.achievement.earned) {
return this.achievement.doneTooltip || this.achievement.tooltip || "You did it!";
}
return this.achievement.goalTooltip || this.achievement.tooltip || "LOCKED";
}
}
}); });
</script> </script>

View file

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

View file

@ -1,97 +1,84 @@
<template> <template>
<div v-if="bar.unlocked" :style="style" :class="{ [layer]: true, bar: true }"> <div
<div class="overlayTextContainer border" :style="borderStyle"> v-if="visibility !== Visibility.None"
<component class="overlayText" :style="textStyle" :is="display" /> v-show="visibility === Visibility.Visible"
:style="[{ width: width + 'px', height: height + 'px' }, style ?? {}]"
:class="{
bar: true,
...classes
}"
>
<div
class="overlayTextContainer border"
:style="[{ width: width + 'px', height: height + 'px' }, borderStyle ?? {}]"
>
<component v-if="component" class="overlayText" :style="textStyle" :is="component" />
</div> </div>
<div class="border" :style="backgroundStyle"> <div
<div class="fill" :style="fillStyle" /> class="border"
:style="[
{ width: width + 'px', height: height + 'px' },
style ?? {},
baseStyle ?? {},
borderStyle ?? {}
]"
>
<div class="fill" :style="[barStyle, style ?? {}, fillStyle ?? {}]" />
</div> </div>
<branch-node :branches="bar.branches" :id="id" featureType="bar" /> <MarkNode :mark="mark" />
<LinkNode :id="id" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { Direction } from "@/game/enums"; import { Direction, GenericBar } from "@/features/bar";
import { layers } from "@/game/layers"; import { FeatureComponent, Visibility } from "@/features/feature";
import { Bar } from "@/typings/features/bar";
import Decimal from "@/util/bignum"; import Decimal from "@/util/bignum";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, CSSProperties, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericBar>>());
name: "bar",
mixins: [InjectLayerMixin], const normalizedProgress = computed(() => {
props: {
id: {
type: [Number, String],
required: true
}
},
computed: {
bar(): Bar {
return layers[this.layer].bars!.data[this.id];
},
progress(): number {
let progress = let progress =
this.bar.progress instanceof Decimal props.progress.value instanceof Decimal
? this.bar.progress.toNumber() ? props.progress.value.toNumber()
: (this.bar.progress as number); : Number(props.progress.value);
return (1 - Math.min(Math.max(progress, 0), 1)) * 100; return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
}, });
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [ const barStyle = computed(() => {
{ width: this.bar.width + "px", height: this.bar.height + "px" }, const barStyle: Partial<CSSProperties> = {
layers[this.layer].componentStyles?.bar, width: unref(props.width) + 0.5 + "px",
this.bar.style height: unref(props.height) + 0.5 + "px"
];
},
borderStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
{ width: this.bar.width + "px", height: this.bar.height + "px" },
this.bar.borderStyle
];
},
textStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [this.bar.style, this.bar.textStyle];
},
backgroundStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
{ width: this.bar.width + "px", height: this.bar.height + "px" },
this.bar.style,
this.bar.baseStyle,
this.bar.borderStyle
];
},
fillStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
const fillStyle: Partial<CSSStyleDeclaration> = {
width: this.bar.width + 0.5 + "px",
height: this.bar.height + 0.5 + "px"
}; };
switch (this.bar.direction) { switch (unref(props.direction)) {
case Direction.Up: case Direction.Up:
fillStyle.clipPath = `inset(${this.progress}% 0% 0% 0%)`; barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
fillStyle.width = this.bar.width + 1 + "px"; barStyle.width = unref(props.width) + 1 + "px";
break; break;
case Direction.Down: case Direction.Down:
fillStyle.clipPath = `inset(0% 0% ${this.progress}% 0%)`; barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
fillStyle.width = this.bar.width + 1 + "px"; barStyle.width = unref(props.width) + 1 + "px";
break; break;
case Direction.Right: case Direction.Right:
fillStyle.clipPath = `inset(0% ${this.progress}% 0% 0%)`; barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
break; break;
case Direction.Left: case Direction.Left:
fillStyle.clipPath = `inset(0% 0% 0% ${this.progress} + '%)`; barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
break; break;
case Direction.Default: case Direction.Default:
fillStyle.clipPath = "inset(0% 50% 0% 0%)"; barStyle.clipPath = "inset(0% 50% 0% 0%)";
break; break;
} }
return [fillStyle, this.bar.style, this.bar.fillStyle]; return barStyle;
}, });
display(): Component | string {
return coerceComponent(this.bar.display); const component = computed(() => {
} const display = props.display.value;
} return display && coerceComponent(display);
}); });
</script> </script>

View file

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

View file

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

View file

@ -1,87 +1,78 @@
<template> <template>
<div <div
v-if="challenge.shown" v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:style="style" :style="style"
:class="{ :class="{
feature: true, feature: true,
[layer]: true,
challenge: true, challenge: true,
resetNotify: challenge.active, resetNotify: active,
notify: challenge.active && challenge.canComplete, notify: active && canComplete,
done: challenge.completed, done: completed,
canStart: challenge.canStart, canStart,
maxed: challenge.maxed maxed,
...classes
}" }"
> >
<div v-if="title"><component :is="title" /></div> <button class="toggleChallenge" @click="toggle">
<button
:style="{ backgroundColor: challenge.canStart ? buttonColor : null }"
@click="toggle"
>
{{ buttonText }} {{ buttonText }}
</button> </button>
<component v-if="fullDisplay" :is="fullDisplay" /> <component v-if="component" :is="component" />
<default-challenge-display v-else :id="id" /> <default-challenge-display v-else :id="id" />
<mark-node :mark="challenge.mark" /> <MarkNode :mark="mark" />
<branch-node :branches="challenge.branches" :id="id" featureType="challenge" /> <LinkNode :id="id" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="tsx">
import { layers } from "@/game/layers"; import { GenericChallenge } from "@/features/challenge";
import { Challenge } from "@/typings/features/challenge"; import { FeatureComponent, Visibility } from "@/features/feature";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, toRefs } from "vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericChallenge>>());
name: "challenge",
mixins: [InjectLayerMixin], const buttonText = computed(() => {
props: { if (props.active.value) {
id: { return props.canComplete.value ? "Finish" : "Exit Early";
type: [Number, String],
required: true
} }
}, if (props.maxed.value) {
computed: {
challenge(): Challenge {
return layers[this.layer].challenges!.data[this.id];
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [layers[this.layer].componentStyles?.challenge, this.challenge.style];
},
title(): Component | string | null {
if (this.challenge.titleDisplay) {
return coerceComponent(this.challenge.titleDisplay, "div");
}
if (this.challenge.name) {
return coerceComponent(this.challenge.name, "h3");
}
return null;
},
buttonColor(): string {
return layers[this.layer].color;
},
buttonText(): string {
if (this.challenge.active) {
return this.challenge.canComplete ? "Finish" : "Exit Early";
}
if (this.challenge.maxed) {
return "Completed"; return "Completed";
} }
return "Start"; return "Start";
}, });
fullDisplay(): Component | string | null {
if (this.challenge.fullDisplay) { const component = computed(() => {
return coerceComponent(this.challenge.fullDisplay, "div"); const display = props.display.value;
} if (display == null) {
return null; return null;
} }
}, if (isCoercableComponent(display)) {
methods: { return coerceComponent(display);
toggle() {
this.challenge.toggle();
}
} }
return (
<span>
<template v-if={display.title}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.title!, "h3")} />
</template>
<component v-is={coerceComponent(display.description, "div")} />
<div v-if={display.goal}>
<br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
Goal: <component v-is={coerceComponent(display.goal!)} />
</div>
<div v-if={display.reward}>
<br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
Reward: <component v-is={coerceComponent(display.reward!)} />
</div>
<div v-if={display.effectDisplay}>
Currently: {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.effectDisplay!)} />
</div>
</span>
);
}); });
</script> </script>
@ -111,5 +102,6 @@ export default defineComponent({
.challenge.canStart button { .challenge.canStart button {
cursor: pointer; cursor: pointer;
background-color: var(--layer-color);
} }
</style> </style>

View file

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

View file

@ -1,95 +1,61 @@
<template> <template>
<div v-if="clickable.unlocked"> <div v-if="visibility !== Visibility.None" v-show="visibility === Visibility.Visible">
<button <button
:style="style" :style="style"
@click="clickable.click" @click="onClick"
@mousedown="start" @mousedown="start"
@mouseleave="stop" @mouseleave="stop"
@mouseup="stop" @mouseup="stop"
@touchstart="start" @touchstart="start"
@touchend="stop" @touchend="stop"
@touchcancel="stop" @touchcancel="stop"
:disabled="!clickable.canClick" :disabled="!canClick"
:class="{ :class="{
feature: true, feature: true,
[layer]: true,
clickable: true, clickable: true,
can: clickable.canClick, can: props.canClick,
locked: !clickable.canClick locked: !canClick,
small,
...classes
}" }"
> >
<div v-if="title"> <component v-if="component" :is="component" />
<component :is="title" /> <MarkNode :mark="mark" />
</div> <LinkNode :id="id" />
<component :is="display" style="white-space: pre-line;" />
<mark-node :mark="clickable.mark" />
<branch-node :branches="clickable.branches" :id="id" featureType="clickable" />
</button> </button>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="tsx">
import { layers } from "@/game/layers"; import { GenericClickable } from "@/features/clickable";
import { Clickable } from "@/typings/features/clickable"; import { FeatureComponent, Visibility } from "@/features/feature";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent, isCoercableComponent, setupHoldToClick } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericClickable>>());
name: "clickable",
mixins: [InjectLayerMixin], const component = computed(() => {
props: { const display = unref(props.display);
id: { if (display == null) {
type: [Number, String],
required: true
},
size: String
},
data() {
return {
interval: null,
time: 0
} as {
interval: number | null;
time: number;
};
},
computed: {
clickable(): Clickable {
return layers[this.layer].clickables!.data[this.id];
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
this.clickable.canClick ? { backgroundColor: layers[this.layer].color } : undefined,
this.size ? { height: this.size, width: this.size } : undefined,
layers[this.layer].componentStyles?.clickable,
this.clickable.style
];
},
title(): Component | string | null {
if (this.clickable.title) {
return coerceComponent(this.clickable.title, "h2");
}
return null; return null;
},
display(): Component | string {
return coerceComponent(this.clickable.display, "div");
}
},
methods: {
start() {
if (!this.interval && this.clickable.click) {
this.interval = setInterval(this.clickable.click, 250);
}
},
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
this.time = 0;
}
} }
if (isCoercableComponent(display)) {
return coerceComponent(display);
} }
return (
<span>
<div v-if={display.title}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.title!, "h2")} />
</div>
<component v-is={coerceComponent(display.description, "div")} />
</span>
);
}); });
const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
</script> </script>
<style scoped> <style scoped>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,23 @@
<template> <template>
<div v-if="grid" class="table"> <div
<div v-for="row in grid.rows" class="row" :key="row"> v-if="visibility !== Visibility.None"
<div v-for="col in grid.cols" :key="col"> v-show="visibility === Visibility.Visible"
<grid-cell class="align" :id="id" :cell="row * 100 + col" /> class="table"
>
<div v-for="row in rows" class="row" :key="row">
<div v-for="col in cols" :key="col">
<GridCell v-bind="cells[row * 100 + col]" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { layers } from "@/game/layers"; import { FeatureComponent, Visibility } from "@/features/feature";
import { Grid } from "@/typings/features/grid"; import { GenericGrid } from "@/features/grid";
import { InjectLayerMixin } from "@/util/vue"; import GridCell from "./GridCell.vue";
import { defineComponent } from "vue";
export default defineComponent({ defineProps<FeatureComponent<GenericGrid>>();
name: "grid",
mixins: [InjectLayerMixin],
props: {
id: {
type: [Number, String],
required: true
}
},
computed: {
grid(): Grid {
return layers[this.layer].grids!.data[this.id];
}
}
});
</script> </script>
<style scoped></style> <style scoped></style>

View file

@ -1,9 +1,10 @@
<template> <template>
<button <button
v-if="gridCell.unlocked" v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:class="{ feature: true, tile: true, can: canClick, locked: !canClick }" :class="{ feature: true, tile: true, can: canClick, locked: !canClick }"
:style="style" :style="style"
@click="gridCell.click" @click="onClick"
@mousedown="start" @mousedown="start"
@mouseleave="stop" @mouseleave="stop"
@mouseup="stop" @mouseup="stop"
@ -12,80 +13,28 @@
@touchcancel="stop" @touchcancel="stop"
:disabled="!canClick" :disabled="!canClick"
> >
<div v-if="title"><component :is="title" /></div> <div v-if="title"><component :is="titleComponent" /></div>
<component :is="display" style="white-space: pre-line;" /> <component :is="component" style="white-space: pre-line;" />
<branch-node :branches="gridCell.branches" :id="id" featureType="gridCell" /> <LinkNode :id="id" />
</button> </button>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { layers } from "@/game/layers"; import { Visibility } from "@/features/feature";
import { GridCell } from "@/typings/features/grid"; import { GridCell } from "@/features/grid";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent, setupHoldToClick } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue";
export default defineComponent({ const props = toRefs(defineProps<GridCell>());
name: "grid-cell",
mixins: [InjectLayerMixin], const { start, stop } = setupHoldToClick(props.onClick, props.onHold);
props: {
id: { const titleComponent = computed(() => {
type: [Number, String], const title = unref(props.title);
required: true return title && coerceComponent(title);
},
cell: {
type: [Number, String],
required: true
},
size: [Number, String]
},
data() {
return {
interval: null,
time: 0
} as {
interval: number | null;
time: number;
};
},
computed: {
gridCell(): GridCell {
return layers[this.layer].grids!.data[this.id][this.cell] as GridCell;
},
canClick(): boolean {
return this.gridCell.canClick;
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
this.canClick ? { backgroundColor: layers[this.layer].color } : {},
layers[this.layer].componentStyles?.["grid-cell"],
this.gridCell.style
];
},
title(): Component | string | null {
if (this.gridCell.title) {
return coerceComponent(this.gridCell.title, "h3");
}
return null;
},
display(): Component | string {
return coerceComponent(this.gridCell.display, "div");
}
},
methods: {
start() {
if (!this.interval && this.gridCell.click) {
this.interval = setInterval(this.gridCell.click, 250);
}
},
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
this.time = 0;
}
}
}
}); });
const component = computed(() => coerceComponent(unref(props.display)));
</script> </script>
<style scoped> <style scoped>
@ -93,5 +42,6 @@ export default defineComponent({
min-height: 80px; min-height: 80px;
width: 80px; width: 80px;
font-size: 10px; font-size: 10px;
background-color: var(--layer-color);
} }
</style> </style>

View file

@ -1,84 +1,43 @@
<template> <template>
<div class="infobox" v-if="infobox.unlocked" :style="style" :class="{ collapsed, stacked }"> <div
<button class="title" :style="titleStyle" @click="toggle"> class="infobox"
v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:style="[{ borderColor: color }, style || []]"
:class="{ collapsed, stacked, ...classes }"
>
<button
class="title"
:style="[{ backgroundColor: color }, titleStyle || []]"
@click="collapsed = !collapsed"
>
<span class="toggle"></span> <span class="toggle"></span>
<component :is="title" /> <component :is="titleComponent" />
</button> </button>
<collapse-transition> <CollapseTransition>
<div v-if="!collapsed" class="body" :style="{ backgroundColor: borderColor }"> <div v-if="!collapsed" class="body" :style="{ backgroundColor: color }">
<component :is="body" :style="bodyStyle" /> <component :is="bodyComponent" :style="bodyStyle" />
</div> </div>
</collapse-transition> </CollapseTransition>
<branch-node :branches="infobox.branches" :id="id" featureType="infobox" /> <LinkNode :id="id" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import themes from "@/data/themes"; import themes from "@/data/themes";
import { layers } from "@/game/layers"; import { FeatureComponent, Visibility } from "@/features/feature";
import player from "@/game/player"; import { GenericInfobox } from "@/features/infobox";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { Infobox } from "@/typings/features/infobox"; import { coerceComponent } from "@/util/vue";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { computed, toRefs, unref } from "vue";
import { Component, defineComponent } from "vue"; import LinkNode from "../system/LinkNode.vue";
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericInfobox>>());
name: "infobox",
mixins: [InjectLayerMixin], const titleComponent = computed(() => coerceComponent(unref(props.title)));
props: { const bodyComponent = computed(() => coerceComponent(unref(props.display)));
id: { const stacked = computed(() => themes[settings.theme].stackedInfoboxes);
type: [Number, String],
required: true
}
},
computed: {
infobox(): Infobox {
return layers[this.layer].infoboxes!.data[this.id];
},
borderColor(): string {
return this.infobox.borderColor || layers[this.layer].color;
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
{ borderColor: this.borderColor },
layers[this.layer].componentStyles?.infobox,
this.infobox.style
];
},
titleStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
{ backgroundColor: layers[this.layer].color },
layers[this.layer].componentStyles?.["infobox-title"],
this.infobox.titleStyle
];
},
bodyStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [layers[this.layer].componentStyles?.["infobox-body"], this.infobox.bodyStyle];
},
title(): Component | string {
if (this.infobox.title) {
return coerceComponent(this.infobox.title);
}
return coerceComponent(layers[this.layer].name || this.layer);
},
body(): Component | string {
return coerceComponent(this.infobox.body);
},
collapsed(): boolean {
return player.layers[this.layer].infoboxes[this.id];
},
stacked(): boolean {
return themes[settings.theme].stackedInfoboxes;
}
},
methods: {
toggle() {
player.layers[this.layer].infoboxes[this.id] = !player.layers[this.layer].infoboxes[
this.id
];
}
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,52 +1,39 @@
<template> <template>
<div> <div>
<span v-if="showPrefix">You have </span> <span v-if="showPrefix">You have </span>
<resource :amount="amount" :color="color" /> <ResourceVue :resource="resource" :color="color || 'white'" />
{{ resource {{ resource
}}<!-- remove whitespace --> }}<!-- remove whitespace -->
<span v-if="effectDisplay">, <component :is="effectDisplay"/></span> <span v-if="effectComponent">, <component :is="effectComponent"/></span>
<br /><br /> <br /><br />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { layers } from "@/game/layers"; import { CoercableComponent } from "@/features/feature";
import player from "@/game/player"; import { Resource } from "@/features/resource";
import { format, formatWhole } from "@/util/bignum"; import Decimal from "@/util/bignum";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, StyleValue, toRefs } from "vue";
import ResourceVue from "../system/Resource.vue";
export default defineComponent({ const props = toRefs(
name: "main-display", defineProps<{
mixins: [InjectLayerMixin], resource: Resource;
props: { color?: string;
precision: Number classes?: Record<string, boolean>;
}, style?: StyleValue;
computed: { effectDisplay?: CoercableComponent;
style(): Partial<CSSStyleDeclaration> | undefined { }>()
return layers[this.layer].componentStyles?.["main-display"]; );
},
resource(): string { const effectComponent = computed(() => {
return layers[this.layer].resource; const effectDisplay = props.effectDisplay?.value;
}, return effectDisplay && coerceComponent(effectDisplay);
effectDisplay(): Component | string | undefined { });
return (
layers[this.layer].effectDisplay && const showPrefix = computed(() => {
coerceComponent(layers[this.layer].effectDisplay!) return Decimal.lt(props.resource.value, "1e1000");
);
},
showPrefix(): boolean {
return player.layers[this.layer].points.lt("1e1000");
},
color(): string {
return layers[this.layer].color;
},
amount(): string {
return this.precision == undefined
? formatWhole(player.layers[this.layer].points)
: format(player.layers[this.layer].points, this.precision);
}
}
}); });
</script> </script>

View file

@ -5,15 +5,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; defineProps<{ mark: boolean | string | undefined }>();
export default defineComponent({
name: "mark-node",
props: {
mark: [Boolean, String]
}
});
</script> </script>
<style scoped> <style scoped>

View file

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

View file

@ -1,57 +1,45 @@
<template> <template>
<div <div
v-if="milestone.shown" v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:style="style" :style="style"
:class="{ feature: true, milestone: true, done: milestone.earned }" :class="{ feature: true, milestone: true, done: earned, ...classes }"
> >
<div v-if="requirementDisplay"><component :is="requirementDisplay" /></div> <component v-if="component" :is="component" />
<div v-if="effectDisplay"><component :is="effectDisplay" /></div> <LinkNode :id="id" />
<component v-if="optionsDisplay" :is="optionsDisplay" />
<branch-node :branches="milestone.branches" :id="id" featureType="milestone" />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="tsx">
import { layers } from "@/game/layers"; import { FeatureComponent, Visibility } from "@/features/feature";
import { Milestone } from "@/typings/features/milestone"; import { GenericMilestone } from "@/features/milestone";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { Component, defineComponent } from "vue"; import { computed, toRefs } from "vue";
import LinkNode from "../system/LinkNode.vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericMilestone>>());
name: "milestone",
mixins: [InjectLayerMixin], const component = computed(() => {
props: { const display = props.display.value;
id: { if (display == null) {
type: [Number, String],
required: true
}
},
computed: {
milestone(): Milestone {
return layers[this.layer].milestones!.data[this.id];
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [layers[this.layer].componentStyles?.milestone, this.milestone.style];
},
requirementDisplay(): Component | string | null {
if (this.milestone.requirementDisplay) {
return coerceComponent(this.milestone.requirementDisplay, "h3");
}
return null;
},
effectDisplay(): Component | string | null {
if (this.milestone.effectDisplay) {
return coerceComponent(this.milestone.effectDisplay, "b");
}
return null;
},
optionsDisplay(): Component | string | null {
if (this.milestone.optionsDisplay && this.milestone.earned) {
return coerceComponent(this.milestone.optionsDisplay, "div");
}
return null; return null;
} }
if (isCoercableComponent(display)) {
return coerceComponent(display);
} }
return (
<span>
<component v-is={coerceComponent(display.requirement, "h3")} />
<div v-if={display.effectDisplay}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.effectDisplay!, "b")} />
</div>
<div v-if={display.optionsDisplay}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.optionsDisplay!, "span")} />
</div>
</span>
);
}); });
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

@ -1,64 +1,65 @@
<template> <template>
<button <button
v-if="upgrade.unlocked" v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:style="style" :style="style"
@click="buy" @click="purchase"
:class="{ :class="{
feature: true, feature: true,
[layer]: true,
upgrade: true, upgrade: true,
can: upgrade.canAfford && !upgrade.bought, can: canPurchase && !bought,
locked: !upgrade.canAfford && !upgrade.bought, locked: !canPurchase && !bought,
bought: upgrade.bought bought,
...classes
}" }"
:disabled="!upgrade.canAfford && !upgrade.bought" :disabled="!canPurchase && !bought"
> >
<component v-if="fullDisplay" :is="fullDisplay" /> <component v-if="component" :is="component" />
<default-upgrade-display v-else :id="id" /> <MarkNode :mark="mark" />
<branch-node :branches="upgrade.branches" :id="id" featureType="upgrade" /> <LinkNode :id="id" />
</button> </button>
</template> </template>
<script lang="ts"> <script setup lang="tsx">
import { layers } from "@/game/layers"; import { FeatureComponent, Visibility } from "@/features/feature";
import { Upgrade } from "@/typings/features/upgrade"; import { displayResource } from "@/features/resource";
import { coerceComponent, InjectLayerMixin } from "@/util/vue"; import { GenericUpgrade } from "@/features/upgrade";
import { Component, defineComponent } from "vue"; import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { computed, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<GenericUpgrade>>());
name: "upgrade",
mixins: [InjectLayerMixin], const component = computed(() => {
props: { const display = unref(props.display);
id: { if (display == null) {
type: [Number, String],
required: true
}
},
computed: {
upgrade(): Upgrade {
return layers[this.layer].upgrades!.data[this.id];
},
style(): Array<Partial<CSSStyleDeclaration> | undefined> {
return [
this.upgrade.canAfford && !this.upgrade.bought
? { backgroundColor: layers[this.layer].color }
: undefined,
layers[this.layer].componentStyles?.upgrade,
this.upgrade.style
];
},
fullDisplay(): Component | string | null {
if (this.upgrade.fullDisplay) {
return coerceComponent(this.upgrade.fullDisplay, "div");
}
return null; return null;
} }
}, if (isCoercableComponent(display)) {
methods: { return coerceComponent(display);
buy() {
this.upgrade.buy();
}
} }
return (
<span>
<div v-if={display.title}>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
<component v-is={coerceComponent(display.title!, "h2")} />
</div>
<component v-is={coerceComponent(display.description, "div")} />
<div v-if={display.effectDisplay}>
<br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
Currently: <component v-is={coerceComponent(display.effectDisplay!)} />
</div>
<template v-if={unref(props.resource) != null && unref(props.cost) != null}>
<br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
Cost: {displayResource(unref(props.resource)!, unref(props.cost))}{" "}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{unref(props.resource)!.displayName}
</template>
</span>
);
}); });
</script> </script>

View file

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

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

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

View file

@ -6,7 +6,7 @@
:transform="`translate(${position.x},${position.y})`" :transform="`translate(${position.x},${position.y})`"
> >
<transition name="actions" appear> <transition name="actions" appear>
<g v-if="selected && actions"> <g v-if="isSelected && actions">
<!-- TODO move to separate file --> <!-- TODO move to separate file -->
<g <g
v-for="(action, index) in actions" v-for="(action, index) in actions"
@ -27,19 +27,13 @@
@touchend.stop="e => actionMouseUp(e, action)" @touchend.stop="e => actionMouseUp(e, action)"
> >
<circle <circle
:fill=" :fill="getNodeProperty(action.fillColor, node)"
action.fillColor
? typeof action.fillColor === 'function'
? action.fillColor(node)
: action.fillColor
: fillColor
"
r="20" r="20"
:stroke-width="selectedAction?.id === action.id ? 4 : 0" :stroke-width="selectedAction?.id === action.id ? 4 : 0"
:stroke="outlineColor" :stroke="outlineColor"
/> />
<text :fill="titleColor" class="material-icons">{{ <text :fill="titleColor" class="material-icons">{{
typeof action.icon === "function" ? action.icon(node) : action.icon getNodeProperty(action.icon, node)
}}</text> }}</text>
</g> </g>
</g> </g>
@ -47,8 +41,8 @@
<g <g
class="node-container" class="node-container"
@mouseenter="mouseEnter" @mouseenter="isHovering = true"
@mouseleave="mouseLeave" @mouseleave="isHovering = false"
@mousedown="mouseDown" @mousedown="mouseDown"
@touchstart="mouseDown" @touchstart="mouseDown"
@mouseup="mouseUp" @mouseup="mouseUp"
@ -163,7 +157,7 @@
<transition name="fade" appear> <transition name="fade" appear>
<text <text
v-if="selected && selectedAction" v-if="isSelected && selectedAction"
:fill="titleColor" :fill="titleColor"
class="node-title" class="node-title"
:y="size + 75" :y="size + 75"
@ -173,192 +167,140 @@
</g> </g>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import themes from "@/data/themes"; import themes from "@/data/themes";
import { ProgressDisplay, Shape } from "@/game/enums"; import {
import { layers } from "@/game/layers"; BoardNode,
import player from "@/game/player"; GenericBoardNodeAction,
import settings from "@/game/settings"; GenericNodeType,
import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board"; getNodeProperty,
import { getNodeTypeProperty } from "@/util/features";
import { defineComponent, PropType } from "vue";
export default defineComponent({
name: "BoardNode",
data() {
return {
ProgressDisplay, ProgressDisplay,
Shape, Shape
hovering: false, } from "@/features/board";
sqrtTwo: Math.sqrt(2) import { Visibility } from "@/features/feature";
import settings from "@/game/settings";
import { computed, ref, toRefs, unref, watch } from "vue";
const sqrtTwo = Math.sqrt(2);
const props = toRefs(
defineProps<{
node: BoardNode;
nodeType: GenericNodeType;
dragging?: BoardNode;
dragged?: {
x: number;
y: number;
}; };
}, hasDragged?: boolean;
emits: ["mouseDown", "endDragging"], receivingNode?: boolean;
props: { selectedNode?: BoardNode | null;
node: { selectedAction?: GenericBoardNodeAction | null;
type: Object as PropType<BoardNode>, }>()
required: true );
}, const emit = defineEmits<{
nodeType: { (e: "mouseDown", event: MouseEvent | TouchEvent, node: number, isDraggable: boolean): void;
type: Object as PropType<NodeType>, (e: "endDragging", node: number): void;
required: true }>();
},
dragging: { const isHovering = ref(false);
type: Object as PropType<BoardNode> const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
}, const isDraggable = computed(() =>
dragged: { getNodeProperty(props.nodeType.value.draggable, unref(props.node))
type: Object as PropType<{ x: number; y: number }>, );
required: true
}, watch(isDraggable, value => {
hasDragged: { const node = unref(props.node);
type: Boolean, if (unref(props.dragging) === node && !value) {
default: false emit("endDragging", node.id);
},
receivingNode: {
type: Boolean,
default: false
}
},
computed: {
board() {
return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
},
selected() {
return this.board.selectedNode === this.node;
},
selectedAction() {
return this.board.selectedAction;
},
actions(): BoardNodeAction[] | null | undefined {
const actions = getNodeTypeProperty(this.nodeType, this.node, "actions") as
| BoardNodeAction[]
| null
| undefined;
if (actions) {
return actions.filter(action => {
if (action.enabled == null) {
return true;
}
if (typeof action.enabled === "function") {
return action.enabled();
}
return action.enabled;
});
}
return null;
},
draggable(): boolean {
return getNodeTypeProperty(this.nodeType, this.node, "draggable");
},
position(): { x: number; y: number } {
return this.draggable && this.dragging?.id === this.node.id
? {
x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
}
: this.node.position;
},
shape(): Shape {
return getNodeTypeProperty(this.nodeType, this.node, "shape");
},
size(): number {
let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
if (this.receivingNode) {
size *= 1.25;
} else if (this.hovering || this.selected) {
size *= 1.15;
}
return size;
},
title(): string {
return getNodeTypeProperty(this.nodeType, this.node, "title");
},
label(): NodeLabel | null | undefined {
return getNodeTypeProperty(this.nodeType, this.node, "label");
},
progress(): number {
return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
},
backgroundColor(): string {
return themes[settings.theme].variables["--background"];
},
outlineColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
themes[settings.theme].variables["--outline"]
);
},
fillColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
themes[settings.theme].variables["--raised-background"]
);
},
progressColor(): string {
return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
},
titleColor(): string {
return (
getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
themes[settings.theme].variables["--foreground"]
);
},
progressDisplay(): ProgressDisplay {
return (
getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
ProgressDisplay.Outline
);
},
canAccept(): boolean {
if (this.dragging == null || !this.hasDragged) {
return false;
}
return typeof this.nodeType.canAccept === "boolean"
? this.nodeType.canAccept
: this.nodeType.canAccept(this.node, this.dragging);
},
actionDistance(): number {
return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
}
},
methods: {
mouseDown(e: MouseEvent | TouchEvent) {
this.$emit("mouseDown", e, this.node.id, this.draggable);
},
mouseUp() {
if (!this.hasDragged) {
this.nodeType.onClick?.(this.node);
}
},
mouseEnter() {
this.hovering = true;
},
mouseLeave() {
this.hovering = false;
},
performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
// If the onClick function made this action selected,
// don't propagate the event (which will deselect everything)
if (action.onClick(this.node) || this.board.selectedAction?.id === action.id) {
e.preventDefault();
e.stopPropagation();
}
},
actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
if (this.board.selectedAction?.id === action.id) {
e.preventDefault();
e.stopPropagation();
}
}
},
watch: {
onDraggableChanged() {
if (this.dragging === this.node && !this.draggable) {
this.$emit("endDragging", this.node.id);
}
}
} }
}); });
const actions = computed(() => {
const node = unref(props.node);
return getNodeProperty(props.nodeType.value.actions, node)?.filter(
action => getNodeProperty(action.visibility, node) !== Visibility.None
);
});
const position = computed(() => {
const node = unref(props.node);
const dragged = unref(props.dragged);
return getNodeProperty(props.nodeType.value.draggable, node) &&
unref(props.dragging)?.id === node.id &&
dragged
? {
x: node.position.x + Math.round(dragged.x / 25) * 25,
y: node.position.y + Math.round(dragged.y / 25) * 25
}
: node.position;
});
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
const label = computed(() => getNodeProperty(props.nodeType.value.label, unref(props.node)));
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
const progress = computed(
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) || 0
);
const backgroundColor = computed(() => themes[settings.theme].variables["--background"]);
const outlineColor = computed(
() =>
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ||
themes[settings.theme].variables["--outline"]
);
const fillColor = computed(
() =>
getNodeProperty(props.nodeType.value.fillColor, unref(props.node)) ||
themes[settings.theme].variables["--raised-background"]
);
const progressColor = computed(() =>
getNodeProperty(props.nodeType.value.progressColor, unref(props.node))
);
const titleColor = computed(
() =>
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ||
themes[settings.theme].variables["--foreground"]
);
const progressDisplay = computed(() =>
getNodeProperty(props.nodeType.value.progressDisplay, unref(props.node))
);
const canAccept = computed(
() =>
unref(props.dragging) != null &&
unref(props.hasDragged) &&
getNodeProperty(props.nodeType.value.canAccept, unref(props.node))
);
const actionDistance = computed(() =>
getNodeProperty(props.nodeType.value.actionDistance, unref(props.node))
);
function mouseDown(e: MouseEvent | TouchEvent) {
emit("mouseDown", e, props.node.value.id, isDraggable.value);
}
function mouseUp() {
if (!props.hasDragged?.value) {
props.nodeType.value.onClick?.(props.node.value);
}
}
function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
// If the onClick function made this action selected,
// don't propagate the event (which will deselect everything)
if (action.onClick(unref(props.node)) || unref(props.selectedAction)?.id === action.id) {
e.preventDefault();
e.stopPropagation();
}
}
function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
if (unref(props.selectedAction)?.id === action.id) {
e.preventDefault();
e.stopPropagation();
}
}
</script> </script>
<style scoped> <style scoped>

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

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

View file

@ -1,50 +1,48 @@
<template> <template>
<span class="container" :class="{ confirming }"> <span class="container" :class="{ confirming: isConfirming }">
<span v-if="confirming">Are you sure?</span> <span v-if="isConfirming">Are you sure?</span>
<button @click.stop="click" class="button danger" :disabled="disabled"> <button @click.stop="click" class="button danger" :disabled="disabled">
<span v-if="confirming">Yes</span> <span v-if="isConfirming">Yes</span>
<slot v-else /> <slot v-else />
</button> </button>
<button v-if="confirming" class="button" @click.stop="cancel">No</button> <button v-if="isConfirming" class="button" @click.stop="cancel">No</button>
</span> </span>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { ref, toRefs, unref, watch } from "vue";
export default defineComponent({ const props = toRefs(
name: "danger-button", defineProps<{
data() { disabled?: boolean;
return { skipConfirm?: boolean;
confirming: false }>()
}; );
}, const emit = defineEmits<{
props: { (e: "click"): void;
disabled: Boolean, (e: "confirmingChanged", value: boolean): void;
skipConfirm: Boolean }>();
},
emits: ["click", "confirmingChanged"], const isConfirming = ref(false);
watch: {
confirming(newValue) { watch(isConfirming, isConfirming => {
this.$emit("confirmingChanged", newValue); emit("confirmingChanged", isConfirming);
} });
},
methods: { function click() {
click() { if (unref(props.skipConfirm)) {
if (this.skipConfirm) { emit("click");
this.$emit("click");
return; return;
} }
if (this.confirming) { if (isConfirming.value) {
this.$emit("click"); emit("click");
} }
this.confirming = !this.confirming; isConfirming.value = !isConfirming.value;
}, }
cancel() {
this.confirming = false; function cancel() {
} isConfirming.value = false;
} }
});
</script> </script>
<style scoped> <style scoped>

View file

@ -4,40 +4,34 @@
</button> </button>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { nextTick, ref, toRefs } from "vue";
export default defineComponent({ toRefs(
name: "feedback-button", defineProps<{
data() { left?: boolean;
return { }>()
activated: false, );
activatedTimeout: null const emit = defineEmits<{
} as { (e: "click"): void;
activated: boolean; }>();
activatedTimeout: number | null;
}; const activated = ref(false);
}, const activatedTimeout = ref<number | null>(null);
props: {
left: Boolean function click() {
}, emit("click");
emits: ["click"],
methods: {
click() {
this.$emit("click");
// Give feedback to user // Give feedback to user
if (this.activatedTimeout) { if (activatedTimeout.value) {
clearTimeout(this.activatedTimeout); clearTimeout(activatedTimeout.value);
} }
this.activated = false; activated.value = false;
this.$nextTick(() => { nextTick(() => {
this.activated = true; activated.value = true;
this.activatedTimeout = setTimeout(() => (this.activated = false), 500); activatedTimeout.value = setTimeout(() => (activated.value = false), 500);
}); });
} }
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,40 +1,49 @@
<template> <template>
<div class="field"> <div class="field">
<span class="field-title" v-if="title">{{ title }}</span> <span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
<vue-select <VueNextSelect
:options="options" :options="options"
:model-value="value" v-model="value"
@update:modelValue="setSelected"
label-by="label" label-by="label"
:value-by="getValue" :reduce="(option: SelectOption) => option.value"
:placeholder="placeholder" :placeholder="placeholder"
:close-on-select="closeOnSelect" :close-on-select="closeOnSelect"
/> />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue";
import { computed, toRefs, unref } from "vue";
import VueNextSelect from "vue-next-select";
import "vue-next-select/dist/index.css";
export default defineComponent({ export type SelectOption = { label: string; value: unknown };
name: "Select",
props: { const props = toRefs(
title: String, defineProps<{
options: Array, // https://vue-select.org/guide/options.html#options-prop title?: CoercableComponent;
value: [String, Object], modelValue?: unknown;
default: [String, Object], options: SelectOption[];
placeholder: String, placeholder?: string;
closeOnSelect: Boolean closeOnSelect?: boolean;
}>()
);
const emit = defineEmits<{
(e: "update:modelValue", value: unknown): void;
}>();
const titleComponent = computed(
() => props.title?.value && coerceComponent(props.title.value, "span")
);
const value = computed({
get() {
return unref(props.modelValue);
}, },
emits: ["change"], set(value: unknown) {
methods: { emit("update:modelValue", value);
setSelected(value: any) {
value = value || this.default;
this.$emit("change", value);
},
getValue(item?: { value: any }) {
return item?.value;
}
} }
}); });
</script> </script>

View file

@ -1,30 +1,35 @@
<template> <template>
<div class="field"> <div class="field">
<span class="field-title" v-if="title">{{ title }}</span> <span class="field-title" v-if="title">{{ title }}</span>
<tooltip :display="`${value}`" :class="{ fullWidth: !title }"> <Tooltip :display="`${value}`" :class="{ fullWidth: !title }">
<input <input type="range" v-model="value" :min="min" :max="max" />
type="range" </Tooltip>
:value="value"
@input="e => $emit('change', parseInt(e.target.value))"
:min="min"
:max="max"
/>
</tooltip>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { computed, toRefs, unref } from "vue";
import Tooltip from "../system/Tooltip.vue";
export default defineComponent({ const props = toRefs(
name: "Slider", defineProps<{
props: { title?: string;
title: String, modelValue?: number;
value: Number, min?: number;
min: Number, max?: number;
max: Number }>()
);
const emit = defineEmits<{
(e: "update:modelValue", value: number): void;
}>();
const value = computed({
get() {
return unref(props.modelValue) || 0;
}, },
emits: ["change"] set(value: number) {
emit("update:modelValue", value);
}
}); });
</script> </script>

View file

@ -1,59 +1,69 @@
<template> <template>
<form @submit.prevent="$emit('submit')"> <form @submit.prevent="submit">
<div class="field"> <div class="field">
<span class="field-title" v-if="title">{{ title }}</span> <span class="field-title" v-if="titleComponent"><component :is="titleComponent"/></span>
<textarea-autosize <VueTextareaAutosize
v-if="textarea" v-if="textArea"
v-model="value"
:placeholder="placeholder" :placeholder="placeholder"
:value="val"
:maxHeight="maxHeight" :maxHeight="maxHeight"
@input="change" @blur="submit"
@blur="() => $emit('blur')"
ref="field" ref="field"
/> />
<input <input
v-else v-else
type="text" type="text"
:value="val" v-model="value"
@input="e => change(e.target.value)"
@blur="() => $emit('blur')"
:placeholder="placeholder" :placeholder="placeholder"
ref="field"
:class="{ fullWidth: !title }" :class="{ fullWidth: !title }"
@blur="submit"
ref="field"
/> />
</div> </div>
</form> </form>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue";
import { computed, onMounted, ref, toRefs, unref } from "vue";
import VueTextareaAutosize from "vue-textarea-autosize";
export default defineComponent({ const props = toRefs(
name: "TextField", defineProps<{
props: { title?: CoercableComponent;
title: String, modelValue?: string;
value: String, textArea?: boolean;
modelValue: String, placeholder?: string;
textarea: Boolean, maxHeight?: number;
placeholder: String, }>()
maxHeight: Number );
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
(e: "submit"): void;
}>();
const titleComponent = computed(
() => props.title?.value && coerceComponent(unref(props.title.value), "span")
);
const field = ref<HTMLElement | null>(null);
onMounted(() => {
field.value?.focus();
});
const value = computed({
get() {
return unref(props.modelValue) || "";
}, },
emits: ["change", "submit", "blur", "update:modelValue"], set(value: string) {
mounted() { emit("update:modelValue", value);
(this.$refs.field as HTMLElement).focus();
},
computed: {
val() {
return this.modelValue || this.value || "";
}
},
methods: {
change(value: string) {
this.$emit("change", value);
this.$emit("update:modelValue", value);
}
} }
}); });
function submit() {
emit("submit");
}
</script> </script>
<style scoped> <style scoped>

View file

@ -1,31 +1,33 @@
<template> <template>
<label class="field"> <label class="field">
<input type="checkbox" class="toggle" :checked="value" @input="handleInput" /> <input type="checkbox" class="toggle" v-model="value" />
<component :is="display" /> <component :is="component" />
</label> </label>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { defineComponent } from "vue"; import { computed, toRefs, unref } from "vue";
// Reference: https://codepen.io/finnhvman/pen/pOeyjE const props = toRefs(
export default defineComponent({ defineProps<{
name: "Toggle", title?: CoercableComponent;
props: { modelValue?: boolean;
title: String, }>()
value: Boolean );
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
const component = computed(() => coerceComponent(unref(props.title) || "", "span"));
const value = computed({
get() {
return !!unref(props.modelValue);
}, },
emits: ["change"], set(value: boolean) {
computed: { emit("update:modelValue", value);
display() {
return coerceComponent(this.title || "", "span");
}
},
methods: {
handleInput(e: InputEvent) {
this.$emit("change", (e.target as HTMLInputElement).checked);
}
} }
}); });
</script> </script>

View file

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

View file

@ -6,10 +6,4 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts"></script>
import { defineComponent } from "vue";
export default defineComponent({
name: "column"
});
</script>

View file

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

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

View file

@ -1,8 +1,8 @@
<template> <template>
<Modal :show="show"> <Modal :model-value="isOpen">
<template v-slot:header> <template v-slot:header>
<div class="game-over-modal-header"> <div class="game-over-modal-header">
<img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="modInfo" /> <img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="title" />
<div class="game-over-modal-title"> <div class="game-over-modal-title">
<h2>Congratulations!</h2> <h2>Congratulations!</h2>
<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4> <h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
@ -35,43 +35,27 @@
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import Modal from "@/components/system/Modal.vue";
import { hasWon } from "@/data/mod"; import { hasWon } from "@/data/mod";
import modInfo from "@/data/modInfo.json"; import modInfo from "@/data/modInfo.json";
import player from "@/game/player"; import player from "@/game/player";
import { formatTime } from "@/util/bignum"; import { formatTime } from "@/util/bignum";
import { defineComponent } from "vue"; import { loadSave, newSave } from "@/util/save";
import { computed } from "vue";
export default defineComponent({ const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
name: "GameOverScreen",
data() { const timePlayed = computed(() => formatTime(player.timePlayed));
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo; const isOpen = computed(() => hasWon.value && !player.keepGoing);
return {
title, function keepGoing() {
logo,
discordName,
discordLink,
versionNumber,
versionTitle
};
},
computed: {
timePlayed() {
return formatTime(player.timePlayed);
},
show() {
return hasWon.value && !player.keepGoing;
}
},
methods: {
keepGoing() {
player.keepGoing = true; player.keepGoing = true;
}, }
playAgain() {
console.warn("Not yet implemented!"); function playAgain() {
} loadSave(newSave());
} }
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,5 +1,5 @@
<template> <template>
<Modal :show="show" @close="$emit('closeDialog', 'Info')"> <Modal v-model="isOpen">
<template v-slot:header> <template v-slot:header>
<div class="info-modal-header"> <div class="info-modal-header">
<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" /> <img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
@ -17,7 +17,7 @@
Aarex Aarex
</div> </div>
<br /> <br />
<div class="link" @click="$emit('openDialog', 'Changelog')"> <div class="link" @click="openChangelog">
Changelog Changelog
</div> </div>
<br /> <br />
@ -51,60 +51,36 @@
</div> </div>
<br /> <br />
<div>Time Played: {{ timePlayed }}</div> <div>Time Played: {{ timePlayed }}</div>
<div v-if="hotkeys.length > 0">
<br />
<h4>Hotkeys</h4>
<div v-for="key in hotkeys" :key="key.key">
{{ key.key }}: {{ key.description }}
</div>
</div>
</div> </div>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import Modal from "@/components/system/Modal.vue";
import type Changelog from "@/data/Changelog.vue";
import modInfo from "@/data/modInfo.json"; import modInfo from "@/data/modInfo.json";
import { hotkeys } from "@/game/layers";
import player from "@/game/player"; import player from "@/game/player";
import { formatTime } from "@/util/bignum"; import { formatTime } from "@/util/bignum";
import { defineComponent } from "vue"; import { computed, ref, toRefs, unref } from "vue";
export default defineComponent({ const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = modInfo;
name: "Info",
data() { const props = toRefs(defineProps<{ changelog: typeof Changelog | null }>());
const {
title, const isOpen = ref(false);
logo,
author, const timePlayed = computed(() => formatTime(player.timePlayed));
discordName,
discordLink, defineExpose({
versionNumber, open() {
versionTitle isOpen.value = true;
} = modInfo;
return {
title,
logo,
author,
discordName,
discordLink,
versionNumber,
versionTitle
};
},
props: {
show: Boolean
},
emits: ["closeDialog", "openDialog"],
computed: {
timePlayed() {
return formatTime(player.timePlayed);
},
hotkeys() {
return hotkeys.filter(hotkey => hotkey.unlocked);
}
} }
}); });
function openChangelog() {
unref(props.changelog)?.open();
}
</script> </script>
<style scoped> <style scoped>

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

View file

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

View file

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

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

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

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

View file

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

View file

@ -7,32 +7,29 @@
> >
<div <div
class="modal-mask" class="modal-mask"
v-show="show" v-show="modelValue"
v-on:pointerdown.self="$emit('close')" v-on:pointerdown.self="close"
v-bind="$attrs" v-bind="$attrs"
> >
<div class="modal-wrapper"> <div class="modal-wrapper">
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header">
<slot name="header" :shown="isVisible"> <slot name="header" :shown="isOpen">
default header default header
</slot> </slot>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<branches> <branches>
<slot name="body" :shown="isVisible"> <slot name="body" :shown="isOpen">
default body default body
</slot> </slot>
</branches> </branches>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<slot name="footer" :shown="isVisible"> <slot name="footer" :shown="isOpen">
<div class="modal-default-footer"> <div class="modal-default-footer">
<div class="modal-default-flex-grow"></div> <div class="modal-default-flex-grow"></div>
<button <button class="button modal-default-button" @click="close">
class="button modal-default-button"
@click="$emit('close')"
>
Close Close
</button> </button>
</div> </div>
@ -45,31 +42,25 @@
</teleport> </teleport>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { computed, ref } from "vue";
export default defineComponent({ const props = defineProps<{
name: "Modal", modelValue: boolean;
data() { }>();
return { const emit = defineEmits<{
isAnimating: false (e: "update:modelValue", value: boolean): void;
}; }>();
},
props: { const isOpen = computed(() => props.modelValue || isAnimating.value);
show: Boolean function close() {
}, emit("update:modelValue", false);
emits: ["close"], }
computed: {
isVisible(): boolean { const isAnimating = ref(false);
return this.show || this.isAnimating; function setAnimating(value: boolean) {
} isAnimating.value = value;
}, }
methods: {
setAnimating(isAnimating: boolean) {
this.isAnimating = isAnimating;
}
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,5 +1,5 @@
<template> <template>
<Modal :show="hasNaN" v-bind="$attrs"> <Modal v-model="hasNaN" v-bind="$attrs">
<template v-slot:header> <template v-slot:header>
<div class="nan-modal-header"> <div class="nan-modal-header">
<h2>NaN value detected!</h2> <h2>NaN value detected!</h2>
@ -7,9 +7,10 @@
</template> </template>
<template v-slot:body> <template v-slot:body>
<div> <div>
Attempted to assign "{{ path }}" to NaN (previously {{ format(previous) }}). Attempted to assign "{{ path }}" to NaN<span v-if="previous">
Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console for {{ " " }}(previously {{ format(previous) }})</span
more details, and consider sharing it with the developers on discord. >. Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console
for more details, and consider sharing it with the developers on discord.
</div> </div>
<br /> <br />
<div> <div>
@ -19,20 +20,20 @@
</a> </a>
</div> </div>
<br /> <br />
<Toggle title="Autosave" :value="autosave" @change="setAutosave" /> <Toggle title="Autosave" v-model="autosave" />
<Toggle title="Pause game" :value="paused" @change="togglePaused" /> <Toggle title="Pause game" v-model="isPaused" />
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<div class="nan-footer"> <div class="nan-footer">
<button @click="toggleSavesManager" class="button"> <button @click="savesManager?.open()" class="button">
Open Saves Manager Open Saves Manager
</button> </button>
<button @click="setZero" class="button">Set to 0</button> <button @click="setZero" class="button">Set to 0</button>
<button @click="setOne" class="button">Set to 1</button> <button @click="setOne" class="button">Set to 1</button>
<button <button
@click="setPrev" @click="hasNaN = false"
class="button" class="button"
v-if="previous && previous.neq(0) && previous.neq(1)" v-if="previous && Decimal.neq(previous, 0) && Decimal.neq(previous, 1)"
> >
Set to previous Set to previous
</button> </button>
@ -40,75 +41,61 @@
</div> </div>
</template> </template>
</Modal> </Modal>
<SavesManager :show="showSaves" @closeDialog="toggleSavesManager" /> <SavesManager ref="savesManager" />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import Modal from "@/components/system/Modal.vue";
import modInfo from "@/data/modInfo.json"; import modInfo from "@/data/modInfo.json";
import player from "@/game/player"; import player from "@/game/player";
import state from "@/game/state"; import state from "@/game/state";
import Decimal, { format } from "@/util/bignum"; import Decimal, { DecimalSource, format } from "@/util/bignum";
import { mapPlayer, mapState } from "@/util/vue"; import { computed, ref, toRef } from "vue";
import { defineComponent } from "vue"; import Toggle from "../fields/Toggle.vue";
import SavesManager from "./SavesManager.vue";
export default defineComponent({ const { discordName, discordLink } = modInfo;
name: "NaNScreen", const autosave = toRef(player, "autosave");
data() { const hasNaN = toRef(state, "hasNaN");
const { discordName, discordLink } = modInfo; const savesManager = ref<typeof SavesManager | null>(null);
return { discordName, discordLink, format, showSaves: false };
}, const path = computed(() => state.NaNPath?.join("."));
computed: { const property = computed(() => state.NaNPath?.slice(-1)[0]);
...mapPlayer(["autosave"]), const previous = computed<DecimalSource | null>(() => {
...mapState(["hasNaN"]), if (state.NaNReceiver && property.value) {
path(): string | undefined { return state.NaNReceiver[property.value] as DecimalSource;
return state.NaNPath?.join(".");
},
previous(): unknown {
if (state.NaNReceiver && this.property) {
return state.NaNReceiver[this.property];
} }
return null; return null;
}, });
paused() { const isPaused = computed({
get() {
return player.devSpeed === 0; return player.devSpeed === 0;
}, },
property(): string | undefined { set(value: boolean) {
return state.NaNPath?.slice(-1)[0]; player.devSpeed = value ? null : 0;
}
},
methods: {
setZero() {
if (state.NaNReceiver && this.property) {
state.NaNReceiver[this.property] = new Decimal(0);
state.hasNaN = false;
}
},
setOne() {
if (state.NaNReceiver && this.property) {
state.NaNReceiver[this.property] = new Decimal(1);
state.hasNaN = false;
}
},
setPrev() {
state.hasNaN = false;
},
ignore() {
if (state.NaNReceiver && this.property) {
state.NaNReceiver[this.property] = new Decimal(NaN);
state.hasNaN = false;
}
},
setAutosave(autosave: boolean) {
player.autosave = autosave;
},
toggleSavesManager() {
this.showSaves = !this.showSaves;
},
togglePaused() {
player.devSpeed = this.paused ? 1 : 0;
}
} }
}); });
function setZero() {
if (state.NaNReceiver && property.value) {
state.NaNReceiver[property.value] = new Decimal(0);
state.hasNaN = false;
}
}
function setOne() {
if (state.NaNReceiver && property.value) {
state.NaNReceiver[property.value] = new Decimal(1);
state.hasNaN = false;
}
}
function ignore() {
if (state.NaNReceiver && property.value) {
state.NaNReceiver[property.value] = new Decimal(NaN);
state.hasNaN = false;
}
}
</script> </script>
<style scoped> <style scoped>

View file

@ -2,14 +2,14 @@
<div class="nav" v-if="useHeader" v-bind="$attrs"> <div class="nav" v-if="useHeader" v-bind="$attrs">
<img v-if="banner" :src="banner" height="100%" :alt="title" /> <img v-if="banner" :src="banner" height="100%" :alt="title" />
<div v-else class="title">{{ title }}</div> <div v-else class="title">{{ title }}</div>
<div @click="openDialog('Changelog')" class="version-container"> <div @click="changelog?.open()" class="version-container">
<tooltip display="<span>Changelog</span>" bottom class="version" <Tooltip display="<span>Changelog</span>" bottom class="version"
><span>v{{ version }}</span></tooltip ><span>v{{ versionNumber }}</span></Tooltip
> >
</div> </div>
<div style="flex-grow: 1; cursor: unset;"></div> <div style="flex-grow: 1; cursor: unset;"></div>
<div class="discord"> <div class="discord">
<span @click="openDialog('Info')" class="material-icons">discord</span> <span @click="openDiscord" class="material-icons">discord</span>
<ul class="discord-links"> <ul class="discord-links">
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'"> <li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
<a :href="discordLink" target="_blank">{{ discordName }}</a> <a :href="discordLink" target="_blank">{{ discordName }}</a>
@ -29,57 +29,57 @@
</div> </div>
<div> <div>
<a href="https://forums.moddingtree.com/" target="_blank"> <a href="https://forums.moddingtree.com/" target="_blank">
<tooltip display="Forums" bottom yoffset="5px"> <Tooltip display="Forums" bottom yoffset="5px">
<span class="material-icons">forum</span> <span class="material-icons">forum</span>
</tooltip> </Tooltip>
</a> </a>
</div> </div>
<div @click="openDialog('Info')"> <div @click="info?.open()">
<tooltip display="Info" bottom class="info"> <Tooltip display="Info" bottom class="info">
<span class="material-icons">info</span> <span class="material-icons">info</span>
</tooltip> </Tooltip>
</div> </div>
<div @click="openDialog('Saves')"> <div @click="savesManager?.open()">
<tooltip display="Saves" bottom xoffset="-20px"> <Tooltip display="Saves" bottom xoffset="-20px">
<span class="material-icons">library_books</span> <span class="material-icons">library_books</span>
</tooltip> </Tooltip>
</div> </div>
<div @click="openDialog('Options')"> <div @click="options?.open()">
<tooltip display="Options" bottom xoffset="-66px"> <Tooltip display="Options" bottom xoffset="-66px">
<span class="material-icons">settings</span> <span class="material-icons">settings</span>
</tooltip> </Tooltip>
</div> </div>
</div> </div>
<div v-else class="overlay-nav" v-bind="$attrs"> <div v-else class="overlay-nav" v-bind="$attrs">
<div @click="openDialog('Changelog')" class="version-container"> <div @click="changelog?.open()" class="version-container">
<tooltip display="Changelog" right xoffset="25%" class="version"> <Tooltip display="Changelog" right xoffset="25%" class="version">
<span>v{{ version }}</span> <span>v{{ versionNumber }}</span>
</tooltip> </tooltip>
</div> </div>
<div @click="openDialog('Saves')"> <div @click="savesManager?.open()">
<tooltip display="Saves" right> <Tooltip display="Saves" right>
<span class="material-icons">library_books</span> <span class="material-icons">library_books</span>
</tooltip> </Tooltip>
</div> </div>
<div @click="openDialog('Options')"> <div @click="options?.open()">
<tooltip display="Options" right> <Tooltip display="Options" right>
<span class="material-icons">settings</span> <span class="material-icons">settings</span>
</tooltip> </Tooltip>
</div> </div>
<div @click="openDialog('Info')"> <div @click="info?.open()">
<tooltip display="Info" right> <Tooltip display="Info" right>
<span class="material-icons">info</span> <span class="material-icons">info</span>
</tooltip> </Tooltip>
</div> </div>
<div> <div>
<a href="https://forums.moddingtree.com/" target="_blank"> <a href="https://forums.moddingtree.com/" target="_blank">
<tooltip display="Forums" right xoffset="7px"> <Tooltip display="Forums" right xoffset="7px">
<span class="material-icons">forum</span> <span class="material-icons">forum</span>
</tooltip> </Tooltip>
</a> </a>
</div> </div>
<div class="discord"> <div class="discord">
<span @click="openDialog('Info')" class="material-icons">discord</span> <span @click="openDiscord" class="material-icons">discord</span>
<ul class="discord-links"> <ul class="discord-links">
<li v-if="discordLink !== 'https://discord.gg/WzejVAx'"> <li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
<a :href="discordLink" target="_blank">{{ discordName }}</a> <a :href="discordLink" target="_blank">{{ discordName }}</a>
@ -98,47 +98,33 @@
</ul> </ul>
</div> </div>
</div> </div>
<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" /> <Info ref="info" :changelog="changelog" />
<SavesManager :show="showSaves" @closeDialog="closeDialog" /> <SavesManager ref="savesManager" />
<Options :show="showOptions" @closeDialog="closeDialog" /> <Options ref="options" />
<Changelog :show="showChangelog" @closeDialog="closeDialog" /> <Changelog ref="changelog" />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import Changelog from "@/data/Changelog.vue";
import modInfo from "@/data/modInfo.json"; import modInfo from "@/data/modInfo.json";
import { defineComponent } from "vue"; import { ComponentPublicInstance, ref } from "vue";
import Info from "./Info.vue";
import Options from "./Options.vue";
import SavesManager from "./SavesManager.vue";
import Tooltip from "./Tooltip.vue";
type modals = "Info" | "Saves" | "Options" | "Changelog"; const info = ref<typeof Info | null>(null);
type showModals = "showInfo" | "showSaves" | "showOptions" | "showChangelog"; const savesManager = ref<typeof SavesManager | null>(null);
const options = ref<typeof Options | null>(null);
// For some reason Info won't accept the changelog unless I do this:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changelog = ref<ComponentPublicInstance<any> | null>(null);
export default defineComponent({ const { useHeader, banner, title, discordName, discordLink, versionNumber } = modInfo;
name: "Nav",
data() { function openDiscord() {
return { window.open(discordLink, "mywindow");
useHeader: modInfo.useHeader, }
banner: modInfo.banner,
title: modInfo.title,
discordName: modInfo.discordName,
discordLink: modInfo.discordLink,
version: modInfo.versionNumber,
showInfo: false,
showSaves: false,
showOptions: false,
showChangelog: false
};
},
methods: {
openDiscord() {
window.open(this.discordLink, "mywindow");
},
openDialog(dialog: modals) {
this[`show${dialog}` as showModals] = true;
},
closeDialog(dialog: modals) {
this[`show${dialog}` as showModals] = false;
}
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,107 +1,70 @@
<template> <template>
<Modal :show="show" @close="$emit('closeDialog', 'Options')"> <Modal v-model="isOpen">
<template v-slot:header> <template v-slot:header>
<div class="header"> <div class="header">
<h2>Options</h2> <h2>Options</h2>
</div> </div>
</template> </template>
<template v-slot:body> <template v-slot:body>
<Select <Select title="Theme" :options="themes" v-model="theme" />
title="Theme" <Select title="Show Milestones" :options="msDisplayOptions" v-model="msDisplay" />
:options="themes" <Toggle title="Show TPS" v-model="showTPS" />
:value="theme" <Toggle title="Hide Maxed Challenges" v-model="hideChallenges" />
@change="setTheme" <Toggle title="Unthrottled" v-model="unthrottled" />
default="classic"
/>
<Select
title="Show Milestones"
:options="msDisplayOptions"
:value="msDisplay"
@change="setMSDisplay"
default="all"
/>
<Toggle title="Show TPS" :value="showTPS" @change="toggleSettingsOption('showTPS')" />
<Toggle
title="Hide Maxed Challenges"
:value="hideChallenges"
@change="toggleSettingsOption('hideChallenges')"
/>
<Toggle
title="Unthrottled"
:value="unthrottled"
@change="toggleSettingsOption('unthrottled')"
/>
<Toggle <Toggle
title="Offline Production<tooltip display='Save-specific'>*</tooltip>" title="Offline Production<tooltip display='Save-specific'>*</tooltip>"
:value="offlineProd" v-model="offlineProd"
@change="togglePlayerOption('offlineProd')"
/> />
<Toggle <Toggle
title="Autosave<tooltip display='Save-specific'>*</tooltip>" title="Autosave<tooltip display='Save-specific'>*</tooltip>"
:value="autosave" v-model="autosave"
@change="togglePlayerOption('autosave')"
/> />
<Toggle <Toggle
title="Pause game<tooltip display='Save-specific'>*</tooltip>" title="Pause game<tooltip display='Save-specific'>*</tooltip>"
:value="paused" v-model="isPaused"
@change="togglePaused"
/> />
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import Modal from "@/components/system/Modal.vue";
import themes, { Themes } from "@/data/themes"; import rawThemes from "@/data/themes";
import { camelToTitle } from "@/util/common"; import { MilestoneDisplay } from "@/features/milestone";
import { mapPlayer, mapSettings } from "@/util/vue";
import player from "@/game/player"; import player from "@/game/player";
import { MilestoneDisplay } from "@/game/enums";
import { PlayerData } from "@/typings/player";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { Settings } from "@/typings/settings"; import { camelToTitle } from "@/util/common";
import { computed, ref, toRefs } from "vue";
import Toggle from "../fields/Toggle.vue";
import Select from "../fields/Select.vue";
export default defineComponent({ const isOpen = ref(false);
name: "Options",
props: { defineExpose({
show: Boolean open() {
}, isOpen.value = true;
emits: ["closeDialog"], }
data() { });
return {
themes: Object.keys(themes).map(theme => ({ const themes = Object.keys(rawThemes).map(theme => ({
label: camelToTitle(theme), label: camelToTitle(theme),
value: theme value: theme
})), }));
msDisplayOptions: Object.values(MilestoneDisplay).map(option => ({
// TODO allow features to register options
const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
label: camelToTitle(option), label: camelToTitle(option),
value: option value: option
})) }));
};
const { showTPS, hideChallenges, theme, msDisplay, unthrottled } = toRefs(settings);
const { autosave, offlineProd, devSpeed } = toRefs(player);
const isPaused = computed({
get() {
return devSpeed.value === 0;
}, },
computed: { set(value: boolean) {
...mapSettings(["showTPS", "hideChallenges", "theme", "msDisplay", "unthrottled"]), devSpeed.value = value ? null : 0;
...mapPlayer(["autosave", "offlineProd"]),
paused() {
return player.devSpeed === 0;
}
},
methods: {
togglePlayerOption(option: keyof PlayerData) {
player[option] = !player[option];
},
toggleSettingsOption(option: keyof Settings) {
settings[option] = !settings[option];
},
setTheme(theme: Themes) {
settings.theme = theme;
},
setMSDisplay(msDisplay: MilestoneDisplay) {
settings.msDisplay = msDisplay;
},
togglePaused() {
player.devSpeed = this.paused ? 1 : 0;
}
} }
}); });
</script> </script>

View file

@ -4,16 +4,16 @@
</h2> </h2>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { displayResource, Resource } from "@/features/resource";
import { computed, toRefs } from "vue";
export default defineComponent({ const props = toRefs(
name: "resource", defineProps<{
props: { resource: Resource;
color: String, color: string;
amount: String }>()
} );
});
const amount = computed(() => displayResource(props.resource));
</script> </script>
<style scoped></style>

View file

@ -6,10 +6,4 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts"></script>
import { defineComponent } from "vue";
export default defineComponent({
name: "row"
});
</script>

View file

@ -1,111 +1,103 @@
<template> <template>
<div class="save" :class="{ active }"> <div class="save" :class="{ active: isActive }">
<div class="handle material-icons">drag_handle</div> <div class="handle material-icons">drag_handle</div>
<div class="actions" v-if="!editing"> <div class="actions" v-if="!isEditing">
<feedback-button <FeedbackButton
@click="$emit('export')" @click="emit('export')"
class="button" class="button"
left left
v-if="save.error == undefined && !confirming" v-if="save.error == undefined && !isConfirming"
> >
<span class="material-icons">content_paste</span> <span class="material-icons">content_paste</span>
</feedback-button> </FeedbackButton>
<button <button
@click="$emit('duplicate')" @click="emit('duplicate')"
class="button" class="button"
v-if="save.error == undefined && !confirming" v-if="save.error == undefined && !isConfirming"
> >
<span class="material-icons">content_copy</span> <span class="material-icons">content_copy</span>
</button> </button>
<button <button
@click="toggleEditing" @click="isEditing = !isEditing"
class="button" class="button"
v-if="save.error == undefined && !confirming" v-if="save.error == undefined && !isConfirming"
> >
<span class="material-icons">edit</span> <span class="material-icons">edit</span>
</button> </button>
<danger-button <DangerButton
:disabled="active" :disabled="isActive"
@click="$emit('delete')" @click="emit('delete')"
@confirmingChanged="confirmingChanged" @confirmingChanged="value => (isConfirming = value)"
> >
<span class="material-icons" style="margin: -2px">delete</span> <span class="material-icons" style="margin: -2px">delete</span>
</danger-button> </DangerButton>
</div> </div>
<div class="actions" v-else> <div class="actions" v-else>
<button @click="changeName" class="button"> <button @click="changeName" class="button">
<span class="material-icons">check</span> <span class="material-icons">check</span>
</button> </button>
<button @click="toggleEditing" class="button"> <button @click="isEditing = !isEditing" class="button">
<span class="material-icons">close</span> <span class="material-icons">close</span>
</button> </button>
</div> </div>
<div class="details" v-if="save.error == undefined && !editing"> <div class="details" v-if="save.error == undefined && !isEditing">
<button class="button open" @click="$emit('open')"> <button class="button open" @click="emit('open')">
<h3>{{ save.name }}</h3> <h3>{{ save.name }}</h3>
</button> </button>
<span class="save-version">v{{ save.modVersion }}</span <span class="save-version">v{{ save.modVersion }}</span
><br /> ><br />
<div v-if="time">Last played {{ dateFormat.format(time) }}</div> <div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
</div> </div>
<div class="details" v-else-if="save.error == undefined && editing"> <div class="details" v-else-if="save.error == undefined && isEditing">
<TextField v-model="newName" class="editname" @submit="changeName" @blur="changeName" /> <Text v-model="newName" class="editname" @submit="changeName" />
</div> </div>
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div> <div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import player from "@/game/player"; import player from "@/game/player";
import { PlayerData } from "@/typings/player"; import { computed, ref, toRefs, unref, watch } from "vue";
import { defineComponent, PropType } from "vue"; import DangerButton from "../fields/DangerButton.vue";
import FeedbackButton from "../fields/FeedbackButton.vue";
import Text from "../fields/Text.vue";
import { LoadablePlayerData } from "./SavesManager.vue";
export default defineComponent({ const props = toRefs(
name: "save", defineProps<{
props: { save: LoadablePlayerData;
save: { }>()
type: Object as PropType<Partial<PlayerData>>, );
required: true const emit = defineEmits<{
} (e: "export"): void;
}, (e: "open"): void;
emits: ["export", "open", "duplicate", "delete", "editSave"], (e: "duplicate"): void;
data() { (e: "delete"): void;
return { (e: "editName", name: string): void;
dateFormat: new Intl.DateTimeFormat("en-US", { }>();
const dateFormat = new Intl.DateTimeFormat("en-US", {
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
second: "numeric" second: "numeric"
}),
editing: false,
confirming: false,
newName: ""
};
},
computed: {
active(): boolean {
return this.save.id === player.id;
},
time(): number | undefined {
return this.active ? player.time : this.save.time;
}
},
methods: {
confirmingChanged(confirming: boolean) {
this.confirming = confirming;
},
toggleEditing() {
this.newName = this.save.name || "";
this.editing = !this.editing;
},
changeName() {
this.$emit("editSave", this.newName);
this.editing = false;
}
}
}); });
const isEditing = ref(false);
const isConfirming = ref(false);
const newName = ref("");
watch(isEditing, () => (newName.value = ""));
const isActive = computed(() => unref(props.save).id === player.id);
const currentTime = computed(() => (isActive.value ? player.time : unref(props.save).time));
function changeName() {
emit("editName", newName.value);
isEditing.value = false;
}
</script> </script>
<style scoped> <style scoped>

View file

@ -1,17 +1,17 @@
<template> <template>
<Modal :show="show" @close="$emit('closeDialog', 'Saves')"> <Modal v-model="isOpen">
<template v-slot:header> <template v-slot:header>
<h2>Saves Manager</h2> <h2>Saves Manager</h2>
</template> </template>
<template v-slot:body> <template v-slot:body>
<div v-sortable="{ update, handle: '.handle' }"> <div v-sortable="{ update, handle: '.handle' }">
<save <Save
v-for="(save, index) in saves" v-for="(save, index) in saves"
:key="index" :key="index"
:save="save" :save="save"
@open="openSave(save.id)" @open="openSave(save.id)"
@export="exportSave(save.id)" @export="exportSave(save.id)"
@editSave="name => editSave(save.id, name)" @editName="name => editSave(save.id, name)"
@duplicate="duplicateSave(save.id)" @duplicate="duplicateSave(save.id)"
@delete="deleteSave(save.id)" @delete="deleteSave(save.id)"
/> />
@ -19,10 +19,8 @@
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<div class="modal-footer"> <div class="modal-footer">
<TextField <Text
:value="saveToImport" v-model="saveToImport"
@submit="importSave"
@change="importSave"
title="Import Save" title="Import Save"
placeholder="Paste your save here!" placeholder="Paste your save here!"
:class="{ importingFailed }" :class="{ importingFailed }"
@ -34,20 +32,17 @@
<Select <Select
v-if="Object.keys(bank).length > 0" v-if="Object.keys(bank).length > 0"
:options="bank" :options="bank"
:modelValue="[]"
@update:modelValue="preset => newFromPreset(preset as string)"
closeOnSelect closeOnSelect
@change="newFromPreset"
placeholder="Select preset" placeholder="Select preset"
class="presets" class="presets"
:value="[]"
/> />
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<div style="flex-grow: 1"></div> <div style="flex-grow: 1"></div>
<button <button class="button modal-default-button" @click="isOpen = false">
class="button modal-default-button"
@click="$emit('closeDialog', 'Saves')"
>
Close Close
</button> </button>
</div> </div>
@ -56,64 +51,81 @@
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import player from "@/game/player"; import Modal from "@/components/system/Modal.vue";
import player, { PlayerData } from "@/game/player";
import settings from "@/game/settings"; import settings from "@/game/settings";
import state from "@/game/state"; import { getUniqueID, loadSave, save, newSave as createNewSave } from "@/util/save";
import { PlayerData } from "@/typings/player"; import { nextTick, ref, watch } from "vue";
import { getUniqueID, loadSave, newSave, save } from "@/util/save"; import Select from "../fields/Select.vue";
import { defineComponent } from "vue"; import Text from "../fields/Text.vue";
import Save from "./Save.vue";
import vSortable from "vue-sortable";
export default defineComponent({ export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
name: "SavesManager",
props: { const isOpen = ref(false);
show: Boolean
}, defineExpose({
emits: ["closeDialog"], open() {
data() { isOpen.value = true;
let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/); }
let bank = bankContext });
.keys()
.reduce((acc: Array<{ label: string; value: string }>, curr) => { const importingFailed = ref(false);
const saveToImport = ref("");
watch(isOpen, isOpen => {
if (isOpen) {
loadSaveData();
}
});
watch(saveToImport, save => {
if (save) {
nextTick(() => {
try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
if (typeof playerData !== "object") {
importingFailed.value = true;
return;
}
const id = getUniqueID();
playerData.id = id;
localStorage.setItem(
id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
saves.value[id] = playerData;
saveToImport.value = "";
importingFailed.value = false;
settings.saves.push(id);
} catch (e) {
importingFailed.value = true;
}
});
} else {
importingFailed.value = false;
}
});
let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/);
let bank = ref(
bankContext.keys().reduce((acc: Array<{ label: string; value: string }>, curr) => {
// .slice(2, -4) strips the leading ./ and the trailing .txt // .slice(2, -4) strips the leading ./ and the trailing .txt
acc.push({ acc.push({
label: curr.slice(2, -4), label: curr.slice(2, -4),
value: bankContext(curr).default value: bankContext(curr).default
}); });
return acc; return acc;
}, []); }, [])
return { );
importingFailed: false,
saves: {}, // Gets populated when the modal is opened const saves = ref<Record<string, LoadablePlayerData>>({});
saveToImport: "",
bank function loadSaveData() {
} as { saves.value = settings.saves.reduce((acc: Record<string, LoadablePlayerData>, curr: string) => {
importingFailed: boolean;
saves: Record<
string,
Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }
>;
saveToImport: string;
bank: Array<{ label: string; value: string }>;
};
},
watch: {
show(newValue) {
if (newValue) {
this.loadSaveData();
}
}
},
methods: {
loadSaveData() {
this.saves = settings.saves.reduce(
(
acc: Record<
string,
Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }
>,
curr: string
) => {
try { try {
const save = localStorage.getItem(curr); const save = localStorage.getItem(curr);
if (save == null) { if (save == null) {
@ -127,17 +139,15 @@ export default defineComponent({
acc[curr] = { error, id: curr }; acc[curr] = { error, id: curr };
} }
return acc; return acc;
}, }, {});
{} }
);
}, function exportSave(id: string) {
exportSave(id: string) {
let saveToExport; let saveToExport;
if (player.id === id) { if (player.id === id) {
save(); saveToExport = save();
saveToExport = state.saveToExport;
} else { } else {
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id])))); saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))));
} }
// Put on clipboard. Using the clipboard API asks for permissions and stuff // Put on clipboard. Using the clipboard API asks for permissions and stuff
@ -148,36 +158,41 @@ export default defineComponent({
el.setSelectionRange(0, 99999); el.setSelectionRange(0, 99999);
document.execCommand("copy"); document.execCommand("copy");
document.body.removeChild(el); document.body.removeChild(el);
}, }
duplicateSave(id: string) {
function duplicateSave(id: string) {
if (player.id === id) { if (player.id === id) {
save(); save();
} }
const playerData = { ...this.saves[id], id: getUniqueID() }; const playerData = { ...saves.value[id], id: getUniqueID() };
localStorage.setItem( localStorage.setItem(
playerData.id, playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))) btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
); );
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
this.saves[playerData.id] = playerData; saves.value[playerData.id] = playerData;
}, }
deleteSave(id: string) {
function deleteSave(id: string) {
settings.saves = settings.saves.filter((save: string) => save !== id); settings.saves = settings.saves.filter((save: string) => save !== id);
localStorage.removeItem(id); localStorage.removeItem(id);
delete this.saves[id]; delete saves.value[id];
}, }
openSave(id: string) {
this.saves[player.id].time = player.time; function openSave(id: string) {
saves.value[player.id].time = player.time;
save(); save();
loadSave(this.saves[id]); loadSave(saves.value[id]);
}, }
newSave() {
const playerData = newSave(); function newSave() {
this.saves[playerData.id] = playerData; const playerData = createNewSave();
}, saves.value[playerData.id] = playerData;
newFromPreset(preset: string) { }
function newFromPreset(preset: string) {
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset)))); const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
playerData.id = getUniqueID(); playerData.id = getUniqueID();
localStorage.setItem( localStorage.setItem(
@ -186,54 +201,25 @@ export default defineComponent({
); );
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
this.saves[playerData.id] = playerData; saves.value[playerData.id] = playerData;
}, }
editSave(id: string, newName: string) {
this.saves[id].name = newName; function editSave(id: string, newName: string) {
saves.value[id].name = newName;
if (player.id === id) { if (player.id === id) {
player.name = newName; player.name = newName;
save(); save();
} else { } else {
localStorage.setItem( localStorage.setItem(
id, id,
btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id])))) btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))))
); );
} }
}, }
importSave(text: string) {
this.saveToImport = text;
if (text) {
this.$nextTick(() => {
try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(text))));
if (typeof playerData !== "object") {
this.importingFailed = true;
return;
}
const id = getUniqueID();
playerData.id = id;
localStorage.setItem(
id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
this.saves[id] = playerData;
this.saveToImport = "";
this.importingFailed = false;
settings.saves.push(id); function update(e: { newIndex: number; oldIndex: number }) {
} catch (e) {
this.importingFailed = true;
}
});
} else {
this.importingFailed = false;
}
},
update(e: { newIndex: number; oldIndex: number }) {
settings.saves.splice(e.newIndex, 0, settings.saves.splice(e.oldIndex, 1)[0]); settings.saves.splice(e.newIndex, 0, settings.saves.splice(e.oldIndex, 1)[0]);
} }
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -1,23 +1,16 @@
<template> <template>
<div :style="{ width: spacingWidth, height: spacingHeight }"></div> <div :style="{ width, height }"></div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; withDefaults(
defineProps<{
export default defineComponent({ width: string;
name: "spacer", height: string;
props: { }>(),
width: String, {
height: String width: "8px",
}, height: "17px"
computed: {
spacingWidth(): string {
return this.width || "8px";
},
spacingHeight(): string {
return this.height || "17px";
} }
} );
});
</script> </script>

View file

@ -1,54 +1,39 @@
<template> <template>
<div class="sticky" :style="{ top }" ref="sticky" data-v-sticky> <div class="sticky" :style="{ top }" ref="element" data-v-sticky>
<slot /> <slot />
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { nextTick, onMounted, ref } from "vue";
export default defineComponent({ const top = ref("0");
name: "sticky", const observer = new ResizeObserver(updateTop);
data() { const element = ref<HTMLElement | null>(null);
return {
top: "0", function updateTop() {
observer: null let el = element.value;
} as {
top: string;
observer: ResizeObserver | null;
};
},
mounted() {
this.setup();
},
methods: {
setup() {
this.$nextTick(() => {
if (this.$refs.sticky == undefined) {
this.$nextTick(this.setup);
} else {
this.updateTop();
this.observer = new ResizeObserver(this.updateTop);
this.observer.observe((this.$refs.sticky as HTMLElement).parentElement!);
}
});
},
updateTop() {
let el = this.$refs.sticky as HTMLElement;
if (el == undefined) { if (el == undefined) {
return; return;
} }
let top = 0; let newTop = 0;
while (el.previousSibling) { while (el.previousSibling) {
const sibling = el.previousSibling as HTMLElement; const sibling = el.previousSibling as HTMLElement;
if (sibling.dataset && "vSticky" in sibling.dataset) { if (sibling.dataset && "vSticky" in sibling.dataset) {
top += sibling.offsetHeight; newTop += sibling.offsetHeight;
} }
el = sibling; el = sibling;
} }
this.top = top + "px"; top.value = newTop + "px";
} }
nextTick(updateTop);
onMounted(() => {
const el = element.value?.parentElement;
if (el) {
observer.observe(el);
} }
}); });
</script> </script>

View file

@ -1,25 +1,18 @@
<template> <template>
<div class="tpsDisplay" v-if="tps !== 'NaN'">TPS: {{ tps }}</div> <div class="tpsDisplay" v-if="!tps.isNan">TPS: {{ tps }}</div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import state from "@/game/state"; import state from "@/game/state";
import Decimal, { formatWhole } from "@/util/bignum"; import Decimal from "@/util/bignum";
import { defineComponent } from "vue"; import { computed } from "vue";
export default defineComponent({ const tps = computed(() =>
name: "TPS",
computed: {
tps() {
return formatWhole(
Decimal.div( Decimal.div(
state.lastTenTicks.length, state.lastTenTicks.length,
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0) state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
) )
); );
}
}
});
</script> </script>
<style scoped> <style scoped>

View file

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

View file

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

View file

@ -1,14 +1,14 @@
<template> <template>
<div <div
class="tooltip-container" class="tooltip-container"
:class="{ shown }" :class="{ shown: isShown }"
@mouseenter="setHover(true)" @mouseenter="setHover(true)"
@mouseleave="setHover(false)" @mouseleave="setHover(false)"
> >
<slot /> <slot />
<transition name="fade"> <transition name="fade">
<div <div
v-if="shown" v-if="isShown"
class="tooltip" class="tooltip"
:class="{ top, left, right, bottom }" :class="{ top, left, right, bottom }"
:style="{ :style="{
@ -16,56 +16,28 @@
'--yoffset': yoffset || '0px' '--yoffset': yoffset || '0px'
}" }"
> >
<component :is="tooltipDisplay" /> <component :is="component" />
</div> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { CoercableComponent } from "@/typings/component"; import { FeatureComponent } from "@/features/feature";
import { Tooltip } from "@/features/tooltip";
import { coerceComponent } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { Component, defineComponent, PropType } from "vue"; import { computed, ref, toRefs, unref } from "vue";
export default defineComponent({ const props = toRefs(defineProps<FeatureComponent<Tooltip>>());
name: "tooltip",
data() { const isHovered = ref(false);
return {
hover: false function setHover(hover: boolean) {
}; isHovered.value = hover;
}, }
props: {
force: Boolean, const isShown = computed(() => unref(props.force) || isHovered.value);
display: { const component = computed(() => props.display.value && coerceComponent(unref(props.display)));
type: [String, Object] as PropType<CoercableComponent>,
required: true
},
top: Boolean,
left: Boolean,
right: Boolean,
bottom: Boolean,
xoffset: String,
yoffset: String
},
computed: {
tooltipDisplay(): Component | string {
return coerceComponent(this.display, "span", false);
},
shown(): boolean {
return this.force || this.hover;
}
},
provide: {
tab() {
return {};
}
},
methods: {
setHover(hover: boolean) {
this.hover = hover;
}
}
});
</script> </script>
<style scoped> <style scoped>

View file

@ -2,15 +2,10 @@
<div class="vr" :style="{ height }"></div> <div class="vr" :style="{ height }"></div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; defineProps<{
height?: string;
export default defineComponent({ }>();
name: "vr",
props: {
height: String
}
});
</script> </script>
<style scoped> <style scoped>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<template> <template>
<Modal :show="show" @close="$emit('closeDialog', 'Changelog')"> <Modal v-model="isOpen">
<template v-slot:header> <template v-slot:header>
<h2>Changelog</h2> <h2>Changelog</h2>
</template> </template>
@ -18,15 +18,16 @@
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import Modal from "@/components/system/Modal.vue";
import { ref } from "vue";
export default defineComponent({ const isOpen = ref(false);
name: "Changelog",
props: { defineExpose({
show: Boolean open() {
}, isOpen.value = true;
emits: ["closeDialog"] }
}); });
</script> </script>

145
src/data/common.tsx Normal file
View 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>;
}

View file

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

View file

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

View file

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

View file

@ -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
View 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 */

View file

@ -1,4 +1,35 @@
import { Theme } from "@/typings/theme"; interface ThemeVars {
"--foreground": string;
"--background": string;
"--feature-foreground": string;
"--tooltip-background": string;
"--raised-background": string;
"--points": string;
"--locked": string;
"--highlighted": string;
"--bought": string;
"--danger": string;
"--link": string;
"--outline": string;
"--accent1": string;
"--accent2": string;
"--accent3": string;
"--border-radius": string;
"--modal-border": string;
"--feature-margin": string;
}
export interface Theme {
variables: ThemeVars;
stackedInfoboxes: boolean;
floatingTabs: boolean;
showSingleTab: boolean;
}
declare module "@vue/runtime-dom" {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface CSSProperties extends Partial<ThemeVars> {}
}
const defaultTheme: Theme = { const defaultTheme: Theme = {
variables: { variables: {

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