Fixed runtime issues with vue

Entire demo tree has been tested and is fully functional,
including all the options and save manager functionality
This commit is contained in:
thepaperpilot 2022-02-27 13:49:34 -06:00
parent ebf26c58e7
commit e2126472b2
77 changed files with 2941 additions and 1862 deletions

View file

@ -5,7 +5,10 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&family=Kalam&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>

View file

@ -3,14 +3,14 @@
<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" />
<Game /> <Game />
<TPS v-if="showTPS" /> <TPS v-if="unref(showTPS)" />
<GameOverScreen /> <GameOverScreen />
<NaNScreen /> <NaNScreen />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, toRef } from "vue"; import { computed, toRef, unref } from "vue";
import Game from "./components/system/Game.vue"; import Game from "./components/system/Game.vue";
import GameOverScreen from "./components/system/GameOverScreen.vue"; import GameOverScreen from "./components/system/GameOverScreen.vue";
import NaNScreen from "./components/system/NaNScreen.vue"; import NaNScreen from "./components/system/NaNScreen.vue";

View file

@ -1,22 +0,0 @@
.notify.floating,
*:not(.subtabs) > .notify,
*:not(.subtabs) > .notify button {
transform: scale(1.05, 1.05);
border-color: rgba(0, 0, 0, 0.125);
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px #ff0000;
z-index: 1;
}
.subtabs > .notify:not(.floating) {
box-shadow: 0px 20px 15px -15px #ff0000;
}
*:not(.subtabs) > .resetNotify:not(.notify),
*:not(.subtabs) > .resetNotify:not(.notify) button {
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 8px #ffffff;
z-index: 1;
}
.subtabs > .resetNotify:not(.notify):not(.floating) {
box-shadow: 0px 10px 8px -10px #ffffff;
}

View file

@ -33,3 +33,59 @@
height: 100%; height: 100%;
margin: 10px 0; margin: 10px 0;
} }
.row.mergeAdjacent > .feature,
.row.mergeAdjacent > .tooltip-container > .feature {
margin-left: 0;
margin-right: 0;
border-radius: 0;
}
.row.mergeAdjacent > .feature:first-child,
.row.mergeAdjacent > .tooltip-container:first-child > .feature {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.row.mergeAdjacent > .feature:last-child,
.row.mergeAdjacent > .tooltip-container:last-child > .feature {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.row.mergeAdjacent > .feature:first-child:last-child,
.row.mergeAdjacent > .tooltip-container:first-child:last-child > .feature {
border-radius: var(--border-radius);
}
/*
TODO how to implement mergeAdjacent for grids?
.row.mergeAdjacent + .row.mergeAdjacent > .feature {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
*/
.col.mergeAdjacent .feature {
margin-top: 0;
margin-bottom: 0;
border-radius: 0;
}
.col.mergeAdjacent .feature:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
.col.mergeAdjacent .feature:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
.col.mergeAdjacent .feature:first-child:last-child {
border-radius: var(--border-radius);
}
/*
TODO how to implement mergeAdjacent for grids?
.col.mergeAdjacent + .col.mergeAdjacent > .feature {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
*/

View file

@ -1,63 +1,67 @@
<template> <template>
<Tooltip
v-if="visibility !== Visibility.None"
v-show="visibility === Visibility.Visible"
:display="tooltip"
>
<div <div
:style="[{ backgroundImage: (earned && image && `url(${image})`) || '' }, style ?? []]" v-if="unref(visibility) !== Visibility.None"
:style="[
{
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined,
backgroundImage: (earned && image && `url(${image})`) || ''
},
unref(style) ?? []
]"
:class="{ :class="{
feature: true, feature: true,
achievement: true, achievement: true,
locked: !earned, locked: !unref(earned),
bought: earned, bought: unref(earned),
...classes ...unref(classes)
}" }"
> >
<component v-if="component" :is="component" /> <component v-if="component" :is="component" />
<MarkNode :mark="mark" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</div> </div>
</Tooltip>
</template> </template>
<script lang="ts"> <script lang="ts">
import { CoercableComponent, Visibility } from "@/features/feature"; import { CoercableComponent, Visibility } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { computeOptionalComponent, processedPropType } from "@/util/vue";
import { computed, defineComponent, PropType, StyleValue, toRefs } from "vue"; import { defineComponent, StyleValue, toRefs, unref } from "vue";
import Tooltip from "@/components/system/Tooltip.vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue"; import MarkNode from "./MarkNode.vue";
import "@/components/common/features.css";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
display: [Object, String] as PropType<CoercableComponent>, display: processedPropType<CoercableComponent>(Object, String, Function),
tooltip: [Object, String] as PropType<CoercableComponent>,
earned: { earned: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
image: String, image: processedPropType<string>(String),
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(String, Object, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
mark: [Boolean, String], mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: String, type: String,
required: true required: true
} }
}, },
components: {
LinkNode,
MarkNode,
Tooltip
},
setup(props) { setup(props) {
const { display } = toRefs(props); const { display } = toRefs(props);
return { return {
component: computed(() => { component: computeOptionalComponent(display),
return display.value && coerceComponent(display.value); unref,
}),
LinkNode,
MarkNode,
Visibility Visibility
}; };
} }

View file

@ -1,31 +1,45 @@
<template> <template>
<div <div
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="[
:style="[{ width: width + 'px', height: height + 'px' }, style ?? {}]" {
width: unref(width) + 'px',
height: unref(height) + 'px',
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? {}
]"
:class="{ :class="{
bar: true, bar: true,
...classes ...unref(classes)
}" }"
> >
<div <div
class="overlayTextContainer border" class="overlayTextContainer border"
:style="[{ width: width + 'px', height: height + 'px' }, borderStyle ?? {}]" :style="[
{ width: unref(width) + 'px', height: unref(height) + 'px' },
unref(borderStyle) ?? {}
]"
> >
<component v-if="component" class="overlayText" :style="textStyle" :is="component" /> <component
v-if="component"
class="overlayText"
:style="unref(textStyle)"
:is="component"
/>
</div> </div>
<div <div
class="border" class="border"
:style="[ :style="[
{ width: width + 'px', height: height + 'px' }, { width: unref(width) + 'px', height: unref(height) + 'px' },
style ?? {}, unref(style) ?? {},
baseStyle ?? {}, unref(baseStyle) ?? {},
borderStyle ?? {} unref(borderStyle) ?? {}
]" ]"
> >
<div class="fill" :style="[barStyle, style ?? {}, fillStyle ?? {}]" /> <div class="fill" :style="[barStyle, unref(style) ?? {}, unref(fillStyle) ?? {}]" />
</div> </div>
<MarkNode :mark="mark" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</div> </div>
</template> </template>
@ -34,46 +48,50 @@
import { Direction } from "@/features/bar"; import { Direction } from "@/features/bar";
import { CoercableComponent, Visibility } from "@/features/feature"; import { CoercableComponent, Visibility } from "@/features/feature";
import Decimal, { DecimalSource } from "@/util/bignum"; import Decimal, { DecimalSource } from "@/util/bignum";
import { coerceComponent } from "@/util/vue"; import { computeOptionalComponent, processedPropType, unwrapRef } from "@/util/vue";
import { computed, CSSProperties, defineComponent, PropType, StyleValue, toRefs, unref } from "vue"; import { computed, CSSProperties, defineComponent, StyleValue, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue"; import MarkNode from "./MarkNode.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
progress: { progress: {
type: Object as PropType<DecimalSource>, type: processedPropType<DecimalSource>(String, Object, Number),
required: true required: true
}, },
width: { width: {
type: Number, type: processedPropType<number>(Number),
required: true required: true
}, },
height: { height: {
type: Number, type: processedPropType<number>(Number),
required: true required: true
}, },
direction: { direction: {
type: Object as PropType<Direction>, type: processedPropType<Direction>(String),
required: true required: true
}, },
display: [Object, String] as PropType<CoercableComponent>, display: processedPropType<CoercableComponent>(Object, String, Function),
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(Object, String, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
borderStyle: Object as PropType<StyleValue>, borderStyle: processedPropType<StyleValue>(Object, String, Array),
textStyle: Object as PropType<StyleValue>, textStyle: processedPropType<StyleValue>(Object, String, Array),
baseStyle: Object as PropType<StyleValue>, baseStyle: processedPropType<StyleValue>(Object, String, Array),
fillStyle: Object as PropType<StyleValue>, fillStyle: processedPropType<StyleValue>(Object, String, Array),
mark: [Boolean, String], mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: String, type: String,
required: true required: true
} }
}, },
components: {
MarkNode,
LinkNode
},
setup(props) { setup(props) {
const { progress, width, height, direction, display } = toRefs(props); const { progress, width, height, direction, display } = toRefs(props);
@ -87,17 +105,17 @@ export default defineComponent({
const barStyle = computed(() => { const barStyle = computed(() => {
const barStyle: Partial<CSSProperties> = { const barStyle: Partial<CSSProperties> = {
width: unref(width) + 0.5 + "px", width: unwrapRef(width) + 0.5 + "px",
height: unref(height) + 0.5 + "px" height: unwrapRef(height) + 0.5 + "px"
}; };
switch (unref(direction)) { switch (unref(direction)) {
case Direction.Up: case Direction.Up:
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`; barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
barStyle.width = unref(width) + 1 + "px"; barStyle.width = unwrapRef(width) + 1 + "px";
break; break;
case Direction.Down: case Direction.Down:
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`; barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
barStyle.width = unref(width) + 1 + "px"; barStyle.width = unwrapRef(width) + 1 + "px";
break; break;
case Direction.Right: case Direction.Right:
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`; barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
@ -112,17 +130,13 @@ export default defineComponent({
return barStyle; return barStyle;
}); });
const component = computed(() => { const component = computeOptionalComponent(display);
const currDisplay = unref(display);
return currDisplay && coerceComponent(unref(currDisplay));
});
return { return {
normalizedProgress, normalizedProgress,
barStyle, barStyle,
component, component,
MarkNode, unref,
LinkNode,
Visibility Visibility
}; };
} }

View file

@ -1,66 +1,85 @@
<template> <template>
<div <div
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="[
:style="style" {
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
notifyStyle,
unref(style) ?? {}
]"
:class="{ :class="{
feature: true, feature: true,
challenge: true, challenge: true,
resetNotify: active, done: unref(completed),
notify: active && canComplete, canStart: unref(canStart),
done: completed, maxed: unref(maxed),
canStart, ...unref(classes)
maxed,
...classes
}" }"
> >
<button class="toggleChallenge" @click="toggle"> <button class="toggleChallenge" @click="toggle">
{{ buttonText }} {{ buttonText }}
</button> </button>
<component v-if="component" :is="component" /> <component v-if="unref(comp)" :is="unref(comp)" />
<MarkNode :mark="mark" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</div> </div>
</template> </template>
<script lang="tsx"> <script lang="tsx">
import "@/components/common/features.css";
import { GenericChallenge } from "@/features/challenge"; import { GenericChallenge } from "@/features/challenge";
import { StyleValue, Visibility } from "@/features/feature"; import { jsx, StyleValue, Visibility } from "@/features/feature";
import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { getHighNotifyStyle, getNotifyStyle } from "@/game/notifications";
import { computed, defineComponent, PropType, toRefs, UnwrapRef } from "vue"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "@/util/vue";
import {
Component,
computed,
defineComponent,
PropType,
shallowRef,
toRefs,
unref,
UnwrapRef,
watchEffect
} from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue"; import MarkNode from "./MarkNode.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
active: { active: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
maxed: { maxed: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
canComplete: { canComplete: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
display: Object as PropType<UnwrapRef<GenericChallenge["display"]>>, display: processedPropType<UnwrapRef<GenericChallenge["display"]>>(
String,
Object,
Function
),
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(String, Object, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
completed: { completed: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
canStart: { canStart: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
mark: [Boolean, String], mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: String, type: String,
required: true required: true
@ -70,6 +89,10 @@ export default defineComponent({
required: true required: true
} }
}, },
components: {
MarkNode,
LinkNode
},
setup(props) { setup(props) {
const { active, maxed, canComplete, display } = toRefs(props); const { active, maxed, canComplete, display } = toRefs(props);
@ -83,46 +106,72 @@ export default defineComponent({
return "Start"; return "Start";
}); });
const component = computed(() => { const comp = shallowRef<Component | string>("");
const currDisplay = display.value;
const notifyStyle = computed(() => {
const currActive = unwrapRef(active);
const currCanComplete = unwrapRef(canComplete);
if (currActive) {
if (currCanComplete) {
return getHighNotifyStyle();
}
return getNotifyStyle();
}
return {};
});
watchEffect(() => {
const currDisplay = unwrapRef(display);
if (currDisplay == null) { if (currDisplay == null) {
return null; comp.value = "";
return;
} }
if (isCoercableComponent(currDisplay)) { if (isCoercableComponent(currDisplay)) {
return coerceComponent(currDisplay); comp.value = coerceComponent(currDisplay);
return;
} }
return ( const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description, "div");
const Goal = coerceComponent(currDisplay.goal || "");
const Reward = coerceComponent(currDisplay.reward || "");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
comp.value = coerceComponent(
jsx(() => (
<span> <span>
<template v-if={currDisplay.title}> {currDisplay.title ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
<component v-is={coerceComponent(currDisplay.title!, "h3")} /> <Title />
</template> </div>
<component v-is={coerceComponent(currDisplay.description, "div")} /> ) : null}
<div v-if={currDisplay.goal}> <Description />
{currDisplay.goal ? (
<div>
<br /> <br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} Goal: <Goal />
Goal: <component v-is={coerceComponent(currDisplay.goal!)} />
</div> </div>
<div v-if={currDisplay.reward}> ) : null}
{currDisplay.reward ? (
<div>
<br /> <br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} Reward: <Reward />
Reward: <component v-is={coerceComponent(currDisplay.reward!)} />
</div> </div>
<div v-if={currDisplay.effectDisplay}> ) : null}
Currently:{" "} {currDisplay.effectDisplay ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
<component v-is={coerceComponent(currDisplay.effectDisplay!)} /> Currently: <EffectDisplay />
</div> </div>
) : null}
</span> </span>
))
); );
}); });
return { return {
buttonText, buttonText,
component, notifyStyle,
MarkNode, comp,
LinkNode, Visibility,
Visibility unref
}; };
} }
}); });

View file

@ -1,7 +1,10 @@
<template> <template>
<div v-if="visibility !== Visibility.None" v-show="visibility === Visibility.Visible"> <div
v-if="unref(visibility) !== Visibility.None"
:style="{ visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined }"
>
<button <button
:style="style" :style="unref(style)"
@click="onClick" @click="onClick"
@mousedown="start" @mousedown="start"
@mouseleave="stop" @mouseleave="stop"
@ -9,18 +12,18 @@
@touchstart="start" @touchstart="start"
@touchend="stop" @touchend="stop"
@touchcancel="stop" @touchcancel="stop"
:disabled="!canClick" :disabled="!unref(canClick)"
:class="{ :class="{
feature: true, feature: true,
clickable: true, clickable: true,
can: canClick, can: unref(canClick),
locked: !canClick, locked: !unref(canClick),
small, small,
...classes ...unref(classes)
}" }"
> >
<component v-if="component" :is="component" /> <component v-if="unref(comp)" :is="unref(comp)" />
<MarkNode :mark="mark" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</button> </button>
</div> </div>
@ -28,56 +31,89 @@
<script lang="tsx"> <script lang="tsx">
import { GenericClickable } from "@/features/clickable"; import { GenericClickable } from "@/features/clickable";
import { StyleValue, Visibility } from "@/features/feature"; import { jsx, StyleValue, Visibility } from "@/features/feature";
import { coerceComponent, isCoercableComponent, setupHoldToClick } from "@/util/vue"; import {
import { computed, defineComponent, PropType, toRefs, unref, UnwrapRef } from "vue"; coerceComponent,
isCoercableComponent,
processedPropType,
setupHoldToClick,
unwrapRef
} from "@/util/vue";
import {
Component,
defineComponent,
PropType,
shallowRef,
toRefs,
unref,
UnwrapRef,
watchEffect
} from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue"; import MarkNode from "./MarkNode.vue";
import "@/components/common/features.css";
export default defineComponent({ export default defineComponent({
props: { props: {
display: { display: {
type: Object as PropType<UnwrapRef<GenericClickable["display"]>>, type: processedPropType<UnwrapRef<GenericClickable["display"]>>(
Object,
String,
Function
),
required: true required: true
}, },
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(Object, String, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
onClick: Function as PropType<VoidFunction>, onClick: Function as PropType<VoidFunction>,
onHold: Function as PropType<VoidFunction>, onHold: Function as PropType<VoidFunction>,
canClick: { canClick: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
small: Boolean, small: Boolean,
mark: [Boolean, String], mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: String, type: String,
required: true required: true
} }
}, },
components: {
LinkNode,
MarkNode
},
setup(props) { setup(props) {
const { display, onClick, onHold } = toRefs(props); const { display, onClick, onHold } = toRefs(props);
const component = computed(() => { const comp = shallowRef<Component | string>("");
const currDisplay = unref(display);
watchEffect(() => {
const currDisplay = unwrapRef(display);
if (currDisplay == null) { if (currDisplay == null) {
return null; comp.value = "";
return;
} }
if (isCoercableComponent(currDisplay)) { if (isCoercableComponent(currDisplay)) {
return coerceComponent(currDisplay); comp.value = coerceComponent(currDisplay);
return;
} }
return ( const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description, "div");
comp.value = coerceComponent(
jsx(() => (
<span> <span>
<div v-if={currDisplay.title}> {currDisplay.title ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
<component v-is={coerceComponent(currDisplay.title!, "h2")} /> <Title />
</div> </div>
<component v-is={coerceComponent(currDisplay.description, "div")} /> ) : null}
<Description />
</span> </span>
))
); );
}); });
@ -86,10 +122,9 @@ export default defineComponent({
return { return {
start, start,
stop, stop,
component, comp,
LinkNode, Visibility,
MarkNode, unref
Visibility
}; };
} }
}); });
@ -101,4 +136,8 @@ export default defineComponent({
width: 120px; width: 120px;
font-size: 10px; font-size: 10px;
} }
.clickable.small {
min-height: unset;
}
</style> </style>

View file

@ -1,13 +1,17 @@
<template> <template>
<div <div
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="{
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
}"
class="table" class="table"
> >
<div v-for="row in rows" class="row" :key="row"> <div v-for="row in unref(rows)" class="row" :class="{ mergeAdjacent }" :key="row">
<div v-for="col in cols" :key="col"> <GridCell
<GridCell v-bind="cells[row * 100 + col]" /> v-for="col in unref(cols)"
</div> :key="col"
v-bind="gatherCellProps(unref(cells)[row * 100 + col])"
/>
</div> </div>
</div> </div>
</template> </template>
@ -15,30 +19,42 @@
<script lang="ts"> <script lang="ts">
import { Visibility } from "@/features/feature"; import { Visibility } from "@/features/feature";
import { GridCell } from "@/features/grid"; import { GridCell } from "@/features/grid";
import { defineComponent, PropType } from "vue"; import { processedPropType } from "@/util/vue";
import { computed, defineComponent, unref } from "vue";
import GridCellVue from "./GridCell.vue"; import GridCellVue from "./GridCell.vue";
import "@/components/common/table.css";
import settings from "@/game/settings";
import themes from "@/data/themes";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
rows: { rows: {
type: Number, type: processedPropType<number>(Number),
required: true required: true
}, },
cols: { cols: {
type: Number, type: processedPropType<number>(Number),
required: true required: true
}, },
cells: { cells: {
type: Object as PropType<Record<string, GridCell>>, type: processedPropType<Record<string, GridCell>>(Object),
required: true required: true
} }
}, },
components: { GridCell: GridCellVue },
setup() { setup() {
return { GridCell: GridCellVue, Visibility }; const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
function gatherCellProps(cell: GridCell) {
const { visibility, onClick, onHold, display, title, style, canClick, id } = cell;
return { visibility, onClick, onHold, display, title, style, canClick, id };
}
return { unref, gatherCellProps, Visibility, mergeAdjacent };
} }
}); });
</script> </script>

View file

@ -1,9 +1,13 @@
<template> <template>
<button <button
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :class="{ feature: true, tile: true, can: unref(canClick), locked: !unref(canClick) }"
:class="{ feature: true, tile: true, can: canClick, locked: !canClick }" :style="[
:style="style" {
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? {}
]"
@click="onClick" @click="onClick"
@mousedown="start" @mousedown="start"
@mouseleave="stop" @mouseleave="stop"
@ -11,7 +15,7 @@
@touchstart="start" @touchstart="start"
@touchend="stop" @touchend="stop"
@touchcancel="stop" @touchcancel="stop"
:disabled="!canClick" :disabled="!unref(canClick)"
> >
<div v-if="title"><component :is="titleComponent" /></div> <div v-if="title"><component :is="titleComponent" /></div>
<component :is="component" style="white-space: pre-line" /> <component :is="component" style="white-space: pre-line" />
@ -21,26 +25,32 @@
<script lang="ts"> <script lang="ts">
import { CoercableComponent, StyleValue, Visibility } from "@/features/feature"; import { CoercableComponent, StyleValue, Visibility } from "@/features/feature";
import { coerceComponent, setupHoldToClick } from "@/util/vue"; import {
import { computed, defineComponent, PropType, toRefs, unref } from "vue"; computeComponent,
computeOptionalComponent,
processedPropType,
setupHoldToClick
} from "@/util/vue";
import { defineComponent, PropType, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import "@/components/common/features.css";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
onClick: Function as PropType<VoidFunction>, onClick: Function as PropType<VoidFunction>,
onHold: Function as PropType<VoidFunction>, onHold: Function as PropType<VoidFunction>,
display: { display: {
type: [Object, String] as PropType<CoercableComponent>, type: processedPropType<CoercableComponent>(Object, String, Function),
required: true required: true
}, },
title: [Object, String] as PropType<CoercableComponent>, title: processedPropType<CoercableComponent>(Object, String, Function),
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(String, Object, Array),
canClick: { canClick: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
id: { id: {
@ -48,16 +58,16 @@ export default defineComponent({
required: true required: true
} }
}, },
components: {
LinkNode
},
setup(props) { setup(props) {
const { onClick, onHold, title, display } = toRefs(props); const { onClick, onHold, title, display } = toRefs(props);
const { start, stop } = setupHoldToClick(onClick, onHold); const { start, stop } = setupHoldToClick(onClick, onHold);
const titleComponent = computed(() => { const titleComponent = computeOptionalComponent(title);
const currTitle = unref(title); const component = computeComponent(display);
return currTitle && coerceComponent(currTitle);
});
const component = computed(() => coerceComponent(unref(display)));
return { return {
start, start,
@ -65,7 +75,7 @@ export default defineComponent({
titleComponent, titleComponent,
component, component,
Visibility, Visibility,
LinkNode unref
}; };
} }
}); });

View file

@ -1,22 +1,27 @@
<template> <template>
<div <div
class="infobox" class="infobox"
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="[
:style="[{ borderColor: color }, style || []]" {
:class="{ collapsed, stacked, ...classes }" borderColor: unref(color),
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? {}
]"
:class="{ collapsed: unref(collapsed), stacked, ...unref(classes) }"
> >
<button <button
class="title" class="title"
:style="[{ backgroundColor: color }, titleStyle || []]" :style="[{ backgroundColor: unref(color) }, unref(titleStyle) || []]"
@click="collapsed = !collapsed" @click="collapsed.value = !unref(collapsed)"
> >
<span class="toggle"></span> <span class="toggle"></span>
<component :is="titleComponent" /> <component :is="titleComponent" />
</button> </button>
<CollapseTransition> <CollapseTransition>
<div v-if="!collapsed" class="body" :style="{ backgroundColor: color }"> <div v-if="!unref(collapsed)" class="body" :style="{ backgroundColor: unref(color) }">
<component :is="bodyComponent" :style="bodyStyle" /> <component :is="bodyComponent" :style="unref(bodyStyle)" />
</div> </div>
</CollapseTransition> </CollapseTransition>
<LinkNode :id="id" /> <LinkNode :id="id" />
@ -27,49 +32,55 @@
import themes from "@/data/themes"; import themes from "@/data/themes";
import { CoercableComponent, Visibility } from "@/features/feature"; import { CoercableComponent, Visibility } from "@/features/feature";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { coerceComponent } from "@/util/vue"; import { computeComponent, processedPropType } from "@/util/vue";
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue"; import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
import { computed, defineComponent, PropType, StyleValue, toRefs } from "vue"; import { computed, defineComponent, PropType, Ref, StyleValue, toRefs, unref } from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
display: { display: {
type: [Object, String] as PropType<CoercableComponent>, type: processedPropType<CoercableComponent>(Object, String, Function),
required: true required: true
}, },
title: [Object, String] as PropType<CoercableComponent>, title: {
color: String, type: processedPropType<CoercableComponent>(Object, String, Function),
required: true
},
color: processedPropType<string>(String),
collapsed: { collapsed: {
type: Boolean, type: Object as PropType<Ref<boolean>>,
required: true required: true
}, },
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(Object, String, Array),
titleStyle: Object as PropType<StyleValue>, titleStyle: processedPropType<StyleValue>(Object, String, Array),
bodyStyle: Object as PropType<StyleValue>, bodyStyle: processedPropType<StyleValue>(Object, String, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
id: { id: {
type: String, type: String,
required: true required: true
} }
}, },
components: {
LinkNode,
CollapseTransition
},
setup(props) { setup(props) {
const { title, display } = toRefs(props); const { title, display } = toRefs(props);
const titleComponent = computed(() => title.value && coerceComponent(title.value)); const titleComponent = computeComponent(title);
const bodyComponent = computed(() => coerceComponent(display.value)); const bodyComponent = computeComponent(display);
const stacked = computed(() => themes[settings.theme].stackedInfoboxes); const stacked = computed(() => themes[settings.theme].stackedInfoboxes);
return { return {
titleComponent, titleComponent,
bodyComponent, bodyComponent,
stacked, stacked,
LinkNode, unref,
CollapseTransition,
Visibility Visibility
}; };
} }

View file

@ -2,7 +2,7 @@
<div> <div>
<span v-if="showPrefix">You have </span> <span v-if="showPrefix">You have </span>
<ResourceVue :resource="resource" :color="color || 'white'" /> <ResourceVue :resource="resource" :color="color || 'white'" />
{{ resource {{ resource.displayName
}}<!-- remove whitespace --> }}<!-- remove whitespace -->
<span v-if="effectComponent">, <component :is="effectComponent" /></span> <span v-if="effectComponent">, <component :is="effectComponent" /></span>
<br /><br /> <br /><br />
@ -13,8 +13,8 @@
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent } from "@/features/feature";
import { Resource } from "@/features/resource"; import { Resource } from "@/features/resource";
import Decimal from "@/util/bignum"; import Decimal from "@/util/bignum";
import { coerceComponent } from "@/util/vue"; import { computeOptionalComponent } from "@/util/vue";
import { computed, StyleValue, toRefs } from "vue"; import { computed, Ref, StyleValue, toRefs } from "vue";
import ResourceVue from "../system/Resource.vue"; import ResourceVue from "../system/Resource.vue";
const _props = defineProps<{ const _props = defineProps<{
@ -26,10 +26,9 @@ const _props = defineProps<{
}>(); }>();
const props = toRefs(_props); const props = toRefs(_props);
const effectComponent = computed(() => { const effectComponent = computeOptionalComponent(
const effectDisplay = props.effectDisplay?.value; props.effectDisplay as Ref<CoercableComponent | undefined>
return effectDisplay && coerceComponent(effectDisplay); );
});
const showPrefix = computed(() => { const showPrefix = computed(() => {
return Decimal.lt(props.resource.value, "1e1000"); return Decimal.lt(props.resource.value, "1e1000");

View file

@ -6,7 +6,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ mark: boolean | string | undefined }>(); defineProps<{ mark?: boolean | string }>();
</script> </script>
<style scoped> <style scoped>

View file

@ -1,36 +1,45 @@
<template> <template>
<div <div
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="[
:style="style" {
:class="{ feature: true, milestone: true, done: earned, ...classes }" visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? {}
]"
:class="{ feature: true, milestone: true, done: unref(earned), ...unref(classes) }"
> >
<component v-if="component" :is="component" /> <component :is="unref(comp)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</div> </div>
</template> </template>
<script lang="tsx"> <script lang="tsx">
import { StyleValue, Visibility } from "@/features/feature"; import { jsx, StyleValue, Visibility } from "@/features/feature";
import { GenericMilestone } from "@/features/milestone"; import { GenericMilestone } from "@/features/milestone";
import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "@/util/vue";
import { computed, defineComponent, PropType, toRefs, UnwrapRef } from "vue"; import { Component, defineComponent, shallowRef, toRefs, unref, UnwrapRef, watchEffect } from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import "@/components/common/features.css";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
display: { display: {
type: Object as PropType<UnwrapRef<GenericMilestone["display"]>>, type: processedPropType<UnwrapRef<GenericMilestone["display"]>>(
String,
Object,
Function
),
required: true required: true
}, },
style: Object as PropType<StyleValue>, style: processedPropType<StyleValue>(String, Object, Array),
classes: Object as PropType<Record<string, boolean>>, classes: processedPropType<Record<string, boolean>>(Object),
earned: { earned: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
id: { id: {
@ -38,35 +47,49 @@ export default defineComponent({
required: true required: true
} }
}, },
components: {
LinkNode
},
setup(props) { setup(props) {
const { display } = toRefs(props); const { display } = toRefs(props);
const component = computed(() => { const comp = shallowRef<Component | string>("");
const currDisplay = display.value;
watchEffect(() => {
const currDisplay = unwrapRef(display);
if (currDisplay == null) { if (currDisplay == null) {
return null; comp.value = "";
return;
} }
if (isCoercableComponent(currDisplay)) { if (isCoercableComponent(currDisplay)) {
return coerceComponent(currDisplay); comp.value = coerceComponent(currDisplay);
return;
} }
return ( const Requirement = coerceComponent(currDisplay.requirement, "h3");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
comp.value = coerceComponent(
jsx(() => (
<span> <span>
<component v-is={coerceComponent(currDisplay.requirement, "h3")} /> <Requirement />
<div v-if={currDisplay.effectDisplay}> {currDisplay.effectDisplay ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
<component v-is={coerceComponent(currDisplay.effectDisplay!, "b")} /> <EffectDisplay />
</div> </div>
<div v-if={currDisplay.optionsDisplay}> ) : null}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} {currDisplay.optionsDisplay ? (
<component v-is={coerceComponent(currDisplay.optionsDisplay!, "span")} /> <div class="equal-spaced">
<OptionsDisplay />
</div> </div>
) : null}
</span> </span>
))
); );
}); });
return { return {
component, comp,
LinkNode, unref,
Visibility Visibility
}; };
} }
@ -84,11 +107,21 @@ export default defineComponent({
border-width: 4px; border-width: 4px;
border-radius: 5px; border-radius: 5px;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
margin: 0; margin-top: 0;
margin-bottom: 0;
} }
.milestone.done { .milestone.done {
background-color: var(--bought); background-color: var(--bought);
cursor: default; cursor: default;
} }
.milestone >>> .equal-spaced {
display: flex;
justify-content: center;
}
.milestone >>> .equal-spaced > * {
margin: auto;
}
</style> </style>

View file

@ -4,10 +4,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { computeComponent } from "@/util/vue";
import { computed, toRefs } from "vue"; import { toRefs } from "vue";
const _props = defineProps<{ display: CoercableComponent }>(); const _props = defineProps<{ display: CoercableComponent }>();
const { display } = toRefs(_props); const { display } = toRefs(_props);
const component = computed(() => coerceComponent(display)); const component = computeComponent(display);
</script> </script>

View file

@ -1,8 +1,15 @@
<template> <template>
<button <button
v-if="unref(visibility) !== Visibility.None"
@click="selectTab" @click="selectTab"
class="tabButton" class="tabButton"
:style="unref(style)" :style="[
{
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
glowColorStyle,
unref(style) ?? {}
]"
:class="{ :class="{
active, active,
...unref(classes) ...unref(classes)
@ -13,27 +20,44 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { CoercableComponent, StyleValue } from "@/features/feature"; import { CoercableComponent, StyleValue, Visibility } from "@/features/feature";
import { ProcessedComputable } from "@/util/computed"; import { getNotifyStyle } from "@/game/notifications";
import { computeComponent } from "@/util/vue"; import { computeComponent, processedPropType, unwrapRef } from "@/util/vue";
import { defineComponent, PropType, toRefs, unref } from "vue"; import { computed, defineComponent, toRefs, unref } from "vue";
export default defineComponent({ export default defineComponent({
props: { props: {
display: { visibility: {
type: [Object, String] as PropType<ProcessedComputable<CoercableComponent>>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
style: Object as PropType<ProcessedComputable<StyleValue>>, display: {
classes: Object as PropType<ProcessedComputable<Record<string, boolean>>>, type: processedPropType<CoercableComponent>(Object, String, Function),
active: [Object, Boolean] as PropType<ProcessedComputable<boolean>> required: true
},
style: processedPropType<StyleValue>(String, Object, Array),
classes: processedPropType<Record<string, boolean>>(Object),
glowColor: processedPropType<string>(String),
active: Boolean,
floating: Boolean
}, },
emits: ["selectTab"], emits: ["selectTab"],
setup(props, { emit }) { setup(props, { emit }) {
const { display } = toRefs(props); const { display, glowColor, floating } = toRefs(props);
const component = computeComponent(display); const component = computeComponent(display);
const glowColorStyle = computed(() => {
const color = unwrapRef(glowColor);
if (!color) {
return {};
}
if (unref(floating)) {
return getNotifyStyle(color);
}
return { boxShadow: `0px 9px 5px -6px ${color}` };
});
function selectTab() { function selectTab() {
emit("selectTab"); emit("selectTab");
} }
@ -41,7 +65,9 @@ export default defineComponent({
return { return {
selectTab, selectTab,
component, component,
unref glowColorStyle,
unref,
Visibility
}; };
} }
}); });
@ -58,6 +84,7 @@ export default defineComponent({
border-radius: 5px; border-radius: 5px;
border: 2px solid; border: 2px solid;
flex-shrink: 0; flex-shrink: 0;
border-color: var(--layer-color);
} }
.tabButton:hover { .tabButton:hover {

View file

@ -1,47 +1,79 @@
<template> <template>
<div class="tab-family-container" :class="classes" :style="style"> <div
v-if="unref(visibility) !== Visibility.None"
class="tab-family-container"
:class="{ ...unref(classes), ...tabClasses }"
:style="[
{
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? [],
tabStyle ?? []
]"
>
<Sticky class="tab-buttons-container"> <Sticky class="tab-buttons-container">
<div class="tab-buttons" :class="{ floating }"> <div class="tab-buttons" :class="{ floating }">
<TabButton <TabButton
v-for="(button, id) in tabs" v-for="(button, id) in unref(tabs)"
@selectTab="selected = id" @selectTab="selected.value = id"
:floating="floating"
:key="id" :key="id"
:active="button.tab === activeTab" :active="unref(button.tab) === unref(activeTab)"
v-bind="button" v-bind="gatherButtonProps(button)"
/> />
</div> </div>
</Sticky> </Sticky>
<template v-if="activeTab"> <template v-if="unref(activeTab)">
<component :is="display!" /> <component :is="unref(component)" />
</template> </template>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import themes from "@/data/themes"; import themes from "@/data/themes";
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent, StyleValue, Visibility } from "@/features/feature";
import { GenericTab } from "@/features/tab"; import { GenericTab } from "@/features/tab";
import { GenericTabButton } from "@/features/tabFamily"; import { GenericTabButton } from "@/features/tabFamily";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "@/util/vue";
import { computed, defineComponent, PropType, toRefs, unref } from "vue"; import {
Component,
computed,
defineComponent,
PropType,
Ref,
shallowRef,
toRefs,
unref,
watchEffect
} from "vue";
import Sticky from "../system/Sticky.vue"; import Sticky from "../system/Sticky.vue";
import TabButton from "./TabButton.vue"; import TabButton from "./TabButton.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
visibility: {
type: processedPropType<Visibility>(Number),
required: true
},
activeTab: { activeTab: {
type: Object as PropType<GenericTab | CoercableComponent | null>, type: processedPropType<GenericTab | CoercableComponent | null>(Object),
required: true required: true
}, },
selected: { selected: {
type: String, type: Object as PropType<Ref<string>>,
required: true required: true
}, },
tabs: { tabs: {
type: Object as PropType<Record<string, GenericTabButton>>, type: processedPropType<Record<string, GenericTabButton>>(Object),
required: true required: true
} },
style: processedPropType<StyleValue>(String, Object, Array),
classes: processedPropType<Record<string, boolean>>(Object)
},
components: {
Sticky,
TabButton
}, },
setup(props) { setup(props) {
const { activeTab } = toRefs(props); const { activeTab } = toRefs(props);
@ -50,19 +82,23 @@ export default defineComponent({
return themes[settings.theme].floatingTabs; return themes[settings.theme].floatingTabs;
}); });
const display = computed(() => { const component = shallowRef<Component | string>("");
const currActiveTab = activeTab.value;
return currActiveTab watchEffect(() => {
? coerceComponent( const currActiveTab = unwrapRef(activeTab);
isCoercableComponent(currActiveTab) if (currActiveTab == null) {
? currActiveTab component.value = "";
: unref(currActiveTab.display) return;
) }
: null; if (isCoercableComponent(currActiveTab)) {
component.value = coerceComponent(currActiveTab);
return;
}
component.value = coerceComponent(unref(currActiveTab.display));
}); });
const classes = computed(() => { const tabClasses = computed(() => {
const currActiveTab = activeTab.value; const currActiveTab = unwrapRef(activeTab);
const tabClasses = const tabClasses =
isCoercableComponent(currActiveTab) || !currActiveTab isCoercableComponent(currActiveTab) || !currActiveTab
? undefined ? undefined
@ -70,20 +106,26 @@ export default defineComponent({
return tabClasses; return tabClasses;
}); });
const style = computed(() => { const tabStyle = computed(() => {
const currActiveTab = activeTab.value; const currActiveTab = unwrapRef(activeTab);
return isCoercableComponent(currActiveTab) || !currActiveTab return isCoercableComponent(currActiveTab) || !currActiveTab
? undefined ? undefined
: unref(currActiveTab.style); : unref(currActiveTab.style);
}); });
function gatherButtonProps(button: GenericTabButton) {
const { display, style, classes, glowColor, visibility } = button;
return { display, style, classes, glowColor, visibility };
}
return { return {
floating, floating,
display, tabClasses,
classes, tabStyle,
style, Visibility,
Sticky, component,
TabButton gatherButtonProps,
unref
}; };
} }
}); });
@ -91,35 +133,44 @@ export default defineComponent({
<style scoped> <style scoped>
.tab-family-container { .tab-family-container {
margin: var(--feature-margin) -11px; margin: calc(50px + var(--feature-margin)) 20px var(--feature-margin) 20px;
position: relative; position: relative;
border: solid 4px var(--outline); border: solid 4px;
border-color: var(--outline);
} }
.tab-buttons:not(.floating) { .layer-tab > .tab-family-container:first-child {
text-align: left; margin: -4px -11px var(--feature-margin) -11px;
border-bottom: inherit; }
border-width: 4px;
box-sizing: border-box; .layer-tab > .tab-family-container:first-child:nth-last-child(3) {
height: 50px; border-bottom-style: none;
border-left-style: none;
border-right-style: none;
height: calc(100% + 50px);
}
.tab-family-container > :nth-child(2) {
margin-top: 20px;
}
.tab-family-container[data-v-f18896fc] > :last-child {
margin-bottom: 20px;
} }
.tab-family-container .sticky { .tab-family-container .sticky {
margin-left: unset !important; margin-left: -3px !important;
margin-right: unset !important; margin-right: -3px !important;
} }
.tab-buttons { .tab-buttons-container {
margin-bottom: 24px; width: calc(100% - 14px);
display: flex;
flex-flow: wrap;
padding-right: 60px;
z-index: 4;
} }
.tab-buttons-container:not(.floating) { .tab-buttons-container:not(.floating) {
border-top: solid 4px var(--outline); border-top: solid 4px;
border-bottom: solid 4px var(--outline); border-bottom: solid 4px;
border-color: inherit;
} }
.tab-buttons-container:not(.floating) .tab-buttons { .tab-buttons-container:not(.floating) .tab-buttons {
@ -137,6 +188,28 @@ export default defineComponent({
margin-top: -25px; margin-top: -25px;
} }
.tab-buttons {
margin-bottom: 24px;
display: flex;
flex-flow: wrap;
z-index: 4;
}
.layer-tab
> .tab-family-container:first-child:nth-last-child(3)
> .tab-buttons-container
> .tab-buttons {
padding-right: 60px;
}
.tab-buttons:not(.floating) {
text-align: left;
border-bottom: inherit;
border-width: 4px;
box-sizing: border-box;
height: 50px;
}
.modal-body .tab-buttons { .modal-body .tab-buttons {
width: 100%; width: 100%;
margin-left: 0; margin-left: 0;
@ -144,9 +217,18 @@ export default defineComponent({
padding-left: 0; padding-left: 0;
} }
.showGoBack > .tab-buttons-container:not(.floating) .subtabs { .showGoBack
padding-left: 0; > .tab-family-container
padding-right: 0; > .tab-buttons-container:not(.floating):first-child
.tab-buttons {
padding-left: 70px;
}
:not(.showGoBack)
> .tab-family-container
> .tab-buttons-container:not(.floating):first-child
.tab-buttons {
padding-left: 2px;
} }
.tab-buttons-container:not(.floating):first-child { .tab-buttons-container:not(.floating):first-child {
@ -161,10 +243,6 @@ export default defineComponent({
margin-top: -50px; margin-top: -50px;
} }
:not(.showGoBack) > .tab-buttons-container:not(.floating) .tab-buttons {
padding-left: 70px;
}
.tab-buttons-container + * { .tab-buttons-container + * {
margin-top: 20px; margin-top: 20px;
} }

View file

@ -1,64 +1,72 @@
<template> <template>
<button <button
v-if="visibility !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="visibility === Visibility.Visible" :style="[
:style="style" {
visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined
},
unref(style) ?? {}
]"
@click="purchase" @click="purchase"
:class="{ :class="{
feature: true, feature: true,
upgrade: true, upgrade: true,
can: canPurchase && !bought, can: unref(canPurchase),
locked: !canPurchase && !bought, locked: !unref(canPurchase),
bought, bought: unref(bought),
...classes ...unref(classes)
}" }"
:disabled="!canPurchase && !bought" :disabled="!unref(canPurchase)"
> >
<component v-if="component" :is="component" /> <component v-if="unref(component)" :is="unref(component)" />
<MarkNode :mark="mark" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="id" /> <LinkNode :id="id" />
</button> </button>
</template> </template>
<script lang="tsx"> <script lang="tsx">
import { StyleValue, Visibility } from "@/features/feature"; import { jsx, StyleValue, Visibility } from "@/features/feature";
import { displayResource, Resource } from "@/features/resource"; import { displayResource, Resource } from "@/features/resource";
import { GenericUpgrade } from "@/features/upgrade"; import { GenericUpgrade } from "@/features/upgrade";
import { DecimalSource } from "@/lib/break_eternity"; import { DecimalSource } from "@/lib/break_eternity";
import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "@/util/vue";
import { computed, defineComponent, PropType, Ref, toRef, toRefs, unref, UnwrapRef } from "vue"; import {
Component,
defineComponent,
PropType,
shallowRef,
toRefs,
unref,
UnwrapRef,
watchEffect
} from "vue";
import LinkNode from "../system/LinkNode.vue"; import LinkNode from "../system/LinkNode.vue";
import MarkNode from "./MarkNode.vue"; import MarkNode from "./MarkNode.vue";
import "@/components/common/features.css";
export default defineComponent({ export default defineComponent({
props: { props: {
display: { display: {
type: Object as PropType<UnwrapRef<GenericUpgrade["display"]>>, type: processedPropType<UnwrapRef<GenericUpgrade["display"]>>(String, Object, Function),
required: true required: true
}, },
visibility: { visibility: {
type: Object as PropType<Visibility>, type: processedPropType<Visibility>(Number),
required: true
},
style: Object as PropType<StyleValue>,
classes: Object as PropType<Record<string, boolean>>,
resource: {
type: Object as PropType<Resource>,
required: true
},
cost: {
type: Object as PropType<DecimalSource>,
required: true required: true
}, },
style: processedPropType<StyleValue>(String, Object, Array),
classes: processedPropType<Record<string, boolean>>(Object),
resource: Object as PropType<Resource>,
cost: processedPropType<DecimalSource>(String, Object, Number),
canPurchase: { canPurchase: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
bought: { bought: {
type: Boolean, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
mark: [Boolean, String], mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: String, type: String,
required: true required: true
@ -68,45 +76,59 @@ export default defineComponent({
required: true required: true
} }
}, },
components: {
LinkNode,
MarkNode
},
setup(props) { setup(props) {
const { display, cost } = toRefs(props); const { display, cost } = toRefs(props);
const resource = toRef(props, "resource") as unknown as Ref<Resource>;
const component = computed(() => { const component = shallowRef<Component | string>("");
const currDisplay = display.value;
watchEffect(() => {
const currDisplay = unwrapRef(display);
if (currDisplay == null) { if (currDisplay == null) {
return null; component.value = "";
return;
} }
if (isCoercableComponent(currDisplay)) { if (isCoercableComponent(currDisplay)) {
return coerceComponent(currDisplay); component.value = coerceComponent(currDisplay);
return;
} }
return ( const currCost = unwrapRef(cost);
const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description, "div");
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
component.value = coerceComponent(
jsx(() => (
<span> <span>
<div v-if={currDisplay.title}> {currDisplay.title ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
<component v-is={coerceComponent(currDisplay.title!, "h2")} /> <Title />
</div> </div>
<component v-is={coerceComponent(currDisplay.description, "div")} /> ) : null}
<div v-if={currDisplay.effectDisplay}> <Description />
<br /> {currDisplay.effectDisplay ? (
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <div>
Currently: <component v-is={coerceComponent(currDisplay.effectDisplay!)} /> Currently: <EffectDisplay />
</div> </div>
<template v-if={resource.value != null && cost.value != null}> ) : null}
{props.resource != null ? (
<>
<br /> <br />
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} Cost: {props.resource &&
Cost: {displayResource(resource.value, cost.value)}{" "} displayResource(props.resource, currCost)}{" "}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} {props.resource?.displayName}
{resource.value.displayName} </>
</template> ) : null}
</span> </span>
))
); );
}); });
return { return {
component, component,
LinkNode, unref,
MarkNode,
Visibility Visibility
}; };
} }

View file

@ -1,26 +1,26 @@
<template> <template>
<span class="row" v-for="(row, index) in nodes" :key="index"> <span class="row" v-for="(row, index) in unref(nodes)" :key="index" v-bind="$attrs">
<TreeNode <TreeNode
v-for="(node, nodeIndex) in row" v-for="(node, nodeIndex) in row"
:key="nodeIndex" :key="nodeIndex"
v-bind="node" v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip" :force-tooltip="node.forceTooltip"
/> />
</span> </span>
<span class="left-side-nodes" v-if="leftSideNodes"> <span class="left-side-nodes" v-if="unref(leftSideNodes)">
<TreeNode <TreeNode
v-for="(node, nodeIndex) in leftSideNodes" v-for="(node, nodeIndex) in unref(leftSideNodes)"
:key="nodeIndex" :key="nodeIndex"
v-bind="node" v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip" :force-tooltip="node.forceTooltip"
small small
/> />
</span> </span>
<span class="side-nodes" v-if="rightSideNodes"> <span class="side-nodes" v-if="unref(rightSideNodes)">
<TreeNode <TreeNode
v-for="(node, nodeIndex) in rightSideNodes" v-for="(node, nodeIndex) in unref(rightSideNodes)"
:key="nodeIndex" :key="nodeIndex"
v-bind="node" v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip" :force-tooltip="node.forceTooltip"
small small
/> />
@ -28,21 +28,60 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import "@/components/common/table.css";
import { GenericTreeNode } from "@/features/tree"; import { GenericTreeNode } from "@/features/tree";
import { defineComponent, PropType } from "vue"; import { processedPropType } from "@/util/vue";
import { defineComponent, unref } from "vue";
import TreeNode from "./TreeNode.vue"; import TreeNode from "./TreeNode.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
nodes: { nodes: {
type: Array as PropType<GenericTreeNode[][]>, type: processedPropType<GenericTreeNode[][]>(Array),
required: true required: true
}, },
leftSideNodes: Array as PropType<GenericTreeNode[]>, leftSideNodes: processedPropType<GenericTreeNode[]>(Array),
rightSideNodes: Array as PropType<GenericTreeNode[]> rightSideNodes: processedPropType<GenericTreeNode[]>(Array)
}, },
components: { TreeNode },
setup() { setup() {
return { TreeNode }; function gatherNodeProps(node: GenericTreeNode) {
const {
display,
visibility,
style,
classes,
tooltip,
onClick,
onHold,
color,
glowColor,
forceTooltip,
canClick,
mark,
id
} = node;
return {
display,
visibility,
style,
classes,
tooltip,
onClick,
onHold,
color,
glowColor,
forceTooltip,
canClick,
mark,
id
};
}
return {
gatherNodeProps,
unref
};
} }
}); });
</script> </script>

View file

@ -1,10 +1,10 @@
<template> <template>
<Tooltip <Tooltip
v-if="unref(visibility) !== Visibility.None" v-if="unref(visibility) !== Visibility.None"
v-show="unref(visibility) === Visibility.Visible" v-bind="tooltipToBind && gatherTooltipProps(tooltipToBind)"
v-bind="tooltipToBind"
:display="tooltipDisplay" :display="tooltipDisplay"
:force="forceTooltip" :force="forceTooltip"
:style="{ visibility: unref(visibility) === Visibility.Hidden ? 'hidden' : undefined }"
:class="{ :class="{
treeNode: true, treeNode: true,
can: unref(canClick), can: unref(canClick),
@ -31,56 +31,71 @@
]" ]"
:disabled="!unref(canClick)" :disabled="!unref(canClick)"
> >
<component :is="component" /> <component :is="unref(comp)" />
</button> </button>
<MarkNode :mark="unref(mark)" /> <MarkNode :mark="unref(mark)" />
<LinkNode :id="unref(id)" /> <LinkNode :id="id" />
</Tooltip> </Tooltip>
</template> </template>
<script lang="ts"> <script lang="ts">
import TooltipVue from "@/components/system/Tooltip.vue"; import TooltipVue from "@/components/system/Tooltip.vue";
import { CoercableComponent, StyleValue, Visibility } from "@/features/feature"; import { CoercableComponent, StyleValue, Visibility } from "@/features/feature";
import { Tooltip } from "@/features/tooltip"; import { gatherTooltipProps, Tooltip } from "@/features/tooltip";
import { ProcessedComputable } from "@/util/computed"; import { ProcessedComputable } from "@/util/computed";
import { import {
computeOptionalComponent, computeOptionalComponent,
isCoercableComponent, isCoercableComponent,
processedPropType,
setupHoldToClick, setupHoldToClick,
unwrapRef unwrapRef
} from "@/util/vue"; } from "@/util/vue";
import { computed, defineComponent, PropType, Ref, toRefs, unref } from "vue"; import {
computed,
defineComponent,
PropType,
Ref,
shallowRef,
toRefs,
unref,
watchEffect
} from "vue";
import LinkNode from "../../system/LinkNode.vue"; import LinkNode from "../../system/LinkNode.vue";
import MarkNode from "../MarkNode.vue"; import MarkNode from "../MarkNode.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
display: [Object, String] as PropType<ProcessedComputable<CoercableComponent>>, display: processedPropType<CoercableComponent>(Object, String, Function),
visibility: { visibility: {
type: Object as PropType<ProcessedComputable<Visibility>>, type: processedPropType<Visibility>(Number),
required: true required: true
}, },
style: Object as PropType<ProcessedComputable<StyleValue>>, style: processedPropType<StyleValue>(String, Object, Array),
classes: Object as PropType<ProcessedComputable<Record<string, boolean>>>, classes: processedPropType<Record<string, boolean>>(Object),
tooltip: Object as PropType<ProcessedComputable<CoercableComponent | Tooltip>>, tooltip: processedPropType<CoercableComponent | Tooltip>(Object, String, Function),
onClick: Function as PropType<VoidFunction>, onClick: Function as PropType<VoidFunction>,
onHold: Function as PropType<VoidFunction>, onHold: Function as PropType<VoidFunction>,
color: [Object, String] as PropType<ProcessedComputable<string>>, color: processedPropType<string>(String),
glowColor: [Object, String] as PropType<ProcessedComputable<string>>, glowColor: processedPropType<string>(String),
forceTooltip: { forceTooltip: {
type: Object as PropType<Ref<boolean>>, type: Object as PropType<Ref<boolean>>,
required: true required: true
}, },
canClick: { canClick: {
type: [Object, Boolean] as PropType<ProcessedComputable<boolean>>, type: processedPropType<boolean>(Boolean),
required: true required: true
}, },
mark: [Object, Boolean, String] as PropType<ProcessedComputable<boolean | string>>, mark: processedPropType<boolean | string>(Boolean, String),
id: { id: {
type: [Object, String] as PropType<ProcessedComputable<string>>, type: String,
required: true required: true
}, },
small: [Object, Boolean] as PropType<ProcessedComputable<boolean>> small: processedPropType<boolean>(Boolean)
},
components: {
Tooltip: TooltipVue,
MarkNode,
LinkNode
}, },
setup(props) { setup(props) {
const { tooltip, forceTooltip, onClick, onHold, display } = toRefs(props); const { tooltip, forceTooltip, onClick, onHold, display } = toRefs(props);
@ -93,14 +108,18 @@ export default defineComponent({
} }
} }
const component = computeOptionalComponent(display); const comp = computeOptionalComponent(display);
const tooltipDisplay = computed(() => { const tooltipDisplay = shallowRef<ProcessedComputable<CoercableComponent> | undefined>(
undefined
);
watchEffect(() => {
const currTooltip = unwrapRef(tooltip); const currTooltip = unwrapRef(tooltip);
if (typeof currTooltip === "object" && !isCoercableComponent(currTooltip)) { if (typeof currTooltip === "object" && !isCoercableComponent(currTooltip)) {
return currTooltip.display; tooltipDisplay.value = currTooltip.display;
return;
} }
return currTooltip || ""; tooltipDisplay.value = currTooltip;
}); });
const tooltipToBind = computed(() => { const tooltipToBind = computed(() => {
const currTooltip = unwrapRef(tooltip); const currTooltip = unwrapRef(tooltip);
@ -117,14 +136,12 @@ export default defineComponent({
click, click,
start, start,
stop, stop,
component, comp,
tooltipDisplay, tooltipDisplay,
tooltipToBind, tooltipToBind,
Tooltip: TooltipVue,
MarkNode,
LinkNode,
unref, unref,
Visibility, Visibility,
gatherTooltipProps,
isCoercableComponent isCoercableComponent
}; };
} }

View file

@ -4,8 +4,9 @@
<VueNextSelect <VueNextSelect
:options="options" :options="options"
v-model="value" v-model="value"
@update:model-value="onUpdate"
:min="1"
label-by="label" label-by="label"
:reduce="(option: SelectOption) => option.value"
:placeholder="placeholder" :placeholder="placeholder"
:close-on-select="closeOnSelect" :close-on-select="closeOnSelect"
/> />
@ -13,38 +14,40 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import "@/components/common/fields.css";
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { computeOptionalComponent } from "@/util/vue";
import { computed, toRefs, unref } from "vue"; import { ref, toRef, watch } from "vue";
import VueNextSelect from "vue-next-select"; import VueNextSelect from "vue-next-select";
import "vue-next-select/dist/index.css"; import "vue-next-select/dist/index.css";
export type SelectOption = { label: string; value: unknown }; export type SelectOption = { label: string; value: unknown };
const _props = defineProps<{ const props = defineProps<{
title?: CoercableComponent; title?: CoercableComponent;
modelValue?: unknown; modelValue?: unknown;
options: SelectOption[]; options: SelectOption[];
placeholder?: string; placeholder?: string;
closeOnSelect?: boolean; closeOnSelect?: boolean;
}>(); }>();
const props = toRefs(_props);
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: unknown): void; (e: "update:modelValue", value: unknown): void;
}>(); }>();
const titleComponent = computed( const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
() => props.title?.value && coerceComponent(props.title.value, "span")
);
const value = computed({ const value = ref<SelectOption | undefined>(
get() { props.options.find(option => option.value === props.modelValue)
return unref(props.modelValue); );
}, watch(toRef(props, "modelValue"), modelValue => {
set(value: unknown) { if (value.value?.value !== modelValue) {
emit("update:modelValue", value); value.value = props.options.find(option => option.value === modelValue);
} }
}); });
function onUpdate(value: SelectOption) {
emit("update:modelValue", value.value);
}
</script> </script>
<style> <style>

View file

@ -10,6 +10,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, toRefs, unref } from "vue"; import { computed, toRefs, unref } from "vue";
import Tooltip from "../system/Tooltip.vue"; import Tooltip from "../system/Tooltip.vue";
import "@/components/common/fields.css";
const _props = defineProps<{ const _props = defineProps<{
title?: string; title?: string;
@ -24,10 +25,10 @@ const emit = defineEmits<{
const value = computed({ const value = computed({
get() { get() {
return unref(props.modelValue) || 0; return String(unref(props.modelValue) || 0);
}, },
set(value: number) { set(value: string) {
emit("update:modelValue", value); emit("update:modelValue", Number(value));
} }
}); });
</script> </script>

View file

@ -30,6 +30,7 @@ import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { computed, onMounted, ref, toRefs, unref } from "vue"; import { computed, onMounted, ref, toRefs, unref } from "vue";
import VueTextareaAutosize from "vue-textarea-autosize"; import VueTextareaAutosize from "vue-textarea-autosize";
import "@/components/common/fields.css";
const _props = defineProps<{ const _props = defineProps<{
title?: CoercableComponent; title?: CoercableComponent;

View file

@ -8,22 +8,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent } from "@/features/feature";
import { coerceComponent } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { computed, toRefs, unref } from "vue"; import { computed, unref } from "vue";
import "@/components/common/fields.css";
const _props = defineProps<{ const props = defineProps<{
title?: CoercableComponent; title?: CoercableComponent;
modelValue?: boolean; modelValue?: boolean;
}>(); }>();
const props = toRefs(_props);
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void; (e: "update:modelValue", value: boolean): void;
}>(); }>();
const component = computed(() => coerceComponent(unref(props.title) || "", "span")); const component = computed(() => coerceComponent(unref(props.title) || "<span></span>", "span"));
const value = computed({ const value = computed({
get() { get() {
return !!unref(props.modelValue); return !!props.modelValue;
}, },
set(value: boolean) { set(value: boolean) {
emit("update:modelValue", value); emit("update:modelValue", value);

View file

@ -1,9 +1,16 @@
<template> <template>
<div class="table"> <div class="table">
<div class="col"> <div class="col" :class="{ mergeAdjacent }">
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import "@/components/common/table.css";
import themes from "@/data/themes";
import settings from "@/game/settings";
import { computed } from "vue";
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
</script>

View file

@ -5,7 +5,7 @@
<div class="inner-tab"> <div class="inner-tab">
<Layer <Layer
v-if="layerKeys.includes(tab)" v-if="layerKeys.includes(tab)"
v-bind="layers[tab]!" v-bind="gatherLayerProps(layers[tab]!)"
:index="index" :index="index"
:tab="() => (($refs[`tab-${index}`] as HTMLElement[] | undefined)?.[0])" :tab="() => (($refs[`tab-${index}`] as HTMLElement[] | undefined)?.[0])"
/> />
@ -18,7 +18,7 @@
<script setup lang="ts"> <script setup lang="ts">
import modInfo from "@/data/modInfo.json"; import modInfo from "@/data/modInfo.json";
import { layers } from "@/game/layers"; import { GenericLayer, layers } from "@/game/layers";
import player from "@/game/player"; import player from "@/game/player";
import { computed, toRef } from "vue"; import { computed, toRef } from "vue";
import Layer from "./Layer.vue"; import Layer from "./Layer.vue";
@ -27,6 +27,11 @@ import Nav from "./Nav.vue";
const tabs = toRef(player, "tabs"); const tabs = toRef(player, "tabs");
const layerKeys = computed(() => Object.keys(layers)); const layerKeys = computed(() => Object.keys(layers));
const useHeader = modInfo.useHeader; const useHeader = modInfo.useHeader;
function gatherLayerProps(layer: GenericLayer) {
const { display, minimized, minWidth, name, color, style, classes, links, minimizable } = layer;
return { display, minimized, minWidth, name, color, style, classes, links, minimizable };
}
</script> </script>
<style scoped> <style scoped>
@ -58,10 +63,10 @@ const useHeader = modInfo.useHeader;
.separator { .separator {
position: absolute; position: absolute;
right: -3px; right: -4px;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 6px; width: 8px;
background: var(--outline); background: var(--outline);
z-index: 1; z-index: 1;
} }
@ -72,7 +77,7 @@ const useHeader = modInfo.useHeader;
height: 4px; height: 4px;
border: none; border: none;
background: var(--outline); background: var(--outline);
margin: 7px -10px; margin: var(--feature-margin) -10px;
} }
.tab .modal-body hr { .tab .modal-body hr {

View file

@ -1,14 +1,18 @@
<template> <template>
<div class="layer-container"> <div class="layer-container" :style="{ '--layer-color': unref(color) }">
<button v-if="showGoBack" class="goBack" @click="goBack"></button> <button v-if="showGoBack" class="goBack" @click="goBack"></button>
<button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false"> <button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false">
<div>{{ unref(name) }}</div> <div>{{ unref(name) }}</div>
</button> </button>
<div class="layer-tab" :style="unref(style)" :class="unref(classes)" v-else> <div
<Links v-if="links" :links="unref(links)"> class="layer-tab"
:style="unref(style)"
:class="[{ showGoBack }, unref(classes)]"
v-else
>
<Links :links="unref(links)">
<component :is="component" /> <component :is="component" />
</Links> </Links>
<component v-else :is="component" />
</div> </div>
<button v-if="unref(minimizable)" class="minimize" @click="minimized.value = true"> <button v-if="unref(minimizable)" class="minimize" @click="minimized.value = true">
@ -22,8 +26,7 @@ import modInfo from "@/data/modInfo.json";
import { CoercableComponent, PersistentRef, StyleValue } from "@/features/feature"; import { CoercableComponent, PersistentRef, StyleValue } from "@/features/feature";
import { Link } from "@/features/links"; import { Link } from "@/features/links";
import player from "@/game/player"; import player from "@/game/player";
import { ProcessedComputable } from "@/util/computed"; import { computeComponent, processedPropType, wrapRef } from "@/util/vue";
import { computeComponent, wrapRef } from "@/util/vue";
import { computed, defineComponent, nextTick, PropType, toRefs, unref, watch } from "vue"; import { computed, defineComponent, nextTick, PropType, toRefs, unref, watch } from "vue";
export default defineComponent({ export default defineComponent({
@ -38,7 +41,7 @@ export default defineComponent({
required: true required: true
}, },
display: { display: {
type: [Object, String] as PropType<ProcessedComputable<CoercableComponent>>, type: processedPropType<CoercableComponent>(Object, String, Function),
required: true required: true
}, },
minimized: { minimized: {
@ -46,28 +49,29 @@ export default defineComponent({
required: true required: true
}, },
minWidth: { minWidth: {
type: [Object, Number] as PropType<ProcessedComputable<number>>, type: processedPropType<number>(Number),
required: true required: true
}, },
name: { name: {
type: [Object, String] as PropType<ProcessedComputable<string>>, type: processedPropType<string>(String),
required: true required: true
}, },
style: Object as PropType<ProcessedComputable<StyleValue>>, color: processedPropType<string>(String),
classes: Object as PropType<ProcessedComputable<Record<string, boolean>>>, style: processedPropType<StyleValue>(String, Object, Array),
links: [Object, Array] as PropType<ProcessedComputable<Link[]>>, classes: processedPropType<Record<string, boolean>>(Object),
minimizable: [Object, Boolean] as PropType<ProcessedComputable<boolean>> links: processedPropType<Link[]>(Array),
minimizable: processedPropType<boolean>(Boolean)
}, },
setup(props) { setup(props) {
const { display, index, minimized, minWidth, tab } = toRefs(props); const { display, index, minimized, minWidth, tab } = toRefs(props);
const component = computeComponent(display); const component = computeComponent(display);
const showGoBack = computed( const showGoBack = computed(
() => modInfo.allowGoBack && unref(index) > 0 && !minimized.value () => modInfo.allowGoBack && index.value > 0 && !minimized.value
); );
function goBack() { function goBack() {
player.tabs = player.tabs.slice(0, unref(props.index)); player.tabs.splice(unref(props.index), Infinity);
} }
nextTick(() => updateTab(minimized.value, unref(minWidth.value))); nextTick(() => updateTab(minimized.value, unref(minWidth.value)));
@ -187,6 +191,7 @@ export default defineComponent({
transform: rotate(-90deg); transform: rotate(-90deg);
top: 10px; top: 10px;
right: 18px; right: 18px;
pointer-events: none;
} }
.goBack { .goBack {

View file

@ -1,5 +1,7 @@
<template> <template>
<line <line
stroke-width="15px"
stroke="white"
v-bind="link" v-bind="link"
:x1="startPosition.x" :x1="startPosition.x"
:y1="startPosition.y" :y1="startPosition.y"

View file

@ -4,7 +4,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { RegisterLinkNodeInjectionKey, UnregisterLinkNodeInjectionKey } from "@/features/links"; import { RegisterLinkNodeInjectionKey, UnregisterLinkNodeInjectionKey } from "@/features/links";
import { computed, inject, ref, toRefs, unref, watch } from "vue"; import { computed, inject, onUnmounted, ref, toRefs, unref, watch } from "vue";
const _props = defineProps<{ id: string }>(); const _props = defineProps<{ id: string }>();
const props = toRefs(_props); const props = toRefs(_props);
@ -24,6 +24,8 @@ if (register && unregister) {
register(newID, newNode); register(newID, newNode);
} }
}); });
onUnmounted(() => unregister(unref(props.id)));
} }
</script> </script>

View file

@ -19,11 +19,11 @@ import {
RegisterLinkNodeInjectionKey, RegisterLinkNodeInjectionKey,
UnregisterLinkNodeInjectionKey UnregisterLinkNodeInjectionKey
} from "@/features/links"; } from "@/features/links";
import { computed, nextTick, onMounted, provide, ref, toRefs } from "vue"; import { computed, nextTick, onMounted, provide, ref, toRef } from "vue";
import LinkVue from "./Link.vue"; import LinkVue from "./Link.vue";
const _props = defineProps<{ links: Link[] }>(); const _props = defineProps<{ links?: Link[] }>();
const { links } = toRefs(_props); const links = toRef(_props, "links");
const observer = new MutationObserver(updateNodes); const observer = new MutationObserver(updateNodes);
const resizeObserver = new ResizeObserver(updateBounds); const resizeObserver = new ResizeObserver(updateBounds);
@ -42,8 +42,9 @@ onMounted(() => {
updateNodes(); updateNodes();
}); });
const validLinks = computed(() => const validLinks = computed(
links.value.filter(link => { () =>
links.value?.filter(link => {
const n = nodes.value; const n = nodes.value;
return ( return (
n[link.startNode.id]?.x != undefined && n[link.startNode.id]?.x != undefined &&
@ -51,7 +52,7 @@ const validLinks = computed(() =>
n[link.endNode.id]?.x != undefined && n[link.endNode.id]?.x != undefined &&
n[link.endNode.id]?.y != undefined n[link.endNode.id]?.y != undefined
); );
}) }) ?? []
); );
const observerOptions = { const observerOptions = {

View file

@ -63,7 +63,7 @@ const isAnimating = ref(false);
defineExpose({ isOpen }); defineExpose({ isOpen });
</script> </script>
<style scoped> <style>
.modal-mask { .modal-mask {
position: fixed; position: fixed;
z-index: 9998; z-index: 9998;

View file

@ -3,7 +3,7 @@
<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="changelog?.open()" class="version-container"> <div @click="changelog?.open()" class="version-container">
<Tooltip display="<span>Changelog</span>" bottom class="version" <Tooltip display="Changelog" bottom class="version"
><span>v{{ versionNumber }}</span></Tooltip ><span>v{{ versionNumber }}</span></Tooltip
> >
</div> </div>

View file

@ -25,10 +25,11 @@ import { MilestoneDisplay } from "@/features/milestone";
import player from "@/game/player"; import player from "@/game/player";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { camelToTitle } from "@/util/common"; import { camelToTitle } from "@/util/common";
import { computed, ref, toRef, toRefs } from "vue"; import { computed, ref, toRefs } from "vue";
import Toggle from "../fields/Toggle.vue"; import Toggle from "../fields/Toggle.vue";
import Select from "../fields/Select.vue"; import Select from "../fields/Select.vue";
import Tooltip from "./Tooltip.vue"; import Tooltip from "./Tooltip.vue";
import { jsx } from "@/features/feature";
const isOpen = ref(false); const isOpen = ref(false);
@ -51,31 +52,30 @@ const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
const { showTPS, hideChallenges, theme, msDisplay, unthrottled } = toRefs(settings); const { showTPS, hideChallenges, theme, msDisplay, unthrottled } = toRefs(settings);
const { autosave, offlineProd } = toRefs(player); const { autosave, offlineProd } = toRefs(player);
const devSpeed = toRef(player, "devSpeed");
const isPaused = computed({ const isPaused = computed({
get() { get() {
return devSpeed.value === 0; return player.devSpeed === 0;
}, },
set(value: boolean) { set(value: boolean) {
devSpeed.value = value ? null : 0; player.devSpeed = value ? 0 : null;
} }
}); });
const offlineProdTitle = ( const offlineProdTitle = jsx(() => (
<template> <span>
Offline Production<Tooltip display="Save-specific">*</Tooltip> Offline Production<Tooltip display="Save-specific">*</Tooltip>
</template> </span>
); ));
const autosaveTitle = ( const autosaveTitle = jsx(() => (
<template> <span>
Autosave<Tooltip display="Save-specific">*</Tooltip> Autosave<Tooltip display="Save-specific">*</Tooltip>
</template> </span>
); ));
const isPausedTitle = ( const isPausedTitle = jsx(() => (
<template> <span>
Pause game<Tooltip display="Save-specific">*</Tooltip> Pause game<Tooltip display="Save-specific">*</Tooltip>
</template> </span>
); ));
</script> </script>
<style scoped> <style scoped>

View file

@ -1,18 +1,17 @@
<template> <template>
<h2 v-bind:style="{ color, 'text-shadow': '0px 0px 10px ' + color }"> <h2 :style="{ color, 'text-shadow': '0px 0px 10px ' + color }">
{{ amount }} {{ amount }}
</h2> </h2>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { displayResource, Resource } from "@/features/resource"; import { displayResource, Resource } from "@/features/resource";
import { computed, toRefs } from "vue"; import { computed } from "vue";
const _props = defineProps<{ const props = defineProps<{
resource: Resource; resource: Resource;
color: string; color: string;
}>(); }>();
const props = toRefs(_props);
const amount = computed(() => displayResource(props.resource)); const amount = computed(() => displayResource(props.resource));
</script> </script>

View file

@ -1,9 +1,16 @@
<template> <template>
<div class="table"> <div class="table">
<div class="row"> <div class="row" :class="{ mergeAdjacent }">
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"></script> <script setup lang="ts">
import "@/components/common/table.css";
import themes from "@/data/themes";
import settings from "@/game/settings";
import { computed } from "vue";
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
</script>

View file

@ -51,7 +51,7 @@
<div class="details" v-else-if="save.error == undefined && isEditing"> <div class="details" v-else-if="save.error == undefined && isEditing">
<Text v-model="newName" class="editname" @submit="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 }}<br/>{{ save.error }}</div>
</div> </div>
</template> </template>
@ -88,7 +88,7 @@ const isEditing = ref(false);
const isConfirming = ref(false); const isConfirming = ref(false);
const newName = ref(""); const newName = ref("");
watch(isEditing, () => (newName.value = "")); watch(isEditing, () => (newName.value = save.value.name || ""));
const isActive = computed(() => save.value && save.value.id === player.id); const isActive = computed(() => save.value && save.value.id === player.id);
const currentTime = computed(() => const currentTime = computed(() =>

View file

@ -37,7 +37,7 @@
<Select <Select
v-if="Object.keys(bank).length > 0" v-if="Object.keys(bank).length > 0"
:options="bank" :options="bank"
:modelValue="[]" :modelValue="undefined"
@update:modelValue="preset => newFromPreset(preset as string)" @update:modelValue="preset => newFromPreset(preset as string)"
closeOnSelect closeOnSelect
placeholder="Select preset" placeholder="Select preset"
@ -61,7 +61,15 @@ import Modal from "@/components/system/Modal.vue";
import player, { PlayerData } from "@/game/player"; import player, { PlayerData } from "@/game/player";
import settings from "@/game/settings"; import settings from "@/game/settings";
import { getUniqueID, loadSave, save, newSave } from "@/util/save"; import { getUniqueID, loadSave, save, newSave } from "@/util/save";
import { ComponentPublicInstance, computed, nextTick, reactive, ref, unref, watch } from "vue"; import {
ComponentPublicInstance,
computed,
nextTick,
ref,
shallowReactive,
unref,
watch
} from "vue";
import Select from "../fields/Select.vue"; import Select from "../fields/Select.vue";
import Text from "../fields/Text.vue"; import Text from "../fields/Text.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
@ -121,22 +129,27 @@ let bank = ref(
}, []) }, [])
); );
const cachedSaves = reactive<Record<string, LoadablePlayerData>>({}); const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
function getCachedSave(id: string) { function getCachedSave(id: string) {
if (!(id in cachedSaves)) { if (cachedSaves[id] == null) {
const save = localStorage.getItem(id); const save = localStorage.getItem(id);
if (save == null) { if (save == null) {
cachedSaves[id] = { error: `Save with id "${id}" doesn't exist`, id }; cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
} else if (save === "dW5kZWZpbmVk") {
cachedSaves[id] = { error: `Save is undefined`, id };
} else { } else {
try { try {
cachedSaves[id] = JSON.parse(decodeURIComponent(escape(atob(save)))); cachedSaves[id] = { ...JSON.parse(decodeURIComponent(escape(atob(save)))), id };
cachedSaves[id].id = id;
} catch (error) { } catch (error) {
cachedSaves[id] = { error, id }; cachedSaves[id] = { error, id };
console.warn(
`SavesManager: Failed to load info about save with id ${id}:\n${error}\n${save}`
);
} }
} }
} }
return cachedSaves[id]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cachedSaves[id]!;
} }
// Wipe cache whenever the modal is opened // Wipe cache whenever the modal is opened
watch(isOpen, isOpen => { watch(isOpen, isOpen => {
@ -187,6 +200,7 @@ function duplicateSave(id: string) {
function 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);
cachedSaves[id] = undefined;
} }
function openSave(id: string) { function openSave(id: string) {
@ -195,6 +209,8 @@ function openSave(id: string) {
save(); save();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadSave(saves.value[id]!); loadSave(saves.value[id]!);
// Delete cached version in case of opening it again
cachedSaves[id] = undefined;
} }
function newFromPreset(preset: string) { function newFromPreset(preset: string) {
@ -217,6 +233,7 @@ function editSave(id: string, newName: string) {
save(); save();
} else { } else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave))))); localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave)))));
cachedSaves[id] = undefined;
} }
} }
} }

View file

@ -46,6 +46,7 @@ onMounted(() => {
margin-right: -10px; margin-right: -10px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
width: 100%;
z-index: 3; z-index: 3;
} }

View file

@ -1,11 +1,16 @@
<template> <template>
<div class="tpsDisplay" v-if="!tps.isNan">TPS: {{ tps }}</div> <div class="tpsDisplay" v-if="!tps.isNan()">
TPS: {{ formatWhole(tps) }}
<transition name="fade"
><span v-if="showLow" class="low">{{ formatWhole(low) }}</span></transition
>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import state from "@/game/state"; import state from "@/game/state";
import Decimal from "@/util/bignum"; import Decimal, { DecimalSource, formatWhole } from "@/util/bignum";
import { computed } from "vue"; import { computed, ref, watchEffect } from "vue";
const tps = computed(() => const tps = computed(() =>
Decimal.div( Decimal.div(
@ -13,6 +18,20 @@ const tps = computed(() =>
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0) state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
) )
); );
const lastTenFPS = ref<number[]>([]);
watchEffect(() => {
lastTenFPS.value.push(Math.round(tps.value.toNumber()));
if (lastTenFPS.value.length > 10) {
lastTenFPS.value = lastTenFPS.value.slice(1);
}
});
const low = computed(() =>
lastTenFPS.value.reduce<DecimalSource>((acc, curr) => Decimal.max(acc, curr), 0)
);
const showLow = computed(() => Decimal.sub(tps.value, low.value).gt(1));
</script> </script>
<style scoped> <style scoped>
@ -22,4 +41,12 @@ const tps = computed(() =>
bottom: 10px; bottom: 10px;
z-index: 100; z-index: 100;
} }
.low {
color: var(--danger);
}
.fade-leave-to {
opacity: 0;
}
</style> </style>

View file

@ -21,7 +21,7 @@
'--yoffset': unref(yoffset) || '0px' '--yoffset': unref(yoffset) || '0px'
}" }"
> >
<component v-if="component" :is="component" /> <component v-if="comp" :is="comp" />
</div> </div>
</transition> </transition>
</div> </div>
@ -29,35 +29,31 @@
<script lang="ts"> <script lang="ts">
import { CoercableComponent } from "@/features/feature"; import { CoercableComponent } from "@/features/feature";
import { ProcessedComputable } from "@/util/computed"; import { computeOptionalComponent, processedPropType, unwrapRef } from "@/util/vue";
import { computeOptionalComponent, unwrapRef } from "@/util/vue"; import { computed, defineComponent, ref, toRefs, unref } from "vue";
import { computed, defineComponent, PropType, ref, toRefs, unref } from "vue";
export default defineComponent({ export default defineComponent({
props: { props: {
display: { display: processedPropType<CoercableComponent>(Object, String, Function),
type: [Object, String] as PropType<ProcessedComputable<CoercableComponent>>, top: processedPropType<boolean>(Boolean),
required: true left: processedPropType<boolean>(Boolean),
}, right: processedPropType<boolean>(Boolean),
top: Boolean as PropType<ProcessedComputable<boolean>>, bottom: processedPropType<boolean>(Boolean),
left: Boolean as PropType<ProcessedComputable<boolean>>, xoffset: processedPropType<string>(String),
right: Boolean as PropType<ProcessedComputable<boolean>>, yoffset: processedPropType<string>(String),
bottom: Boolean as PropType<ProcessedComputable<boolean>>, force: processedPropType<boolean>(Boolean)
xoffset: String as PropType<ProcessedComputable<string>>,
yoffset: String as PropType<ProcessedComputable<string>>,
force: Boolean as PropType<ProcessedComputable<boolean>>
}, },
setup(props) { setup(props) {
const { display, force } = toRefs(props); const { display, force } = toRefs(props);
const isHovered = ref(false); const isHovered = ref(false);
const isShown = computed(() => unwrapRef(force) || isHovered.value); const isShown = computed(() => (unwrapRef(force) || isHovered.value) && comp.value);
const component = computeOptionalComponent(display); const comp = computeOptionalComponent(display);
return { return {
isHovered, isHovered,
isShown, isShown,
component, comp,
unref unref
}; };
} }

View file

@ -13,6 +13,6 @@ defineProps<{
width: 4px; width: 4px;
background: var(--outline); background: var(--outline);
height: 100%; height: 100%;
margin: auto 7px; margin: auto var(--feature-margin);
} }
</style> </style>

View file

@ -5,7 +5,7 @@ import {
GenericClickable GenericClickable
} from "@/features/clickable"; } from "@/features/clickable";
import { GenericConversion } from "@/features/conversion"; import { GenericConversion } from "@/features/conversion";
import { CoercableComponent, Replace, setDefault } from "@/features/feature"; import { CoercableComponent, jsx, Replace, setDefault } from "@/features/feature";
import { displayResource } from "@/features/resource"; import { displayResource } from "@/features/resource";
import { import {
createTreeNode, createTreeNode,
@ -56,55 +56,61 @@ export type GenericResetButton = Replace<
>; >;
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>( export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
options: T optionsFunc: () => T
): ResetButton<T> { ): ResetButton<T> {
setDefault(options, "showNextAt", true); return createClickable(() => {
if (options.resetDescription == null) { const resetButton = optionsFunc();
options.resetDescription = computed(() =>
Decimal.lt(proxy.conversion.gainResource.value, 1e3) ? "Reset for " : "" processComputable(resetButton as T, "showNextAt");
setDefault(resetButton, "showNextAt", true);
if (resetButton.resetDescription == null) {
resetButton.resetDescription = computed(() =>
Decimal.lt(resetButton.conversion.gainResource.value, 1e3) ? "Reset for " : ""
); );
} else {
processComputable(resetButton as T, "resetDescription");
} }
if (options.display == null) {
options.display = computed(() => { if (resetButton.display == null) {
const nextAt = unref(proxy.showNextAt) && ( resetButton.display = jsx(() => (
<template> <span>
<br /> {unref(resetButton.resetDescription as ProcessedComputable<string>)}
<b>
{displayResource(
resetButton.conversion.gainResource,
unref(resetButton.conversion.currentGain)
)}
</b>{" "}
{resetButton.conversion.gainResource.displayName}
<div v-show={unref(resetButton.showNextAt)}>
<br /> <br />
Next:{" "} Next:{" "}
{displayResource( {displayResource(
proxy.conversion.baseResource, resetButton.conversion.baseResource,
unref(proxy.conversion.nextAt) unref(resetButton.conversion.nextAt)
)}{" "} )}{" "}
{proxy.conversion.baseResource.displayName} {resetButton.conversion.baseResource.displayName}
</template> </div>
);
return (
<span>
{proxy.resetDescription}
<b>
{displayResource(
proxy.conversion.gainResource,
unref(proxy.conversion.currentGain)
)}
</b>
{proxy.conversion.gainResource.displayName}
{nextAt}
</span> </span>
));
}
if (resetButton.canClick == null) {
resetButton.canClick = computed(() =>
Decimal.gt(unref(resetButton.conversion.currentGain), 0)
); );
});
} }
if (options.canClick == null) {
options.canClick = computed(() => Decimal.gt(unref(proxy.conversion.currentGain), 0)); const onClick = resetButton.onClick;
} resetButton.onClick = function () {
const onClick = options.onClick; resetButton.conversion.convert();
options.onClick = function () { resetButton.tree.reset(resetButton.treeNode);
proxy.conversion.convert();
proxy.tree.reset(proxy.treeNode);
onClick?.(); onClick?.();
}; };
const proxy = createClickable(options) as unknown as ResetButton<T>; return resetButton;
return proxy; }) as unknown as ResetButton<T>;
} }
export interface LayerTreeNodeOptions extends TreeNodeOptions { export interface LayerTreeNodeOptions extends TreeNodeOptions {
@ -120,10 +126,13 @@ export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
>; >;
export type GenericLayerTreeNode = LayerTreeNode<LayerTreeNodeOptions>; export type GenericLayerTreeNode = LayerTreeNode<LayerTreeNodeOptions>;
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(options: T): LayerTreeNode<T> { export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
optionsFunc: () => T
): LayerTreeNode<T> {
return createTreeNode(() => {
const options = optionsFunc();
processComputable(options as T, "append"); processComputable(options as T, "append");
return {
return createTreeNode({
...options, ...options,
display: options.layerID, display: options.layerID,
onClick: onClick:
@ -131,16 +140,14 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(options: T):
? function () { ? function () {
if (player.tabs.includes(options.layerID)) { if (player.tabs.includes(options.layerID)) {
const index = player.tabs.lastIndexOf(options.layerID); const index = player.tabs.lastIndexOf(options.layerID);
player.tabs = [ player.tabs.splice(index, 1);
...player.tabs.slice(0, index),
...player.tabs.slice(index + 1)
];
} else { } else {
player.tabs = [...player.tabs, options.layerID]; player.tabs.push(options.layerID);
} }
} }
: function () { : function () {
player.tabs.splice(1, 1, options.layerID); player.tabs.splice(1, 1, options.layerID);
} }
};
}) as unknown as LayerTreeNode<T>; }) as unknown as LayerTreeNode<T>;
} }

View file

@ -1,6 +1,8 @@
import Row from "@/components/system/Row.vue";
import Tooltip from "@/components/system/Tooltip.vue"; import Tooltip from "@/components/system/Tooltip.vue";
import { main } from "@/data/mod"; import { main } from "@/data/mod";
import { createAchievement } from "@/features/achievement"; import { createAchievement } from "@/features/achievement";
import { jsx } from "@/features/feature";
import { createGrid } from "@/features/grid"; import { createGrid } from "@/features/grid";
import { createResource } from "@/features/resource"; import { createResource } from "@/features/resource";
import { createTreeNode } from "@/features/tree"; import { createTreeNode } from "@/features/tree";
@ -8,6 +10,7 @@ import { createLayer } from "@/game/layers";
import { DecimalSource } from "@/lib/break_eternity"; import { DecimalSource } from "@/lib/break_eternity";
import Decimal from "@/util/bignum"; import Decimal from "@/util/bignum";
import { render, renderRow } from "@/util/vue"; import { render, renderRow } from "@/util/vue";
import { computed } from "vue";
import f from "./f"; import f from "./f";
const layer = createLayer(() => { const layer = createLayer(() => {
@ -16,59 +19,64 @@ const layer = createLayer(() => {
const name = "Achievements"; const name = "Achievements";
const points = createResource<DecimalSource>(0, "achievement power"); const points = createResource<DecimalSource>(0, "achievement power");
const treeNode = createTreeNode({ const treeNode = createTreeNode(() => ({
tooltip: "Achievements", display: "A",
color,
tooltip: {
display: "Achievements",
right: true
},
onClick() { onClick() {
// TODO open this layer as a modal main.showAchievements.value = true;
} }
}); }));
const ach1 = createAchievement({ const ach1 = createAchievement(() => ({
image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png", image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
display: "Get me!", display: "Get me!",
tooltip() { tooltip: computed(() => {
if (this.earned.value) { if (ach1.earned.value) {
return "You did it!"; return "You did it!";
} }
return "How did this happen?"; return "How did this happen?";
}, }),
shouldEarn: true shouldEarn: true
}); }));
const ach2 = createAchievement({ const ach2 = createAchievement(() => ({
display: "Impossible!", display: "Impossible!",
tooltip() { tooltip: computed(() => {
if (this.earned.value) { if (ach2.earned.value) {
return "HOW????"; return "HOW????";
} }
return "Mwahahaha!"; return "Mwahahaha!";
}, }),
style: { color: "#04e050" } style: { color: "#04e050" }
}); }));
const ach3 = createAchievement({ const ach3 = createAchievement(() => ({
display: "EIEIO", display: "EIEIO",
tooltip: tooltip:
"Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).", "Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).",
shouldEarn: function () { shouldEarn: function () {
return Decimal.gte(f.value.points.value, 1); return Decimal.gte(f.points.value, 1);
}, },
onComplete() { onComplete() {
console.log("Bork bork bork!"); console.log("Bork bork bork!");
} }
}); }));
const achievements = [ach1, ach2, ach3]; const achievements = [ach1, ach2, ach3];
const grid = createGrid({ const grid = createGrid(() => ({
rows: 2, rows: 2,
cols: 2, cols: 2,
getStartState(id) { getStartState(id) {
return id; return id;
}, },
getStyle(id) { getStyle(id, state) {
return { backgroundColor: `#${(Number(id) * 1234) % 999999}` }; return { backgroundColor: `#${(Number(state) * 1234) % 999999}` };
}, },
// TODO display should return an object // TODO display should return an object
getTitle(id) { getTitle(id) {
let direction; let direction = "";
if (id === "101") { if (id === "101") {
direction = "top"; direction = "top";
} else if (id === "102") { } else if (id === "102") {
@ -78,29 +86,39 @@ const layer = createLayer(() => {
} else if (id === "202") { } else if (id === "202") {
direction = "right"; direction = "right";
} }
return ( return jsx(() => (
<Tooltip display={JSON.stringify(this.cells[id].style)} {...{ direction }}> <Tooltip display={JSON.stringify(this.cells[id].style)} {...{ [direction]: true }}>
<h3>Gridable #{id}</h3> <h3>Gridable #{id}</h3>
</Tooltip> </Tooltip>
); ));
}, },
getDisplay(id) { getDisplay(id, state) {
return String(id); return String(state);
}, },
getCanClick(): boolean { getCanClick() {
return Decimal.eq(main.value.points.value, 10); return Decimal.eq(main.points.value, 10);
}, },
onClick(id, state) { onClick(id, state) {
this.cells[id].state = Number(state) + 1; this.cells[id].state = Number(state) + 1;
} }
}); }));
const display = ( const display = jsx(() => (
<template> <>
{renderRow(achievements)} <Row>
{render(grid)} <Tooltip display={ach1.tooltip} bottom>
</template> {render(ach1)}
); </Tooltip>
<Tooltip display={ach2.tooltip} bottom>
{render(ach2)}
</Tooltip>
<Tooltip display={ach3.tooltip} bottom>
{render(ach3)}
</Tooltip>
</Row>
{renderRow(grid)}
</>
));
return { return {
id, id,

View file

@ -3,6 +3,7 @@ import Slider from "@/components/fields/Slider.vue";
import Text from "@/components/fields/Text.vue"; import Text from "@/components/fields/Text.vue";
import Toggle from "@/components/fields/Toggle.vue"; import Toggle from "@/components/fields/Toggle.vue";
import Column from "@/components/system/Column.vue"; import Column from "@/components/system/Column.vue";
import Modal from "@/components/system/Modal.vue";
import Resource from "@/components/system/Resource.vue"; import Resource from "@/components/system/Resource.vue";
import Row from "@/components/system/Row.vue"; import Row from "@/components/system/Row.vue";
import Spacer from "@/components/system/Spacer.vue"; import Spacer from "@/components/system/Spacer.vue";
@ -10,33 +11,39 @@ import Sticky from "@/components/system/Sticky.vue";
import VerticalRule from "@/components/system/VerticalRule.vue"; import VerticalRule from "@/components/system/VerticalRule.vue";
import { createLayerTreeNode, createResetButton } from "@/data/common"; import { createLayerTreeNode, createResetButton } from "@/data/common";
import { main } from "@/data/mod"; import { main } from "@/data/mod";
import themes from "@/data/themes";
import { createBar, Direction } from "@/features/bar"; import { createBar, Direction } from "@/features/bar";
import { createBuyable } from "@/features/buyable"; import { createBuyable } from "@/features/buyable";
import { createChallenge } from "@/features/challenge"; import { createChallenge } from "@/features/challenge";
import { createClickable } from "@/features/clickable"; import { createClickable } from "@/features/clickable";
import { createCumulativeConversion, createExponentialScaling } from "@/features/conversion"; import {
import { CoercableComponent, persistent, showIf } from "@/features/feature"; addSoftcap,
createCumulativeConversion,
createExponentialScaling
} from "@/features/conversion";
import { jsx, persistent, showIf, Visibility } from "@/features/feature";
import { createHotkey } from "@/features/hotkey"; import { createHotkey } from "@/features/hotkey";
import { createInfobox } from "@/features/infobox"; import { createInfobox } from "@/features/infobox";
import { createMilestone } from "@/features/milestone"; import { createMilestone } from "@/features/milestone";
import { createReset } from "@/features/reset"; import { createReset } from "@/features/reset";
import { addSoftcap, createResource, displayResource, trackBest } from "@/features/resource"; import { createResource, displayResource, trackBest } from "@/features/resource";
import { createTab } from "@/features/tab"; import { createTab } from "@/features/tab";
import { createTabButton, createTabFamily } from "@/features/tabFamily"; import { createTabButton, createTabFamily } from "@/features/tabFamily";
import { createTree, createTreeNode, GenericTreeNode, TreeBranch } from "@/features/tree"; import { createTree, createTreeNode, GenericTreeNode, TreeBranch } from "@/features/tree";
import { createUpgrade } from "@/features/upgrade"; import { createUpgrade } from "@/features/upgrade";
import { createLayer, getLayer } from "@/game/layers"; import { createLayer } from "@/game/layers";
import settings from "@/game/settings";
import { DecimalSource } from "@/lib/break_eternity"; import { DecimalSource } from "@/lib/break_eternity";
import Decimal, { format, formatWhole } from "@/util/bignum"; import Decimal, { format, formatWhole } from "@/util/bignum";
import { render, renderCol, renderRow } from "@/util/vue"; import { render, renderCol, renderRow } from "@/util/vue";
import { computed, Ref } from "vue"; import { computed, ComputedRef, ref } from "vue";
import f from "./f"; import f from "./f";
const layer = createLayer(() => { const layer = createLayer(() => {
const id = "c"; const id = "c";
const color = "#4BDC13"; const color = "#4BDC13";
const name = "Candies"; const name = "Candies";
const points = addSoftcap(createResource<DecimalSource>(0, "lollipops"), 1e100, 0.5); const points = createResource<DecimalSource>(0, "lollipops");
const best = trackBest(points); const best = trackBest(points);
const beep = persistent<boolean>(false); const beep = persistent<boolean>(false);
const thingy = persistent<string>("pointy"); const thingy = persistent<string>("pointy");
@ -46,14 +53,15 @@ const layer = createLayer(() => {
const waffleBoost = computed(() => Decimal.pow(points.value, 0.2)); const waffleBoost = computed(() => Decimal.pow(points.value, 0.2));
const icecreamCap = computed(() => Decimal.times(points.value, 10)); const icecreamCap = computed(() => Decimal.times(points.value, 10));
const coolInfo = createInfobox({ const coolInfo = createInfobox(() => ({
title: "Lore", title: "Lore",
titleStyle: { color: "#FE0000" }, titleStyle: { color: "#FE0000" },
display: "DEEP LORE!", display: "DEEP LORE!",
bodyStyle: { backgroundColor: "#0000EE" } bodyStyle: { backgroundColor: "#0000EE" },
}); color: "rgb(75, 220, 19)"
}));
const lollipopMilestone3 = createMilestone({ const lollipopMilestone3 = createMilestone(() => ({
shouldEarn() { shouldEarn() {
return Decimal.gte(best.value, 3); return Decimal.gte(best.value, 3);
}, },
@ -61,8 +69,8 @@ const layer = createLayer(() => {
requirement: "3 Lollipops", requirement: "3 Lollipops",
effectDisplay: "Unlock the next milestone" effectDisplay: "Unlock the next milestone"
} }
}); }));
const lollipopMilestone4 = createMilestone({ const lollipopMilestone4 = createMilestone(() => ({
visibility() { visibility() {
return showIf(lollipopMilestone3.earned.value); return showIf(lollipopMilestone3.earned.value);
}, },
@ -72,14 +80,20 @@ const layer = createLayer(() => {
display: { display: {
requirement: "4 Lollipops", requirement: "4 Lollipops",
effectDisplay: "You can toggle beep and boop (which do nothing)", effectDisplay: "You can toggle beep and boop (which do nothing)",
optionsDisplay() { optionsDisplay: jsx(() => (
return ( <>
<div style="display: flex; justify-content: center"> <Toggle
<Toggle title="beep" v-model={beep} /> title="beep"
<Toggle title="boop" v-model={f.value.boop as Ref<boolean>} /> onUpdate:modelValue={value => (beep.value = value)}
</div> modelValue={beep.value}
); />
} <Toggle
title="boop"
onUpdate:modelValue={value => (f.boop.value = value)}
modelValue={f.boop.value}
/>
</>
))
}, },
style() { style() {
if (this.earned) { if (this.earned) {
@ -87,27 +101,28 @@ const layer = createLayer(() => {
} }
return {}; return {};
} }
}); }));
const lollipopMilestones = [lollipopMilestone3, lollipopMilestone4]; const lollipopMilestones = [lollipopMilestone3, lollipopMilestone4];
const funChallenge = createChallenge({ const funChallenge = createChallenge(() => ({
title: "Fun", title: "Fun",
completionLimit: 3, completionLimit: 3,
display: { display() {
description() { return {
return `Makes the game 0% harder<br>${this.completions}/${this.completionLimit} completions`; description: `Makes the game 0% harder<br>${formatWhole(this.completions.value)}/${
}, this.completionLimit
} completions`,
goal: "Have 20 points I guess", goal: "Have 20 points I guess",
reward: "Says hi", reward: "Says hi",
effectDisplay() { effectDisplay: format(funEffect.value) + "x"
return format(funEffect.value) + "x"; };
}
}, },
visibility() { visibility() {
return showIf(Decimal.gt(best.value, 0)); return showIf(Decimal.gt(best.value, 0));
}, },
goal: 20, goal: 20,
resource: main.value.points, reset,
resource: main.points,
onComplete() { onComplete() {
console.log("hiii"); console.log("hiii");
}, },
@ -120,38 +135,40 @@ const layer = createLayer(() => {
style: { style: {
height: "200px" height: "200px"
} }
}); }));
const funEffect = computed(() => Decimal.add(points.value, 1).tetrate(0.02)); const funEffect = computed(() => Decimal.add(points.value, 1).tetrate(0.02));
const generatorUpgrade = createUpgrade({ const generatorUpgrade = createUpgrade(() => ({
display: {
title: "Generator of Genericness", title: "Generator of Genericness",
display: "Gain 1 point every second", description: "Gain 1 point every second"
},
cost: 1, cost: 1,
resource: points resource: points
}); }));
const lollipopMultiplierUpgrade = createUpgrade({ const lollipopMultiplierUpgrade = createUpgrade(() => ({
display: () => display: () => ({
`Point generation is faster based on your unspent Lollipops<br>Currently: ${format( description: "Point generation is faster based on your unspent Lollipops",
lollipopMultiplierEffect.value effectDisplay: `${format(lollipopMultiplierEffect.value)}x`
)}x`, }),
cost: 1, cost: 1,
resource: points, resource: points,
visibility: () => showIf(generatorUpgrade.bought.value) visibility: () => showIf(generatorUpgrade.bought.value)
}); }));
const lollipopMultiplierEffect = computed(() => { const lollipopMultiplierEffect = computed(() => {
let ret = Decimal.add(points.value, 1).pow(0.5); let ret = Decimal.add(points.value, 1).pow(0.5);
if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000"); if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000");
return ret; return ret;
}); });
const unlockIlluminatiUpgrade = createUpgrade({ const unlockIlluminatiUpgrade = createUpgrade(() => ({
visibility() { visibility() {
return showIf(lollipopMultiplierUpgrade.bought.value); return showIf(lollipopMultiplierUpgrade.bought.value);
}, },
canPurchase() { canAfford() {
return Decimal.lt(main.value.points.value, 7); return Decimal.lt(main.points.value, 7);
}, },
onPurchase() { onPurchase() {
main.value.points.value = Decimal.add(main.value.points.value, 7); main.points.value = Decimal.add(main.points.value, 7);
}, },
display: display:
"Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab.", "Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab.",
@ -164,10 +181,18 @@ const layer = createLayer(() => {
} }
return {}; return {};
} }
}); }));
const quasiUpgrade = createUpgrade(() => ({
resource: createResource(exhancers.amount, "Exhancers", 0),
cost: 3,
display: {
title: "This upgrade doesn't exist",
description: "Or does it?"
}
}));
const upgrades = [generatorUpgrade, lollipopMultiplierUpgrade, unlockIlluminatiUpgrade]; const upgrades = [generatorUpgrade, lollipopMultiplierUpgrade, unlockIlluminatiUpgrade];
const exhancers = createBuyable({ const exhancers = createBuyable(() => ({
resource: points, resource: points,
cost() { cost() {
let x = new Decimal(this.amount.value); let x = new Decimal(this.amount.value);
@ -177,49 +202,51 @@ const layer = createLayer(() => {
const cost = Decimal.pow(2, x.pow(1.5)); const cost = Decimal.pow(2, x.pow(1.5));
return cost.floor(); return cost.floor();
}, },
display: { display() {
return {
title: "Exhancers", title: "Exhancers",
description() { description: `Adds ${format(
return `Adds ${format( thingEffect.value
exhancersFirstEffect.value )} things and multiplies stuff by ${format(stuffEffect.value)}.`
)} things and multiplies stuff by ${format(exhancersSecondEffect.value)}.`; };
}
}, },
onPurchase(cost) { onPurchase(cost) {
spentOnBuyables.value = Decimal.add(spentOnBuyables.value, cost); spentOnBuyables.value = Decimal.add(spentOnBuyables.value, cost);
}, },
style: { height: "222px" }, style: { height: "222px" },
purchaseLimit: 4 purchaseLimit: 4
}); }));
const exhancersFirstEffect = computed(() => { // The following need redundant ComputedRef<Decimal> type annotations because otherwise the ts
// interpreter thinks exhancers are cyclically referenced
const thingEffect: ComputedRef<Decimal> = computed(() => {
if (Decimal.gte(exhancers.amount.value, 0)) { if (Decimal.gte(exhancers.amount.value, 0)) {
return Decimal.pow(25, Decimal.pow(exhancers.amount.value, 1.1)); 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)); return Decimal.pow(1 / 25, Decimal.times(exhancers.amount.value, -1).pow(1.1));
}); });
const exhancersSecondEffect = computed(() => { const stuffEffect: ComputedRef<Decimal> = computed(() => {
if (Decimal.gte(exhancers.amount.value, 0)) { if (Decimal.gte(exhancers.amount.value, 0)) {
return Decimal.pow(25, Decimal.pow(exhancers.amount.value, 1.1)); 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)); return Decimal.pow(1 / 25, Decimal.times(exhancers.amount.value, -1).pow(1.1));
}); });
const confirmRespec = persistent<boolean>(false); const confirmRespec = persistent<boolean>(false);
const respecBuyables = createClickable({ const confirming = ref(false);
const respecBuyables = createClickable(() => ({
small: true, small: true,
display: "Respec Thingies", display: "Respec Thingies",
onClick() { onClick() {
if ( if (confirmRespec.value && !confirming.value) {
confirmRespec.value && confirming.value = true;
!confirm("Are you sure? Respeccing these doesn't accomplish much.")
) {
return; return;
} }
points.value = Decimal.add(points.value, spentOnBuyables.value); points.value = Decimal.add(points.value, spentOnBuyables.value);
main.value.tree.reset(treeNode); exhancers.amount.value = 0;
main.tree.reset(treeNode);
} }
}); }));
const sellExhancer = createClickable({ const sellExhancer = createClickable(() => ({
small: true, small: true,
display: "Sell One", display: "Sell One",
onClick() { onClick() {
@ -228,20 +255,53 @@ const layer = createLayer(() => {
} }
exhancers.amount.value = Decimal.sub(exhancers.amount.value, 1); exhancers.amount.value = Decimal.sub(exhancers.amount.value, 1);
points.value = Decimal.add(points.value, exhancers.cost.value); points.value = Decimal.add(points.value, exhancers.cost.value);
spentOnBuyables.value = Decimal.sub(spentOnBuyables.value, exhancers.cost.value);
} }
}); }));
const buyablesDisplay = ( const buyablesDisplay = jsx(() => (
<Column> <Column>
<Row> <Row>
<Toggle title="Confirm" v-model={confirmRespec} /> <Toggle
{render(respecBuyables)} title="Confirm"
onUpdate:modelValue={value => (confirmRespec.value = value)}
modelValue={confirmRespec.value}
/>
{renderRow(respecBuyables)}
</Row> </Row>
{render(exhancers)} {renderRow(exhancers)}
{render(sellExhancer)} {renderRow(sellExhancer)}
<Modal
modelValue={confirming.value}
onUpdate:modelValue={value => (confirming.value = value)}
v-slots={{
header: () => <h2>Confirm Respec</h2>,
body: () => <>Are you sure? Respeccing these doesn't accomplish much</>,
footer: () => (
<div class="modal-default-footer">
<div class="modal-default-flex-grow"></div>
<button
class="button modal-default-button"
onClick={() => (confirming.value = false)}
>
Cancel
</button>
<button
class="button modal-default-button danger"
onClick={() => {
respecBuyables.onClick();
confirming.value = false;
}}
>
Respec
</button>
</div>
)
}}
/>
</Column> </Column>
); ));
const longBoi = createBar({ const longBoi = createBar(() => ({
fillStyle: { backgroundColor: "#FFFFFF" }, fillStyle: { backgroundColor: "#FFFFFF" },
baseStyle: { backgroundColor: "#696969" }, baseStyle: { backgroundColor: "#696969" },
textStyle: { color: "#04e050" }, textStyle: { color: "#04e050" },
@ -249,13 +309,13 @@ const layer = createLayer(() => {
width: 300, width: 300,
height: 30, height: 30,
progress() { progress() {
return Decimal.add(main.value.points.value, 1).log(10).div(10).toNumber(); return Decimal.add(main.points.value, 1).log(10).div(10).toNumber();
}, },
display() { display() {
return format(main.value.points.value) + " / 1e10 points"; return format(main.points.value) + " / 1e10 points";
} }
}); }));
const tallBoi = createBar({ const tallBoi = createBar(() => ({
fillStyle: { backgroundColor: "#4BEC13" }, fillStyle: { backgroundColor: "#4BEC13" },
baseStyle: { backgroundColor: "#000000" }, baseStyle: { backgroundColor: "#000000" },
textStyle: { textShadow: "0px 0px 2px #000000" }, textStyle: { textShadow: "0px 0px 2px #000000" },
@ -264,13 +324,13 @@ const layer = createLayer(() => {
width: 50, width: 50,
height: 200, height: 200,
progress() { progress() {
return Decimal.div(main.value.points.value, 100); return Decimal.div(main.points.value, 100);
}, },
display() { display() {
return formatWhole(Decimal.div(main.value.points.value, 1).min(100)) + "%"; return formatWhole(Decimal.div(main.points.value, 1).min(100)) + "%";
} }
}); }));
const flatBoi = createBar({ const flatBoi = createBar(() => ({
fillStyle: { backgroundColor: "#FE0102" }, fillStyle: { backgroundColor: "#FE0102" },
baseStyle: { backgroundColor: "#222222" }, baseStyle: { backgroundColor: "#222222" },
textStyle: { textShadow: "0px 0px 2px #000000" }, textStyle: { textShadow: "0px 0px 2px #000000" },
@ -280,39 +340,39 @@ const layer = createLayer(() => {
progress() { progress() {
return Decimal.div(points.value, 50); return Decimal.div(points.value, 50);
} }
}); }));
const conversion = createCumulativeConversion({ const conversion = createCumulativeConversion(() => ({
scaling: createExponentialScaling(10, 5, 0.5), scaling: addSoftcap(createExponentialScaling(10, 5, 0.5), 1e100, 0.5),
baseResource: main.value.points, baseResource: main.points,
gainResource: points, gainResource: points,
roundUpCost: true roundUpCost: true
}); }));
const reset = createReset({ const reset = createReset(() => ({
thingsToReset: () => [getLayer("c")] thingsToReset: (): Record<string, unknown>[] => [layer]
}); }));
const hotkeys = [ const hotkeys = [
createHotkey({ createHotkey(() => ({
key: "c", key: "c",
description: "reset for lollipops or whatever", description: "reset for lollipops or whatever",
onPress() { onPress() {
if (resetButton.canClick) { if (resetButton.canClick.value) {
reset.reset(); resetButton.onClick();
} }
} }
}), })),
createHotkey({ createHotkey(() => ({
key: "ctrl+c", key: "ctrl+c",
description: "respec things", description: "respec things",
onPress() { onPress() {
respecBuyables.onClick(); respecBuyables.onClick();
} }
}) }))
]; ];
const treeNode = createLayerTreeNode({ const treeNode = createLayerTreeNode(() => ({
layerID: id, layerID: id,
color, color,
reset, reset,
@ -330,27 +390,27 @@ const layer = createLayer(() => {
color: "#3325CC", color: "#3325CC",
textDecoration: "underline" textDecoration: "underline"
} }
}); }));
const resetButton = createResetButton({ const resetButton = createResetButton(() => ({
conversion, conversion,
tree: main.value.tree, tree: main.tree,
treeNode, treeNode,
style: { style: {
color: "#AA66AA" color: "#AA66AA"
}, },
resetDescription: "Melt your points into " resetDescription: "Melt your points into "
}); }));
const g = createTreeNode({ const g = createTreeNode(() => ({
display: "TH", display: "TH",
color: "#6d3678", color: "#6d3678",
canClick() { canClick() {
return Decimal.gte(points.value, 10); return Decimal.gte(main.points.value, 10);
}, },
tooltip: "Thanos your points", tooltip: "Thanos your points",
onClick() { onClick() {
points.value = Decimal.div(points.value, 2); main.points.value = Decimal.div(main.points.value, 2);
console.log("Thanos'd"); console.log("Thanos'd");
}, },
glowColor() { glowColor() {
@ -359,35 +419,41 @@ const layer = createLayer(() => {
} }
return ""; return "";
} }
}); }));
const h = createTreeNode({ const h = createTreeNode(() => ({
id: "h", display: "h",
tooltip() { color() {
return `Restore your points to ${format(otherThingy.value)}`; return themes[settings.theme].variables["--locked"];
},
tooltip: {
display: computed(() => `Restore your points to ${format(otherThingy.value)}`),
right: true
}, },
canClick() { canClick() {
return Decimal.lt(main.value.points.value, otherThingy.value); return Decimal.lt(main.points.value, otherThingy.value);
}, },
onClick() { onClick() {
main.value.points.value = otherThingy.value; main.points.value = otherThingy.value;
} }
}); }));
const spook = createTreeNode({}); const spook = createTreeNode(() => ({
const tree = createTree({ visibility: Visibility.Hidden
}));
const tree = createTree(() => ({
nodes(): GenericTreeNode[][] { nodes(): GenericTreeNode[][] {
return [ return [
[f.value.treeNode, treeNode], [f.treeNode, treeNode],
[g, spook, h] [g, spook, h]
]; ];
}, },
branches(): TreeBranch[] { branches(): TreeBranch[] {
return [ return [
{ {
startNode: f.value.treeNode, startNode: f.treeNode,
endNode: treeNode, endNode: treeNode,
"stroke-width": "25px",
stroke: "green",
style: { style: {
strokeWidth: "25px",
stroke: "blue",
filter: "blur(5px)" filter: "blur(5px)"
} }
}, },
@ -395,41 +461,41 @@ const layer = createLayer(() => {
{ startNode: g, endNode: h } { startNode: g, endNode: h }
]; ];
} }
}); }));
const illuminatiTabs = createTabFamily({ const illuminatiTabs = createTabFamily(() => ({
tabs: { tabs: {
first: createTabButton({ first: createTabButton({
tab: ( tab: jsx(() => (
<template> <>
{renderRow(upgrades)} {renderRow(...upgrades)}
{renderRow(quasiUpgrade)}
<div>confirmed</div> <div>confirmed</div>
</template> </>
), )),
display: "first" display: "first"
}), }),
second: createTabButton({ second: createTabButton({
tab: f.value.display as CoercableComponent, tab: f.display,
display: "second" display: "second"
}) })
}, },
style: { style: {
width: "660px", width: "660px",
height: "370px",
backgroundColor: "brown", backgroundColor: "brown",
"--background": "brown", "--background": "brown",
border: "solid white", border: "solid white",
margin: "auto" marginLeft: "auto",
marginRight: "auto"
} }
}); }));
const tabs = createTabFamily({ const tabs = createTabFamily(() => ({
tabs: { tabs: {
mainTab: createTabButton({ mainTab: createTabButton({
tab: createTab({ tab: createTab(() => ({
display() { display: jsx(() => (
return ( <>
<template>
<MainDisplay <MainDisplay
resource={points} resource={points}
color={color} color={color}
@ -444,22 +510,22 @@ const layer = createLayer(() => {
<Spacer height="5px" /> <Spacer height="5px" />
<button onClick={() => console.log("yeet")}>'HI'</button> <button onClick={() => console.log("yeet")}>'HI'</button>
<div>Name your points!</div> <div>Name your points!</div>
<Text v-model={thingy} /> <Text
modelValue={thingy.value}
onUpdate:modelValue={value => (thingy.value = value)}
/>
<Sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;"> <Sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">
I have {displayResource(main.value.points)}! I have {displayResource(main.points)} {thingy.value} points!
</Sticky> </Sticky>
<hr /> <hr />
{renderCol(lollipopMilestones)} {renderCol(...lollipopMilestones)}
<Spacer /> <Spacer />
{renderRow(upgrades)} {renderRow(...upgrades)}
{render(funChallenge)} {renderRow(quasiUpgrade)}
</template> {renderRow(funChallenge)}
); </>
}, ))
style: { })),
backgroundColor: "#3325CC"
}
}),
display: "main tab", display: "main tab",
glowColor() { glowColor() {
if ( if (
@ -475,37 +541,39 @@ const layer = createLayer(() => {
style: { color: "orange" } style: { color: "orange" }
}), }),
thingies: createTabButton({ thingies: createTabButton({
tab: createTab({ tab: createTab(() => ({
glowColor: "white",
style() { style() {
return { backgroundColor: "#222222", "--background": "#222222" }; return { backgroundColor: "#222222", "--background": "#222222" };
}, },
display() { display: jsx(() => (
return ( <>
<template> {render(buyablesDisplay)}
{buyablesDisplay}
<Spacer /> <Spacer />
<Row style="width: 600px; height: 350px; background-color: green; border-style: solid;"> <Row style="width: 600px; height: 350px; background-color: green; border-style: solid;">
<Toggle v-model={beep} /> <Toggle
onUpdate:modelValue={value => (beep.value = value)}
modelValue={beep.value}
/>
<Spacer width="30px" height="10px" /> <Spacer width="30px" height="10px" />
<div>Beep</div> <div>
<span>Beep</span>
</div>
<Spacer /> <Spacer />
<VerticalRule height="200px" /> <VerticalRule height="200px" />
</Row> </Row>
<Spacer /> <Spacer />
<img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" /> <img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" />
</template> </>
); ))
} })),
}), glowColor: "white",
display: "thingies", display: "thingies",
style: { borderColor: "orange" } style: { borderColor: "orange" }
}), }),
jail: createTabButton({ jail: createTabButton({
tab: createTab({ tab: createTab(() => ({
display() { display: jsx(() => (
return ( <>
<template>
{render(coolInfo)} {render(coolInfo)}
{render(longBoi)} {render(longBoi)}
<Spacer /> <Spacer />
@ -525,39 +593,40 @@ const layer = createLayer(() => {
<Spacer /> <Spacer />
<div>It's jail because "bars"! So funny! Ha ha!</div> <div>It's jail because "bars"! So funny! Ha ha!</div>
{render(tree)} {render(tree)}
</template> </>
); ))
}, })),
style: {
backgroundColor: "#3325CC"
}
}),
display: "jail" display: "jail"
}), }),
illuminati: createTabButton({ illuminati: createTabButton({
tab: createTab({ tab: createTab(() => ({
display() { display: jsx(() => (
return ( // This should really just be <> and </>, however for some reason the
<template> // typescript interpreter can't figure out this layer and f.tsx otherwise
<div>
<h1> C O N F I R M E D </h1> <h1> C O N F I R M E D </h1>
<Spacer /> <Spacer />
{render(illuminatiTabs)} {render(illuminatiTabs)}
<div>Adjust how many points H gives you!</div> <div>Adjust how many points H gives you!</div>
<Slider v-model={otherThingy} min={1} max={30} /> <Slider
</template> onUpdate:modelValue={value => (otherThingy.value = value)}
); modelValue={otherThingy.value}
}, min={1}
max={30}
/>
</div>
)),
style: { style: {
backgroundColor: "#3325CC" backgroundColor: "#3325CC"
} }
}), })),
visibility() { visibility() {
return showIf(unlockIlluminatiUpgrade.bought.value); return showIf(unlockIlluminatiUpgrade.bought.value);
}, },
display: "illuminati" display: "illuminati"
}) })
} }
}); }));
return { return {
id, id,
@ -568,11 +637,14 @@ const layer = createLayer(() => {
links.push({ links.push({
startNode: h, startNode: h,
endNode: flatBoi, endNode: flatBoi,
"stroke-width": "5px",
stroke: "red",
offsetEnd: { x: -50 + 100 * flatBoi.progress.value.toNumber(), y: 0 } offsetEnd: { x: -50 + 100 * flatBoi.progress.value.toNumber(), y: 0 }
}); });
return links; return links;
}, },
points, points,
best,
beep, beep,
thingy, thingy,
otherThingy, otherThingy,
@ -587,9 +659,8 @@ const layer = createLayer(() => {
lollipopMultiplierUpgrade, lollipopMultiplierUpgrade,
lollipopMultiplierEffect, lollipopMultiplierEffect,
unlockIlluminatiUpgrade, unlockIlluminatiUpgrade,
quasiUpgrade,
exhancers, exhancers,
exhancersFirstEffect,
exhancersSecondEffect,
respecBuyables, respecBuyables,
sellExhancer, sellExhancer,
bars: { tallBoi, longBoi, flatBoi }, bars: { tallBoi, longBoi, flatBoi },
@ -602,8 +673,10 @@ const layer = createLayer(() => {
hotkeys, hotkeys,
treeNode, treeNode,
resetButton, resetButton,
confirmRespec,
minWidth: 800, minWidth: 800,
display: render(tabs) tabs,
display: jsx(() => <>{render(tabs)}</>)
}; };
}); });

View file

@ -3,11 +3,11 @@ import { createLayerTreeNode, createResetButton } from "@/data/common";
import { main } from "@/data/mod"; import { main } from "@/data/mod";
import { createClickable } from "@/features/clickable"; import { createClickable } from "@/features/clickable";
import { createExponentialScaling, createIndependentConversion } from "@/features/conversion"; import { createExponentialScaling, createIndependentConversion } from "@/features/conversion";
import { persistent } from "@/features/feature"; import { jsx, persistent } from "@/features/feature";
import { createInfobox } from "@/features/infobox"; import { createInfobox } from "@/features/infobox";
import { createReset } from "@/features/reset"; import { createReset } from "@/features/reset";
import { createResource, displayResource } from "@/features/resource"; import { createResource, displayResource } from "@/features/resource";
import { createLayer, getLayer } from "@/game/layers"; import { createLayer } from "@/game/layers";
import Decimal, { DecimalSource, formatWhole } from "@/util/bignum"; import Decimal, { DecimalSource, formatWhole } from "@/util/bignum";
import { render } from "@/util/vue"; import { render } from "@/util/vue";
import c from "./c"; import c from "./c";
@ -19,20 +19,20 @@ const layer = createLayer(() => {
const points = createResource<DecimalSource>(0, "farm points"); const points = createResource<DecimalSource>(0, "farm points");
const boop = persistent<boolean>(false); const boop = persistent<boolean>(false);
const coolInfo = createInfobox({ const coolInfo = createInfobox(() => ({
title: "Lore", title: "Lore",
titleStyle: { color: "#FE0000" }, titleStyle: { color: "#FE0000" },
display: "DEEP LORE!", display: "DEEP LORE!",
bodyStyle: { backgroundColor: "#0000EE" } bodyStyle: { backgroundColor: "#0000EE" }
}); }));
const clickableState = persistent<string>("Start"); const clickableState = persistent<string>("Start");
const clickable = createClickable({ const clickable = createClickable(() => ({
display: { display() {
return {
title: "Clicky clicky!", title: "Clicky clicky!",
description() { description: "Current state:<br>" + clickableState.value
return "Current state:<br>" + clickableState.value; };
}
}, },
initialState: "Start", initialState: "Start",
canClick() { canClick() {
@ -75,9 +75,9 @@ const layer = createLayer(() => {
return {}; return {};
} }
} }
}); }));
const resetClickable = createClickable({ const resetClickable = createClickable(() => ({
onClick() { onClick() {
if (clickableState.value == "Borkened...") { if (clickableState.value == "Borkened...") {
clickableState.value = "Start"; clickableState.value = "Start";
@ -86,20 +86,20 @@ const layer = createLayer(() => {
display() { display() {
return clickableState.value == "Borkened..." ? "Fix the clickable!" : "Does nothing"; return clickableState.value == "Borkened..." ? "Fix the clickable!" : "Does nothing";
} }
}); }));
const reset = createReset({ const reset = createReset(() => ({
thingsToReset: () => [getLayer("f")] thingsToReset: (): Record<string, unknown>[] => [layer]
}); }));
const conversion = createIndependentConversion({ const conversion = createIndependentConversion(() => ({
scaling: createExponentialScaling(10, 3, 0.5), scaling: createExponentialScaling(10, 3, 0.5),
baseResource: main.value.points, baseResource: main.points,
gainResource: points, gainResource: points,
modifyGainAmount: gain => Decimal.times(gain, c.value.otherThingy.value) modifyGainAmount: gain => Decimal.times(gain, c.otherThingy.value)
}); }));
const treeNode = createLayerTreeNode({ const treeNode = createLayerTreeNode(() => ({
layerID: id, layerID: id,
color, color,
reset, reset,
@ -108,26 +108,26 @@ const layer = createLayer(() => {
return `${displayResource(points)} ${points.displayName}`; 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( return `This weird farmer dinosaur will only see you if you have at least 10 points. You only have ${displayResource(
main.value.points main.points
)}`; )}`;
}, },
canClick() { canClick() {
return Decimal.gte(main.value.points.value, 10); return Decimal.gte(main.points.value, 10);
} }
}); }));
const resetButton = createResetButton({ const resetButton = createResetButton(() => ({
conversion, conversion,
tree: main.value.tree, tree: main.tree,
treeNode, treeNode,
display() { display: jsx(() => {
if (this.conversion.buyMax) { if (resetButton.conversion.buyMax) {
return ( return (
<span> <span>
Hi! I'm a <u>weird dinosaur</u> and I'll give you{" "} Hi! I'm a <u>weird dinosaur</u> and I'll give you{" "}
<b>{formatWhole(this.conversion.currentGain.value)}</b> Farm Points in <b>{formatWhole(resetButton.conversion.currentGain.value)}</b> Farm Points
exchange for all of your points and lollipops! (You'll get another one at{" "} in exchange for all of your points and lollipops! (You'll get another one at{" "}
{formatWhole(this.conversion.nextAt.value)} points) {formatWhole(resetButton.conversion.nextAt.value)} points)
</span> </span>
); );
} else { } else {
@ -135,15 +135,15 @@ const layer = createLayer(() => {
<span> <span>
Hi! I'm a <u>weird dinosaur</u> and I'll give you a Farm Point in exchange 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{" "} for all of your points and lollipops! (At least{" "}
{formatWhole(this.conversion.nextAt.value)} points) {formatWhole(resetButton.conversion.nextAt.value)} points)
</span> </span>
); );
} }
} })
}); }));
const tab = (): JSX.Element => ( const tab = jsx(() => (
<template> <>
{render(coolInfo)} {render(coolInfo)}
<MainDisplay resource={points} color={color} /> <MainDisplay resource={points} color={color} />
{render(resetButton)} {render(resetButton)}
@ -154,8 +154,8 @@ const layer = createLayer(() => {
<div>Bork Bork!</div> <div>Bork Bork!</div>
</div> </div>
{render(clickable)} {render(clickable)}
</template> </>
); ));
return { return {
id, id,

View file

@ -1,53 +1,60 @@
import Modal from "@/components/system/Modal.vue"; import Modal from "@/components/system/Modal.vue";
import Spacer from "@/components/system/Spacer.vue"; import Spacer from "@/components/system/Spacer.vue";
import { jsx } from "@/features/feature";
import { createResource, trackBest, trackOOMPS, trackTotal } from "@/features/resource"; import { createResource, trackBest, trackOOMPS, trackTotal } from "@/features/resource";
import { createTree, GenericTree } from "@/features/tree"; import { branchedResetPropagation, createTree, GenericTree } from "@/features/tree";
import { globalBus } from "@/game/events"; import { globalBus } from "@/game/events";
import { createLayer, GenericLayer } from "@/game/layers"; import { createLayer, GenericLayer } from "@/game/layers";
import player, { PlayerData } from "@/game/player"; import player, { PlayerData } from "@/game/player";
import { DecimalSource } from "@/lib/break_eternity"; import { DecimalSource } from "@/lib/break_eternity";
import Decimal, { format, formatSmall, formatTime } from "@/util/bignum"; import Decimal, { format, formatTime } from "@/util/bignum";
import { render } from "@/util/vue"; import { render } from "@/util/vue";
import { computed, ref } from "vue"; import { computed, ref, toRaw } from "vue";
import a from "./layers/aca/a"; import a from "./layers/aca/a";
import c from "./layers/aca/c"; import c from "./layers/aca/c";
import f from "./layers/aca/f"; import f from "./layers/aca/f";
export const main = createLayer(() => { export const main = createLayer(() => {
const points = createResource<DecimalSource>(0); const points = createResource<DecimalSource>(10);
const best = trackBest(points); const best = trackBest(points);
const total = trackTotal(points); const total = trackTotal(points);
const oomps = trackOOMPS(points); const showAchievements = ref(false);
const showModal = ref(false);
const pointGain = computed(() => { const pointGain = computed(() => {
if (!c.value.generatorUpgrade.bought) return new Decimal(0); if (!c.generatorUpgrade.bought.value) return new Decimal(0);
let gain = new Decimal(3.19); let gain = new Decimal(3.19);
if (c.value.lollipopMultiplierUpgrade.bought) if (c.lollipopMultiplierUpgrade.bought.value)
gain = gain.times(c.value.lollipopMultiplierEffect.value); gain = gain.times(c.lollipopMultiplierEffect.value);
return gain; return gain;
}); });
globalBus.on("update", diff => { globalBus.on("update", diff => {
points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff)); points.value = Decimal.add(points.value, Decimal.times(pointGain.value, diff));
}); });
const oomps = trackOOMPS(points, pointGain);
// Note: Casting as generic tree to avoid recursive type definitions // Note: Casting as generic tree to avoid recursive type definitions
const tree = createTree({ const tree = createTree(() => ({
nodes: [[c.value.treeNode], [f.value.treeNode, c.value.spook]], nodes: [[c.treeNode], [f.treeNode, c.spook]],
leftSideNodes: [a.value.treeNode, c.value.h], leftSideNodes: [a.treeNode, c.h],
branches: [ branches: [
{ {
startNode: f.value.treeNode, startNode: f.treeNode,
endNode: c.value.treeNode, endNode: c.treeNode,
stroke: "blue", stroke: "blue",
"stroke-width": "25px", "stroke-width": "25px",
style: { style: {
filter: "blur(5px)" filter: "blur(5px)"
} }
}, },
{ startNode: c.value.treeNode, endNode: c.value.g } { startNode: c.treeNode, endNode: c.g }
] ],
}) as GenericTree; onReset() {
points.value = toRaw(this.resettingNode.value) === toRaw(c.treeNode) ? 0 : 10;
best.value = points.value;
total.value = points.value;
},
resetPropagation: branchedResetPropagation
})) as GenericTree;
// Note: layers don't _need_ a reference to everything, // 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. // but I'd recommend it over trying to remember what does and doesn't need to be included.
@ -56,8 +63,8 @@ export const main = createLayer(() => {
id: "main", id: "main",
name: "Tree", name: "Tree",
links: tree.links, links: tree.links,
display: ( display: jsx(() => (
<template> <>
<div v-show={player.devSpeed === 0}>Game Paused</div> <div v-show={player.devSpeed === 0}>Game Paused</div>
<div v-show={player.devSpeed && player.devSpeed !== 1}> <div v-show={player.devSpeed && player.devSpeed !== 1}>
Dev Speed: {format(player.devSpeed || 0)}x Dev Speed: {format(player.devSpeed || 0)}x
@ -70,37 +77,33 @@ export const main = createLayer(() => {
<h2>{format(points.value)}</h2> <h2>{format(points.value)}</h2>
<span v-show={Decimal.lt(points.value, "1e1e6")}> points</span> <span v-show={Decimal.lt(points.value, "1e1e6")}> points</span>
</div> </div>
<div v-show={Decimal.gt(pointGain.value, 0)}> <div v-show={Decimal.gt(pointGain.value, 0)}>({oomps.value})</div>
({oomps.value === "" ? formatSmall(pointGain.value) : oomps.value}/sec)
</div>
<Spacer /> <Spacer />
<button onClick={() => (showAchievements.value = true)}>open achievements</button>
<Modal <Modal
modelValue={showModal.value} modelValue={showAchievements.value}
onUpdate:modelValue={value => (showModal.value = value)} onUpdate:modelValue={value => (showAchievements.value = value)}
> v-slots={{
<svg style="height: 80vmin; width: 80vmin;"> header: () => <h2>Achievements</h2>,
<path d="M 32 222 Q 128 222, 128 0 Q 128 222, 224 222 L 224 224 L 32 224" /> body: a.display
}}
<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>
{render(tree)} {render(tree)}
</template> </>
), )),
points, points,
best, best,
total, total,
oomps, oomps,
tree tree,
showAchievements
}; };
}); });
export const getInitialLayers = ( export const getInitialLayers = (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
player: Partial<PlayerData> player: Partial<PlayerData>
): Array<GenericLayer> => [main.value, f.value, c.value, a.value]; ): Array<GenericLayer> => [main, f, c, a];
export const hasWon = computed(() => { export const hasWon = computed(() => {
return false; return false;

View file

@ -8,7 +8,7 @@
"versionNumber": "0.0", "versionNumber": "0.0",
"versionTitle": "Initial Commit", "versionTitle": "Initial Commit",
"allowGoBack": false, "allowGoBack": true,
"allowSmall": false, "allowSmall": false,
"defaultDecimalsShown": 2, "defaultDecimalsShown": 2,
"useHeader": true, "useHeader": true,

View file

@ -24,6 +24,7 @@ export interface Theme {
stackedInfoboxes: boolean; stackedInfoboxes: boolean;
floatingTabs: boolean; floatingTabs: boolean;
showSingleTab: boolean; showSingleTab: boolean;
mergeAdjacent: boolean;
} }
declare module "@vue/runtime-dom" { declare module "@vue/runtime-dom" {
@ -55,7 +56,8 @@ const defaultTheme: Theme = {
}, },
stackedInfoboxes: false, stackedInfoboxes: false,
floatingTabs: true, floatingTabs: true,
showSingleTab: false showSingleTab: false,
mergeAdjacent: true
}; };
export enum Themes { export enum Themes {

View file

@ -3,6 +3,7 @@ import {
CoercableComponent, CoercableComponent,
Component, Component,
findFeatures, findFeatures,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -21,7 +22,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { coerceComponent } from "@/util/vue"; import { coerceComponent } from "@/util/vue";
import { Unsubscribe } from "nanoevents"; import { Unsubscribe } from "nanoevents";
import { Ref, unref } from "vue"; import { Ref, unref } from "vue";
@ -37,7 +38,6 @@ export interface AchievementOptions {
image?: Computable<string>; image?: Computable<string>;
style?: Computable<StyleValue>; style?: Computable<StyleValue>;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
tooltip?: Computable<CoercableComponent>;
onComplete?: VoidFunction; onComplete?: VoidFunction;
} }
@ -47,6 +47,7 @@ interface BaseAchievement extends Persistent<boolean> {
complete: VoidFunction; complete: VoidFunction;
type: typeof AchievementType; type: typeof AchievementType;
[Component]: typeof AchievementComponent; [Component]: typeof AchievementComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Achievement<T extends AchievementOptions> = Replace< export type Achievement<T extends AchievementOptions> = Replace<
@ -59,7 +60,6 @@ export type Achievement<T extends AchievementOptions> = Replace<
image: GetComputableType<T["image"]>; image: GetComputableType<T["image"]>;
style: GetComputableType<T["style"]>; style: GetComputableType<T["style"]>;
classes: GetComputableType<T["classes"]>; classes: GetComputableType<T["classes"]>;
tooltip: GetComputableTypeWithDefault<T["tooltip"], GetComputableType<T["display"]>>;
} }
>; >;
@ -71,9 +71,10 @@ export type GenericAchievement = Replace<
>; >;
export function createAchievement<T extends AchievementOptions>( export function createAchievement<T extends AchievementOptions>(
options: T & ThisType<Achievement<T>> optionsFunc: () => T & ThisType<Achievement<T>>
): Achievement<T> { ): Achievement<T> {
const achievement: T & Partial<BaseAchievement> = options; return createLazyProxy(() => {
const achievement: T & Partial<BaseAchievement> = optionsFunc();
makePersistent<boolean>(achievement, false); makePersistent<boolean>(achievement, false);
achievement.id = getUniqueID("achievement-"); achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType; achievement.type = AchievementType;
@ -81,7 +82,7 @@ export function createAchievement<T extends AchievementOptions>(
achievement.earned = achievement[PersistentState]; achievement.earned = achievement[PersistentState];
achievement.complete = function () { achievement.complete = function () {
proxy[PersistentState].value = true; achievement[PersistentState].value = true;
}; };
processComputable(achievement as T, "visibility"); processComputable(achievement as T, "visibility");
@ -92,11 +93,14 @@ export function createAchievement<T extends AchievementOptions>(
processComputable(achievement as T, "image"); processComputable(achievement as T, "image");
processComputable(achievement as T, "style"); processComputable(achievement as T, "style");
processComputable(achievement as T, "classes"); processComputable(achievement as T, "classes");
processComputable(achievement as T, "tooltip");
setDefault(achievement, "tooltip", achievement.display);
const proxy = createProxy(achievement as unknown as Achievement<T>); achievement[GatherProps] = function (this: GenericAchievement) {
return proxy; const { visibility, display, earned, image, style, classes, mark, id } = this;
return { visibility, display, earned, image, style, classes, mark, id };
};
return achievement as unknown as Achievement<T>;
});
} }
const toast = useToast(); const toast = useToast();

View file

@ -2,6 +2,7 @@ import BarComponent from "@/components/features/Bar.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
setDefault, setDefault,
@ -16,7 +17,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
export const BarType = Symbol("Bar"); export const BarType = Symbol("Bar");
@ -48,6 +49,7 @@ interface BaseBar {
id: string; id: string;
type: typeof BarType; type: typeof BarType;
[Component]: typeof BarComponent; [Component]: typeof BarComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Bar<T extends BarOptions> = Replace< export type Bar<T extends BarOptions> = Replace<
@ -76,8 +78,9 @@ export type GenericBar = Replace<
} }
>; >;
export function createBar<T extends BarOptions>(options: T & ThisType<Bar<T>>): Bar<T> { export function createBar<T extends BarOptions>(optionsFunc: () => T & ThisType<Bar<T>>): Bar<T> {
const bar: T & Partial<BaseBar> = options; return createLazyProxy(() => {
const bar: T & Partial<BaseBar> = optionsFunc();
bar.id = getUniqueID("bar-"); bar.id = getUniqueID("bar-");
bar.type = BarType; bar.type = BarType;
bar[Component] = BarComponent; bar[Component] = BarComponent;
@ -97,6 +100,41 @@ export function createBar<T extends BarOptions>(options: T & ThisType<Bar<T>>):
processComputable(bar as T, "display"); processComputable(bar as T, "display");
processComputable(bar as T, "mark"); processComputable(bar as T, "mark");
const proxy = createProxy(bar as unknown as Bar<T>); bar[GatherProps] = function (this: GenericBar) {
return proxy; const {
progress,
width,
height,
direction,
display,
visibility,
style,
classes,
borderStyle,
textStyle,
baseStyle,
fillStyle,
mark,
id
} = this;
return {
progress,
width,
height,
direction,
display,
visibility,
style,
classes,
borderStyle,
textStyle,
baseStyle,
fillStyle,
mark,
id
};
};
return bar as unknown as Bar<T>;
});
} }

View file

@ -2,6 +2,7 @@ import BoardComponent from "@/components/features/board/Board.vue";
import { import {
Component, Component,
findFeatures, findFeatures,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -22,7 +23,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { Unsubscribe } from "nanoevents"; import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { Link } from "./links"; import { Link } from "./links";
@ -177,6 +178,7 @@ interface BaseBoard extends Persistent<BoardData> {
selectedAction: Ref<GenericBoardNodeAction | null>; selectedAction: Ref<GenericBoardNodeAction | null>;
type: typeof BoardType; type: typeof BoardType;
[Component]: typeof BoardComponent; [Component]: typeof BoardComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Board<T extends BoardOptions> = Replace< export type Board<T extends BoardOptions> = Replace<
@ -198,8 +200,11 @@ export type GenericBoard = Replace<
} }
>; >;
export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<T>>): Board<T> { export function createBoard<T extends BoardOptions>(
const board: T & Partial<BaseBoard> = options; optionsFunc: () => T & ThisType<Board<T>>
): Board<T> {
return createLazyProxy(() => {
const board: T & Partial<BaseBoard> = optionsFunc();
makePersistent<BoardData>(board, { makePersistent<BoardData>(board, {
nodes: [], nodes: [],
selectedNode: null, selectedNode: null,
@ -209,32 +214,37 @@ export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<
board.type = BoardType; board.type = BoardType;
board[Component] = BoardComponent; board[Component] = BoardComponent;
board.nodes = computed(() => proxy[PersistentState].value.nodes); board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed( board.selectedNode = computed(
() => () =>
proxy.nodes.value.find(node => node.id === proxy[PersistentState].value.selectedNode) || processedBoard.nodes.value.find(
null node => node.id === board[PersistentState].value.selectedNode
) || null
); );
board.selectedAction = computed(() => { board.selectedAction = computed(() => {
if (proxy.selectedNode.value == null) { const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null; return null;
} }
const type = proxy.types[proxy.selectedNode.value.type]; const type = processedBoard.types[selectedNode.type];
if (type.actions == null) { if (type.actions == null) {
return null; return null;
} }
return ( return (
type.actions.find( type.actions.find(
action => action.id === proxy[PersistentState].value.selectedAction action => action.id === processedBoard[PersistentState].value.selectedAction
) || null ) || null
); );
}); });
board.links = computed(() => { board.links = computed(() => {
if (proxy.selectedAction.value == null) { if (processedBoard.selectedAction.value == null) {
return null; return null;
} }
if (proxy.selectedAction.value.links && proxy.selectedNode.value) { if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) {
return getNodeProperty(proxy.selectedAction.value.links, proxy.selectedNode.value); return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
} }
return null; return null;
}); });
@ -250,31 +260,31 @@ export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<
for (const type in board.types) { for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type]; const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType, "title"); processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType, "label"); processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType, "size"); processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50); setDefault(nodeType, "size", 50);
processComputable(nodeType, "draggable"); processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false); setDefault(nodeType, "draggable", false);
processComputable(nodeType, "shape"); processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle); setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType, "canAccept"); processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false); setDefault(nodeType, "canAccept", false);
processComputable(nodeType, "progress"); processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType, "progressDisplay"); processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill); setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType, "progressColor"); processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none"); setDefault(nodeType, "progressColor", "none");
processComputable(nodeType, "fillColor"); processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType, "outlineColor"); processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType, "titleColor"); processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType, "actionDistance"); processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6); setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() => nodeType.nodes = computed(() =>
proxy[PersistentState].value.nodes.filter(node => node.type === type) board[PersistentState].value.nodes.filter(node => node.type === type)
); );
setDefault(nodeType, "onClick", function (node: BoardNode) { setDefault(nodeType, "onClick", function (node: BoardNode) {
proxy[PersistentState].value.selectedNode = node.id; board[PersistentState].value.selectedNode = node.id;
}); });
if (nodeType.actions) { if (nodeType.actions) {
@ -287,12 +297,42 @@ export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<
processComputable(action, "links"); processComputable(action, "links");
} }
} }
board.types[type] = createProxy(nodeType as unknown as GenericNodeType);
} }
const proxy = createProxy(board as unknown as Board<T>); board[GatherProps] = function (this: GenericBoard) {
return proxy; const {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
} = this;
return {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
};
};
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
});
} }
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T { export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {

View file

@ -1,6 +1,6 @@
import ClickableComponent from "@/components/features/Clickable.vue"; import ClickableComponent from "@/components/features/Clickable.vue";
import { Resource } from "@/features/resource"; import { Resource } from "@/features/resource";
import Decimal, { DecimalSource, format } from "@/util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -8,13 +8,15 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { isCoercableComponent } from "@/util/vue"; import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
jsx,
makePersistent, makePersistent,
Persistent, Persistent,
PersistentState, PersistentState,
@ -51,13 +53,14 @@ export interface BuyableOptions {
interface BaseBuyable extends Persistent<DecimalSource> { interface BaseBuyable extends Persistent<DecimalSource> {
id: string; id: string;
amount: Ref<DecimalSource>; amount: Ref<DecimalSource>;
bought: Ref<boolean>; maxed: Ref<boolean>;
canAfford: Ref<boolean>; canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>; canClick: ProcessedComputable<boolean>;
onClick: VoidFunction; onClick: VoidFunction;
purchase: VoidFunction; purchase: VoidFunction;
type: typeof BuyableType; type: typeof BuyableType;
[Component]: typeof ClickableComponent; [Component]: typeof ClickableComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Buyable<T extends BuyableOptions> = Replace< export type Buyable<T extends BuyableOptions> = Replace<
@ -86,85 +89,121 @@ export type GenericBuyable = Replace<
>; >;
export function createBuyable<T extends BuyableOptions>( export function createBuyable<T extends BuyableOptions>(
options: T & ThisType<Buyable<T>> optionsFunc: () => T & ThisType<Buyable<T>>
): Buyable<T> { ): Buyable<T> {
if (options.canPurchase == null && (options.resource == null || options.cost == null)) { return createLazyProxy(() => {
const buyable: T & Partial<BaseBuyable> = optionsFunc();
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn( console.warn(
"Cannot create buyable without a canPurchase property or a resource and cost property", "Cannot create buyable without a canPurchase property or a resource and cost property",
options buyable
); );
throw "Cannot create buyable without a canPurchase property or a resource and cost property"; throw "Cannot create buyable without a canPurchase property or a resource and cost property";
} }
const buyable: T & Partial<BaseBuyable> = options;
makePersistent<DecimalSource>(buyable, 0); makePersistent<DecimalSource>(buyable, 0);
buyable.id = getUniqueID("buyable-"); buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType; buyable.type = BuyableType;
buyable[Component] = ClickableComponent; buyable[Component] = ClickableComponent;
buyable.amount = buyable[PersistentState]; buyable.amount = buyable[PersistentState];
buyable.bought = computed(() => Decimal.gt(proxy.amount.value, 0)); buyable.canAfford = computed(() => {
buyable.canAfford = computed( const genericBuyable = buyable as GenericBuyable;
() => const cost = unref(genericBuyable.cost);
proxy.resource != null && return (
proxy.cost != null && genericBuyable.resource != null &&
Decimal.gte(unref<Resource>(proxy.resource).value, unref(proxy.cost)) cost != null &&
Decimal.gte(genericBuyable.resource.value, cost)
); );
});
if (buyable.canPurchase == null) { if (buyable.canPurchase == null) {
buyable.canPurchase = computed( buyable.canPurchase = computed(
() => () =>
proxy.purchaseLimit != null && unref((buyable as GenericBuyable).visibility) === Visibility.Visible &&
proxy.canAfford && unref((buyable as GenericBuyable).canAfford) &&
Decimal.lt(proxy.amount.value, unref(proxy.purchaseLimit)) Decimal.lt(
(buyable as GenericBuyable).amount.value,
unref((buyable as GenericBuyable).purchaseLimit)
)
); );
} }
buyable.maxed = computed(() =>
Decimal.gte(
(buyable as GenericBuyable).amount.value,
unref((buyable as GenericBuyable).purchaseLimit)
)
);
processComputable(buyable as T, "classes");
const classes = buyable.classes as ProcessedComputable<Record<string, boolean>> | undefined;
buyable.classes = computed(() => {
const currClasses = unref(classes) || {};
if ((buyable as GenericBuyable).maxed.value) {
currClasses.bought = true;
}
return currClasses;
});
processComputable(buyable as T, "canPurchase"); processComputable(buyable as T, "canPurchase");
// TODO once processComputable typing works, this can be replaced buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
//buyable.canClick = buyable.canPurchase;
buyable.canClick = computed(() => unref(proxy.canPurchase));
buyable.onClick = buyable.purchase = function () { buyable.onClick = buyable.purchase = function () {
if (!unref(proxy.canPurchase) || proxy.cost == null || proxy.resource == null) { const genericBuyable = buyable as GenericBuyable;
if (
!unref(genericBuyable.canPurchase) ||
genericBuyable.cost == null ||
genericBuyable.resource == null
) {
return; return;
} }
const cost = unref(proxy.cost); const cost = unref(genericBuyable.cost);
unref<Resource>(proxy.resource).value = Decimal.sub( genericBuyable.resource.value = Decimal.sub(genericBuyable.resource.value, cost);
unref<Resource>(proxy.resource).value, genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
cost
);
proxy.amount.value = Decimal.add(proxy.amount.value, 1);
this.onPurchase?.(cost); this.onPurchase?.(cost);
}; };
processComputable(buyable as T, "display"); processComputable(buyable as T, "display");
const display = buyable.display; const display = buyable.display;
buyable.display = computed(() => { buyable.display = jsx(() => {
// TODO once processComputable types correctly, remove this "as X" // TODO once processComputable types correctly, remove this "as X"
const currDisplay = unref(display) as BuyableDisplay; const currDisplay = unref(display) as BuyableDisplay;
if ( if (
currDisplay != null && currDisplay != null &&
!isCoercableComponent(currDisplay) && !isCoercableComponent(currDisplay) &&
proxy.cost != null && buyable.cost != null &&
proxy.resource != null buyable.resource != null
) { ) {
const genericBuyable = buyable as GenericBuyable;
const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description);
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
return ( return (
<span> <span>
<div v-if={currDisplay.title}> {currDisplay.title ? (
<component v-is={currDisplay.title} /> <div>
<Title />
</div> </div>
<component v-is={currDisplay.description} /> ) : null}
<Description />
<div> <div>
<br /> <br />
Amount: {format(proxy.amount.value)} / {format(unref(proxy.purchaseLimit))} Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
{formatWhole(unref(genericBuyable.purchaseLimit))}
</div> </div>
<div v-if={currDisplay.effectDisplay}> {currDisplay.effectDisplay ? (
<div>
<br /> <br />
Currently: <component v-is={currDisplay.effectDisplay} /> Currently: <EffectDisplay />
</div> </div>
) : null}
{genericBuyable.cost && !genericBuyable.maxed.value ? (
<div>
<br /> <br />
Cost: {format(unref(proxy.cost))} {unref<Resource>(proxy.resource).displayName} Cost: {format(unref(genericBuyable.cost) || 0)}{" "}
{buyable.resource.displayName}
</div>
) : null}
</span> </span>
); );
} }
return null; return "";
}); });
processComputable(buyable as T, "visibility"); processComputable(buyable as T, "visibility");
@ -173,11 +212,16 @@ export function createBuyable<T extends BuyableOptions>(
processComputable(buyable as T, "resource"); processComputable(buyable as T, "resource");
processComputable(buyable as T, "purchaseLimit"); processComputable(buyable as T, "purchaseLimit");
setDefault(buyable, "purchaseLimit", 1); setDefault(buyable, "purchaseLimit", 1);
processComputable(buyable as T, "classes");
processComputable(buyable as T, "style"); processComputable(buyable as T, "style");
processComputable(buyable as T, "mark"); processComputable(buyable as T, "mark");
processComputable(buyable as T, "small"); processComputable(buyable as T, "small");
const proxy = createProxy(buyable as unknown as Buyable<T>); buyable[GatherProps] = function (this: GenericBuyable) {
return proxy; const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
this;
return { display, visibility, style, classes, onClick, canClick, small, mark, id };
};
return buyable as unknown as Buyable<T>;
});
} }

View file

@ -2,6 +2,7 @@ import ChallengeComponent from "@/components/features/Challenge.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
persistent, persistent,
PersistentRef, PersistentRef,
@ -21,7 +22,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { GenericReset } from "./reset"; import { GenericReset } from "./reset";
@ -62,6 +63,7 @@ interface BaseChallenge {
toggle: VoidFunction; toggle: VoidFunction;
type: typeof ChallengeType; type: typeof ChallengeType;
[Component]: typeof ChallengeComponent; [Component]: typeof ChallengeComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Challenge<T extends ChallengeOptions> = Replace< export type Challenge<T extends ChallengeOptions> = Replace<
@ -97,74 +99,101 @@ export function createActiveChallenge(
} }
export function createChallenge<T extends ChallengeOptions>( export function createChallenge<T extends ChallengeOptions>(
options: T & ThisType<Challenge<T>> optionsFunc: () => T & ThisType<Challenge<T>>
): Challenge<T> { ): Challenge<T> {
if (options.canComplete == null && (options.resource == null || options.goal == null)) { return createLazyProxy(() => {
const challenge: T & Partial<BaseChallenge> = optionsFunc();
if (
challenge.canComplete == null &&
(challenge.resource == null || challenge.goal == null)
) {
console.warn( console.warn(
"Cannot create challenge without a canComplete property or a resource and goal property", "Cannot create challenge without a canComplete property or a resource and goal property",
options challenge
); );
throw "Cannot create challenge without a canComplete property or a resource and goal property"; 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.id = getUniqueID("challenge-");
challenge.type = ChallengeType; challenge.type = ChallengeType;
challenge[Component] = ChallengeComponent; challenge[Component] = ChallengeComponent;
challenge.completions = persistent(0); challenge.completions = persistent(0);
challenge.active = persistent(false); challenge.active = persistent(false);
challenge.completed = computed(() => Decimal.gt(proxy.completions.value, 0)); challenge.completed = computed(() =>
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
);
challenge.maxed = computed(() => challenge.maxed = computed(() =>
Decimal.gte(proxy.completions.value, unref(proxy.completionLimit)) Decimal.gte(
(challenge as GenericChallenge).completions.value,
unref((challenge as GenericChallenge).completionLimit)
)
); );
challenge.toggle = function () { challenge.toggle = function () {
if (proxy.active.value) { const genericChallenge = challenge as GenericChallenge;
if (proxy.canComplete && unref(proxy.canComplete) && !proxy.maxed.value) { if (genericChallenge.active.value) {
let completions: boolean | DecimalSource = unref(proxy.canComplete); if (
genericChallenge.canComplete &&
unref(genericChallenge.canComplete) &&
!genericChallenge.maxed.value
) {
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
if (typeof completions === "boolean") { if (typeof completions === "boolean") {
completions = 1; completions = 1;
} }
proxy.completions.value = Decimal.min( genericChallenge.completions.value = Decimal.min(
Decimal.add(proxy.completions.value, completions), Decimal.add(genericChallenge.completions.value, completions),
unref(proxy.completionLimit) unref(genericChallenge.completionLimit)
); );
proxy.onComplete?.(); genericChallenge.onComplete?.();
} }
proxy.active.value = false; genericChallenge.active.value = false;
proxy.onExit?.(); genericChallenge.onExit?.();
proxy.reset?.reset(); genericChallenge.reset?.reset();
} else if (unref(proxy.canStart)) { } else if (unref(genericChallenge.canStart)) {
proxy.reset?.reset(); genericChallenge.reset?.reset();
proxy.active.value = true; genericChallenge.active.value = true;
proxy.onEnter?.(); genericChallenge.onEnter?.();
} }
}; };
processComputable(challenge as T, "visibility"); processComputable(challenge as T, "visibility");
setDefault(challenge, "visibility", Visibility.Visible); setDefault(challenge, "visibility", Visibility.Visible);
const visibility = challenge.visibility as ProcessedComputable<Visibility>; const visibility = challenge.visibility as ProcessedComputable<Visibility>;
challenge.visibility = computed(() => { challenge.visibility = computed(() => {
if (settings.hideChallenges === true && unref(proxy.maxed)) { if (settings.hideChallenges === true && unref(challenge.maxed)) {
return Visibility.None; return Visibility.None;
} }
return unref(visibility); return unref(visibility);
}); });
if (challenge.canStart == null) { if (challenge.canStart == null) {
challenge.canStart = computed(() => challenge.canStart = computed(
Decimal.lt(proxy.completions.value, unref(proxy.completionLimit)) () =>
unref((challenge as GenericChallenge).visibility) === Visibility.Visible &&
Decimal.lt(
(challenge as GenericChallenge).completions.value,
unref((challenge as GenericChallenge).completionLimit)
)
); );
} }
if (challenge.canComplete == null) { if (challenge.canComplete == null) {
challenge.canComplete = computed(() => { challenge.canComplete = computed(() => {
if (!proxy.active.value || proxy.resource == null || proxy.goal == null) { const genericChallenge = challenge as GenericChallenge;
if (
!genericChallenge.active.value ||
genericChallenge.resource == null ||
genericChallenge.goal == null
) {
return false; return false;
} }
return Decimal.gte(proxy.resource.value, unref(proxy.goal)); return Decimal.gte(genericChallenge.resource.value, unref(genericChallenge.goal));
}); });
} }
if (challenge.mark == null) { if (challenge.mark == null) {
challenge.mark = computed( challenge.mark = computed(
() => Decimal.gt(unref(proxy.completionLimit), 1) && unref(proxy.maxed) () =>
Decimal.gt(unref((challenge as GenericChallenge).completionLimit), 1) &&
!!unref(challenge.maxed)
); );
} }
@ -186,8 +215,39 @@ export function createChallenge<T extends ChallengeOptions>(
}); });
} }
const proxy = createProxy(challenge as unknown as Challenge<T>); challenge[GatherProps] = function (this: GenericChallenge) {
return proxy; const {
active,
maxed,
canComplete,
display,
visibility,
style,
classes,
completed,
canStart,
mark,
id,
toggle
} = this;
return {
active,
maxed,
canComplete,
display,
visibility,
style,
classes,
completed,
canStart,
mark,
id,
toggle
};
};
return challenge as unknown as Challenge<T>;
});
} }
declare module "@/game/settings" { declare module "@/game/settings" {

View file

@ -2,6 +2,7 @@ import ClickableComponent from "@/components/features/Clickable.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
setDefault, setDefault,
@ -15,7 +16,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
export const ClickableType = Symbol("Clickable"); export const ClickableType = Symbol("Clickable");
@ -41,6 +42,7 @@ interface BaseClickable {
id: string; id: string;
type: typeof ClickableType; type: typeof ClickableType;
[Component]: typeof ClickableComponent; [Component]: typeof ClickableComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Clickable<T extends ClickableOptions> = Replace< export type Clickable<T extends ClickableOptions> = Replace<
@ -64,9 +66,10 @@ export type GenericClickable = Replace<
>; >;
export function createClickable<T extends ClickableOptions>( export function createClickable<T extends ClickableOptions>(
options: T & ThisType<Clickable<T>> optionsFunc: () => T & ThisType<Clickable<T>>
): Clickable<T> { ): Clickable<T> {
const clickable: T & Partial<BaseClickable> = options; return createLazyProxy(() => {
const clickable: T & Partial<BaseClickable> = optionsFunc();
clickable.id = getUniqueID("clickable-"); clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType; clickable.type = ClickableType;
clickable[Component] = ClickableComponent; clickable[Component] = ClickableComponent;
@ -74,11 +77,39 @@ export function createClickable<T extends ClickableOptions>(
processComputable(clickable as T, "visibility"); processComputable(clickable as T, "visibility");
setDefault(clickable, "visibility", Visibility.Visible); setDefault(clickable, "visibility", Visibility.Visible);
processComputable(clickable as T, "canClick"); processComputable(clickable as T, "canClick");
setDefault(clickable, "canClick", true);
processComputable(clickable as T, "classes"); processComputable(clickable as T, "classes");
processComputable(clickable as T, "style"); processComputable(clickable as T, "style");
processComputable(clickable as T, "mark"); processComputable(clickable as T, "mark");
processComputable(clickable as T, "display"); processComputable(clickable as T, "display");
const proxy = createProxy(clickable as unknown as Clickable<T>); clickable[GatherProps] = function (this: GenericClickable) {
return proxy; const {
display,
visibility,
style,
classes,
onClick,
onHold,
canClick,
small,
mark,
id
} = this;
return {
display,
visibility,
style,
classes,
onClick,
onHold,
canClick,
small,
mark,
id
};
};
return clickable as unknown as Clickable<T>;
});
} }

View file

@ -6,7 +6,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, isRef, Ref, unref } from "vue"; import { computed, isRef, Ref, unref } from "vue";
import { Replace, setDefault } from "./feature"; import { Replace, setDefault } from "./feature";
import { Resource } from "./resource"; import { Resource } from "./resource";
@ -48,30 +48,37 @@ export type GenericConversion = Replace<
>; >;
export function createConversion<T extends ConversionOptions>( export function createConversion<T extends ConversionOptions>(
options: T & ThisType<Conversion<T>> optionsFunc: () => T & ThisType<Conversion<T>>
): Conversion<T> { ): Conversion<T> {
const conversion: T = options; return createLazyProxy(() => {
const conversion: T = optionsFunc();
if (conversion.currentGain == null) {
conversion.currentGain = computed(() =>
conversion.scaling.currentGain(conversion as GenericConversion)
);
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() =>
conversion.scaling.nextAt(conversion as GenericConversion)
);
}
if (conversion.convert == null) { if (conversion.convert == null) {
conversion.convert = function () { conversion.convert = function () {
unref<Resource>(proxy.gainResource).value = Decimal.add( conversion.gainResource.value = Decimal.add(
unref<Resource>(proxy.gainResource).value, conversion.gainResource.value,
proxy.modifyGainAmount conversion.modifyGainAmount
? proxy.modifyGainAmount(unref(proxy.currentGain)) ? conversion.modifyGainAmount(
: unref(proxy.currentGain) unref((conversion as GenericConversion).currentGain)
)
: unref((conversion as GenericConversion).currentGain)
); );
// TODO just subtract cost? // TODO just subtract cost?
proxy.baseResource.value = 0; conversion.baseResource.value = 0;
}; };
} }
if (conversion.currentGain == null) {
conversion.currentGain = computed(() => proxy.scaling.currentGain(proxy));
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() => proxy.scaling.nextAt(proxy));
}
processComputable(conversion as T, "currentGain"); processComputable(conversion as T, "currentGain");
processComputable(conversion as T, "nextAt"); processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax"); processComputable(conversion as T, "buyMax");
@ -79,8 +86,8 @@ export function createConversion<T extends ConversionOptions>(
processComputable(conversion as T, "roundUpCost"); processComputable(conversion as T, "roundUpCost");
setDefault(conversion, "roundUpCost", true); setDefault(conversion, "roundUpCost", true);
const proxy = createProxy(conversion as unknown as Conversion<T>); return conversion as unknown as Conversion<T>;
return proxy; });
} }
export type ScalingFunction = { export type ScalingFunction = {
@ -88,14 +95,22 @@ export type ScalingFunction = {
nextAt: (conversion: GenericConversion) => DecimalSource; nextAt: (conversion: GenericConversion) => DecimalSource;
}; };
// Gain formula is (baseResource - base) * coefficient
// e.g. if base is 10 and coefficient is 0.5, 10 points makes 1 gain, 12 points is 2
export function createLinearScaling( export function createLinearScaling(
base: DecimalSource | Ref<DecimalSource>, base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource> coefficient: DecimalSource | Ref<DecimalSource>
): ScalingFunction { ): ScalingFunction {
return { return {
currentGain(conversion) { currentGain(conversion) {
let gain = Decimal.sub(unref<Resource>(conversion.baseResource).value, unref(base)) if (Decimal.lt(conversion.baseResource.value, unref(base))) {
.div(unref(coefficient)) return 0;
}
let gain = Decimal.sub(conversion.baseResource.value, unref(base))
.sub(1)
.times(unref(coefficient))
.add(1)
.floor() .floor()
.max(0); .max(0);
@ -115,8 +130,8 @@ export function createLinearScaling(
}; };
} }
// Note: Not sure this actually should be described as exponential // Gain formula is (baseResource / base) ^ exponent
// Gain formula is base * coefficient ^ (baseResource ^ exponent) // e.g. if exponent is 0.5 and base is 10, then having 10 points makes gain 1, and 40 points is 2
export function createExponentialScaling( export function createExponentialScaling(
base: DecimalSource | Ref<DecimalSource>, base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource>, coefficient: DecimalSource | Ref<DecimalSource>,
@ -124,12 +139,15 @@ export function createExponentialScaling(
): ScalingFunction { ): ScalingFunction {
return { return {
currentGain(conversion) { currentGain(conversion) {
let gain = Decimal.div(unref<Resource>(conversion.baseResource).value, unref(base)) let gain = Decimal.div(conversion.baseResource.value, unref(base))
.log(unref(coefficient)) .pow(unref(exponent))
.pow(Decimal.div(1, unref(exponent)))
.floor() .floor()
.max(0); .max(0);
if (gain.isNan()) {
return new Decimal(0);
}
if (!conversion.buyMax) { if (!conversion.buyMax) {
gain = gain.min(1); gain = gain.min(1);
} }
@ -147,51 +165,89 @@ export function createExponentialScaling(
} }
export function createCumulativeConversion<S extends ConversionOptions>( export function createCumulativeConversion<S extends ConversionOptions>(
options: S & ThisType<Conversion<S>> optionsFunc: () => S & ThisType<Conversion<S>>
): Conversion<S> { ): Conversion<S> {
return createConversion(options); return createConversion(optionsFunc);
} }
export function createIndependentConversion<S extends ConversionOptions>( export function createIndependentConversion<S extends ConversionOptions>(
options: S & ThisType<Conversion<S>> optionsFunc: () => S & ThisType<Conversion<S>>
): Conversion<S> { ): Conversion<S> {
const conversion: S = options; return createConversion(() => {
const conversion: S = optionsFunc();
setDefault(conversion, "buyMax", false); setDefault(conversion, "buyMax", false);
if (conversion.currentGain == null) { if (conversion.currentGain == null) {
conversion.currentGain = computed(() => conversion.currentGain = computed(() =>
Decimal.sub(proxy.scaling.currentGain(proxy), unref<Resource>(proxy.gainResource).value) Decimal.sub(
conversion.scaling.currentGain(conversion as GenericConversion),
conversion.gainResource.value
)
.add(1) .add(1)
.max(1) .max(1)
); );
} }
setDefault(conversion, "convert", function () { setDefault(conversion, "convert", function () {
unref<Resource>(proxy.gainResource).value = proxy.modifyGainAmount conversion.gainResource.value = conversion.modifyGainAmount
? proxy.modifyGainAmount(unref(proxy.currentGain)) ? conversion.modifyGainAmount(unref((conversion as GenericConversion).currentGain))
: unref(proxy.currentGain); : unref((conversion as GenericConversion).currentGain);
// TODO just subtract cost? // TODO just subtract cost?
// Maybe by adding a cost function to scaling and nextAt just calls the cost function // Maybe by adding a cost function to scaling and nextAt just calls the cost function
// with 1 + currentGain // with 1 + currentGain
proxy.baseResource.value = 0; conversion.baseResource.value = 0;
}); });
const proxy = createConversion(conversion); return conversion;
return proxy; });
} }
export function setupPassiveGeneration( export function setupPassiveGeneration(
layer: GenericLayer, layer: GenericLayer,
conversion: GenericConversion, conversion: GenericConversion,
rate: DecimalSource | Ref<DecimalSource> = 1 rate: ProcessedComputable<DecimalSource> = 1
): void { ): void {
layer.on("preUpdate", (diff: Decimal) => { layer.on("preUpdate", (diff: Decimal) => {
const currRate = isRef(rate) ? rate.value : rate; const currRate = isRef(rate) ? rate.value : rate;
if (Decimal.neq(currRate, 0)) { if (Decimal.neq(currRate, 0)) {
conversion.gainResource.value = Decimal.add( conversion.gainResource.value = Decimal.add(
unref<Resource>(conversion.gainResource).value, conversion.gainResource.value,
Decimal.times(currRate, diff).times(unref(conversion.currentGain)) Decimal.times(currRate, diff).times(unref(conversion.currentGain))
); );
} }
}); });
} }
function softcap(
value: DecimalSource,
cap: DecimalSource,
power: DecimalSource = 0.5
): DecimalSource {
if (Decimal.lte(value, cap)) {
return value;
} else {
return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power)));
}
}
export function addSoftcap(
scaling: ScalingFunction,
cap: ProcessedComputable<DecimalSource>,
power: ProcessedComputable<DecimalSource> = 0.5
): ScalingFunction {
return {
...scaling,
currentGain: conversion =>
softcap(scaling.currentGain(conversion), unref(cap), unref(power))
};
}
export function addHardcap(
scaling: ScalingFunction,
cap: ProcessedComputable<DecimalSource>
): ScalingFunction {
return {
...scaling,
currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap))
};
}

View file

@ -1,13 +1,15 @@
import { globalBus } from "@/game/events"; import { globalBus } from "@/game/events";
import { GenericLayer } from "@/game/layers"; import { GenericLayer } from "@/game/layers";
import Decimal, { DecimalSource } from "@/util/bignum"; import Decimal, { DecimalSource } from "@/util/bignum";
import { ProcessedComputable } from "@/util/computed"; import { DoNotCache, ProcessedComputable } from "@/util/computed";
import { ProxyState } from "@/util/proxies";
import { isArray } from "@vue/shared"; import { isArray } from "@vue/shared";
import { ComponentOptions, CSSProperties, DefineComponent, isRef, ref, Ref } from "vue"; import { CSSProperties, DefineComponent, isRef, ref, Ref } from "vue";
export const PersistentState = Symbol("PersistentState"); export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue"); export const DefaultValue = Symbol("DefaultValue");
export const Component = Symbol("Component"); export const Component = Symbol("Component");
export const GatherProps = Symbol("GatherProps");
// Note: This is a union of things that should be safely stringifiable without needing // Note: This is a union of things that should be safely stringifiable without needing
// special processes for knowing what to load them in as // special processes for knowing what to load them in as
@ -20,7 +22,8 @@ export type State =
| DecimalSource | DecimalSource
| { [key: string]: State } | { [key: string]: State }
| { [key: number]: State }; | { [key: number]: State };
export type CoercableComponent = string | ComponentOptions | DefineComponent | JSX.Element; export type JSXFunction = (() => JSX.Element) & { [DoNotCache]: true };
export type CoercableComponent = string | DefineComponent | JSXFunction;
export type StyleValue = string | CSSProperties | Array<string | CSSProperties>; export type StyleValue = string | CSSProperties | Array<string | CSSProperties>;
export type Persistent<T extends State = State> = { export type Persistent<T extends State = State> = {
@ -58,6 +61,11 @@ export enum Visibility {
None None
} }
export function jsx(func: () => JSX.Element | ""): JSXFunction {
(func as Partial<JSXFunction>)[DoNotCache] = true;
return func as JSXFunction;
}
export function showIf(condition: boolean, otherwise = Visibility.None): Visibility { export function showIf(condition: boolean, otherwise = Visibility.None): Visibility {
return condition ? Visibility.Visible : otherwise; return condition ? Visibility.Visible : otherwise;
} }
@ -101,7 +109,7 @@ export function findFeatures(obj: Record<string, unknown>, type: symbol): unknow
if (value && typeof value === "object") { if (value && typeof value === "object") {
if ((value as Record<string, unknown>).type === type) { if ((value as Record<string, unknown>).type === type) {
objects.push(value); objects.push(value);
} else { } else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>); handleObject(value as Record<string, unknown>);
} }
} }
@ -135,8 +143,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
// Load previously saved value // Load previously saved value
if (savedValue != null) { if (savedValue != null) {
(persistentState[key] as Ref<unknown>).value = savedValue; (persistentState[key] as Ref<unknown>).value = savedValue;
} else {
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
DefaultValue
];
} }
} else if (!(value instanceof Decimal)) { } else if (!(value instanceof Decimal) && !isRef(value)) {
// Continue traversing // Continue traversing
const foundPersistentInChild = handleObject(value as Record<string, unknown>, [ const foundPersistentInChild = handleObject(value as Record<string, unknown>, [
...path, ...path,
@ -146,10 +158,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
// Show warning for persistent values inside arrays // Show warning for persistent values inside arrays
// TODO handle arrays better // TODO handle arrays better
if (foundPersistentInChild) { if (foundPersistentInChild) {
if (isArray(value)) { if (isArray(value) && !isArray(obj)) {
console.warn( console.warn(
"Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.", "Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.",
obj, ProxyState in obj
? (obj as Record<PropertyKey, unknown>)[ProxyState]
: obj,
key key
); );
} else { } else {

View file

@ -2,6 +2,7 @@ import GridComponent from "@/components/features/Grid.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -20,32 +21,30 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy, Proxied } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, unref } from "vue"; import { computed, Ref, unref } from "vue";
export const GridType = Symbol("Grid"); export const GridType = Symbol("Grid");
export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T); export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T);
function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> { function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> {
return new Proxy({}, getGridHandler(grid)) as Proxied<Record<string | number, GridCell>>; return new Proxy({}, getGridHandler(grid)) as Record<string | number, GridCell>;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function getGridHandler( function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number, GridCell>> {
grid: GenericGrid
): ProxyHandler<Record<string | number, Proxied<GridCell>>> {
const keys = computed(() => { const keys = computed(() => {
const keys = []; const keys = [];
for (let row = 1; row <= grid.rows; row++) { for (let row = 1; row <= unref(grid.rows); row++) {
for (let col = 1; col <= grid.cols; col++) { for (let col = 1; col <= unref(grid.cols); col++) {
keys.push((row * 100 + col).toString()); keys.push((row * 100 + col).toString());
} }
} }
return keys; return keys;
}); });
return { return {
get(target, key) { get(target: Record<string | number, GridCell>, key: PropertyKey) {
if (key === "isProxy") { if (key === "isProxy") {
return true; return true;
} }
@ -54,29 +53,57 @@ function getGridHandler(
return (grid as never)[key]; return (grid as never)[key];
} }
if (!keys.value.includes(key.toString())) {
return undefined;
}
if (target[key] == null) { if (target[key] == null) {
target[key] = new Proxy( target[key] = new Proxy(
grid, grid,
getCellHandler(key.toString()) getCellHandler(key.toString())
) as unknown as Proxied<GridCell>; ) as unknown as GridCell;
} }
return target[key]; return target[key];
}, },
set(target, key, value) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
set(target: Record<string | number, GridCell>, key: PropertyKey, value: any) {
console.warn("Cannot set grid cells", target, key, value); console.warn("Cannot set grid cells", target, key, value);
return false; return false;
}, },
ownKeys() { ownKeys() {
return keys.value; return keys.value;
}, },
has(target, key) { has(target: Record<string | number, GridCell>, key: PropertyKey) {
return keys.value.includes(key.toString()); return keys.value.includes(key.toString());
},
getOwnPropertyDescriptor(target: Record<string | number, GridCell>, key: PropertyKey) {
if (keys.value.includes(key.toString())) {
return {
configurable: true,
enumerable: true,
writable: false
};
}
} }
}; };
} }
function getCellHandler(id: string): ProxyHandler<GenericGrid> { function getCellHandler(id: string): ProxyHandler<GenericGrid> {
const keys = [
"id",
"visibility",
"canClick",
"startState",
"state",
"style",
"classes",
"title",
"display",
"onClick",
"onHold"
];
const cache: Record<string, Ref<unknown>> = {};
return { return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target, key, receiver): any { get(target, key, receiver): any {
@ -96,10 +123,17 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
key = key.slice(0, 1).toUpperCase() + key.slice(1); key = key.slice(0, 1).toUpperCase() + key.slice(1);
if (key === "startState") {
return prop.call(receiver, id);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
prop = (target as any)[`get${key}`]; prop = (target as any)[`get${key}`];
if (isFunction(prop)) { if (isFunction(prop)) {
return prop.call(receiver, id, target.getState(id)); if (!(key in cache)) {
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
}
return cache[key].value;
} else if (prop != undefined) { } else if (prop != undefined) {
return unref(prop); return unref(prop);
} }
@ -117,17 +151,29 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean { set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean {
if ( key = `set${key.slice(0, 1).toUpperCase() + key.slice(1)}`;
`${key}Set` in target && if (key in target && isFunction(target[key]) && target[key].length < 3) {
isFunction(target[`${key}Set`]) && target[key].call(receiver, id, value);
target[`${key}Set`].length < 3
) {
target[`${key}Set`].call(receiver, id, value);
return true; return true;
} else { } else {
console.warn(`No setter for "${key}".`, target); console.warn(`No setter for "${key}".`, target);
return false; return false;
} }
},
ownKeys() {
return keys;
},
has(target, key) {
return keys.includes(key.toString());
},
getOwnPropertyDescriptor(target, key) {
if (keys.includes(key.toString())) {
return {
configurable: true,
enumerable: true,
writable: false
};
}
} }
}; };
} }
@ -169,6 +215,7 @@ export interface BaseGrid extends Persistent<Record<string | number, State>> {
cells: Record<string | number, GridCell>; cells: Record<string | number, GridCell>;
type: typeof GridType; type: typeof GridType;
[Component]: typeof GridComponent; [Component]: typeof GridComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Grid<T extends GridOptions> = Replace< export type Grid<T extends GridOptions> = Replace<
@ -196,8 +243,11 @@ export type GenericGrid = Replace<
} }
>; >;
export function createGrid<T extends GridOptions>(options: T & ThisType<Grid<T>>): Grid<T> { export function createGrid<T extends GridOptions>(
const grid: T & Partial<BaseGrid> = options; optionsFunc: () => T & ThisType<Grid<T>>
): Grid<T> {
return createLazyProxy(() => {
const grid: T & Partial<BaseGrid> = optionsFunc();
makePersistent(grid, {}); makePersistent(grid, {});
grid.id = getUniqueID("grid-"); grid.id = getUniqueID("grid-");
grid[Component] = GridComponent; grid[Component] = GridComponent;
@ -215,6 +265,8 @@ export function createGrid<T extends GridOptions>(options: T & ThisType<Grid<T>>
this[PersistentState].value[cell] = state; this[PersistentState].value[cell] = state;
}; };
grid.cells = createGridProxy(grid as GenericGrid);
processComputable(grid as T, "visibility"); processComputable(grid as T, "visibility");
setDefault(grid, "visibility", Visibility.Visible); setDefault(grid, "visibility", Visibility.Visible);
processComputable(grid as T, "rows"); processComputable(grid as T, "rows");
@ -229,7 +281,11 @@ export function createGrid<T extends GridOptions>(options: T & ThisType<Grid<T>>
processComputable(grid as T, "getTitle"); processComputable(grid as T, "getTitle");
processComputable(grid as T, "getDisplay"); processComputable(grid as T, "getDisplay");
const proxy = createProxy(grid as unknown as Grid<T>); grid[GatherProps] = function (this: GenericGrid) {
(proxy as GenericGrid).cells = createGridProxy(proxy as GenericGrid); const { visibility, rows, cols, cells, id } = this;
return proxy; return { visibility, rows, cols, cells, id };
};
return grid as unknown as Grid<T>;
});
} }

View file

@ -8,7 +8,7 @@ import {
ProcessedComputable, ProcessedComputable,
processComputable processComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { unref } from "vue"; import { unref } from "vue";
import { findFeatures, Replace, setDefault } from "./feature"; import { findFeatures, Replace, setDefault } from "./feature";
@ -41,16 +41,19 @@ export type GenericHotkey = Replace<
} }
>; >;
export function createHotkey<T extends HotkeyOptions>(options: T & ThisType<Hotkey<T>>): Hotkey<T> { export function createHotkey<T extends HotkeyOptions>(
const hotkey: T & Partial<BaseHotkey> = options; optionsFunc: () => T & ThisType<Hotkey<T>>
): Hotkey<T> {
return createLazyProxy(() => {
const hotkey: T & Partial<BaseHotkey> = optionsFunc();
hotkey.type = HotkeyType; hotkey.type = HotkeyType;
processComputable(hotkey as T, "enabled"); processComputable(hotkey as T, "enabled");
setDefault(hotkey, "enabled", true); setDefault(hotkey, "enabled", true);
processComputable(hotkey as T, "description"); processComputable(hotkey as T, "description");
const proxy = createProxy(hotkey as unknown as Hotkey<T>); return hotkey as unknown as Hotkey<T>;
return proxy; });
} }
globalBus.on("addLayer", layer => { globalBus.on("addLayer", layer => {

View file

@ -2,6 +2,7 @@ import InfoboxComponent from "@/components/features/Infobox.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -18,7 +19,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { Ref } from "vue"; import { Ref } from "vue";
export const InfoboxType = Symbol("Infobox"); export const InfoboxType = Symbol("Infobox");
@ -39,6 +40,7 @@ interface BaseInfobox extends Persistent<boolean> {
collapsed: Ref<boolean>; collapsed: Ref<boolean>;
type: typeof InfoboxType; type: typeof InfoboxType;
[Component]: typeof InfoboxComponent; [Component]: typeof InfoboxComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Infobox<T extends InfoboxOptions> = Replace< export type Infobox<T extends InfoboxOptions> = Replace<
@ -63,9 +65,10 @@ export type GenericInfobox = Replace<
>; >;
export function createInfobox<T extends InfoboxOptions>( export function createInfobox<T extends InfoboxOptions>(
options: T & ThisType<Infobox<T>> optionsFunc: () => T & ThisType<Infobox<T>>
): Infobox<T> { ): Infobox<T> {
const infobox: T & Partial<BaseInfobox> = options; return createLazyProxy(() => {
const infobox: T & Partial<BaseInfobox> = optionsFunc();
makePersistent<boolean>(infobox, false); makePersistent<boolean>(infobox, false);
infobox.id = getUniqueID("infobox-"); infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType; infobox.type = InfoboxType;
@ -83,6 +86,33 @@ export function createInfobox<T extends InfoboxOptions>(
processComputable(infobox as T, "title"); processComputable(infobox as T, "title");
processComputable(infobox as T, "display"); processComputable(infobox as T, "display");
const proxy = createProxy(infobox as unknown as Infobox<T>); infobox[GatherProps] = function (this: GenericInfobox) {
return proxy; const {
visibility,
display,
title,
color,
collapsed,
style,
titleStyle,
bodyStyle,
classes,
id
} = this;
return {
visibility,
display,
title,
color,
collapsed,
style,
titleStyle,
bodyStyle,
classes,
id
};
};
return infobox as unknown as Infobox<T>;
});
} }

View file

@ -3,6 +3,7 @@ import {
CoercableComponent, CoercableComponent,
Component, Component,
findFeatures, findFeatures,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -22,7 +23,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { Unsubscribe } from "nanoevents"; import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
@ -59,6 +60,7 @@ interface BaseMilestone extends Persistent<boolean> {
earned: Ref<boolean>; earned: Ref<boolean>;
type: typeof MilestoneType; type: typeof MilestoneType;
[Component]: typeof MilestoneComponent; [Component]: typeof MilestoneComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Milestone<T extends MilestoneOptions> = Replace< export type Milestone<T extends MilestoneOptions> = Replace<
@ -80,9 +82,10 @@ export type GenericMilestone = Replace<
>; >;
export function createMilestone<T extends MilestoneOptions>( export function createMilestone<T extends MilestoneOptions>(
options: T & ThisType<Milestone<T>> optionsFunc: () => T & ThisType<Milestone<T>>
): Milestone<T> { ): Milestone<T> {
const milestone: T & Partial<BaseMilestone> = options; return createLazyProxy(() => {
const milestone: T & Partial<BaseMilestone> = optionsFunc();
makePersistent<boolean>(milestone, false); makePersistent<boolean>(milestone, false);
milestone.id = getUniqueID("milestone-"); milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType; milestone.type = MilestoneType;
@ -93,24 +96,25 @@ export function createMilestone<T extends MilestoneOptions>(
setDefault(milestone, "visibility", Visibility.Visible); setDefault(milestone, "visibility", Visibility.Visible);
const visibility = milestone.visibility as ProcessedComputable<Visibility>; const visibility = milestone.visibility as ProcessedComputable<Visibility>;
milestone.visibility = computed(() => { milestone.visibility = computed(() => {
const display = unref((milestone as GenericMilestone).display);
switch (settings.msDisplay) { switch (settings.msDisplay) {
default: default:
case MilestoneDisplay.All: case MilestoneDisplay.All:
return unref(visibility); return unref(visibility);
case MilestoneDisplay.Configurable: case MilestoneDisplay.Configurable:
if ( if (
unref(proxy.earned) && unref(milestone.earned) &&
!( !(
proxy.display != null && display != null &&
typeof unref(proxy.display) == "object" && typeof display == "object" &&
"optionsDisplay" in (unref(proxy.display) as Record<string, unknown>) "optionsDisplay" in (display as Record<string, unknown>)
) )
) { ) {
return Visibility.None; return Visibility.None;
} }
return unref(visibility); return unref(visibility);
case MilestoneDisplay.Incomplete: case MilestoneDisplay.Incomplete:
if (unref(proxy.earned)) { if (unref(milestone.earned)) {
return Visibility.None; return Visibility.None;
} }
return unref(visibility); return unref(visibility);
@ -124,8 +128,13 @@ export function createMilestone<T extends MilestoneOptions>(
processComputable(milestone as T, "classes"); processComputable(milestone as T, "classes");
processComputable(milestone as T, "display"); processComputable(milestone as T, "display");
const proxy = createProxy(milestone as unknown as Milestone<T>); milestone[GatherProps] = function (this: GenericMilestone) {
return proxy; const { visibility, display, style, classes, earned, id } = this;
return { visibility, display, style, classes, earned, id };
};
return milestone as unknown as Milestone<T>;
});
} }
const toast = useToast(); const toast = useToast();
@ -150,14 +159,14 @@ globalBus.on("addLayer", layer => {
isCoercableComponent(display) ? display : display.requirement isCoercableComponent(display) ? display : display.requirement
); );
toast( toast(
<template> <>
<h3>Milestone earned!</h3> <h3>Milestone earned!</h3>
<div> <div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */} {/* @ts-ignore */}
<Display /> <Display />
</div> </div>
</template> </>
); );
} }
} }

View file

@ -4,14 +4,14 @@ import {
Persistent, Persistent,
persistent, persistent,
PersistentRef, PersistentRef,
Replace, PersistentState,
SetupPersistence Replace
} from "@/features/feature"; } from "@/features/feature";
import { globalBus } from "@/game/events"; import { globalBus } from "@/game/events";
import { GenericLayer } from "@/game/layers"; import { GenericLayer } from "@/game/layers";
import Decimal from "@/lib/break_eternity"; import Decimal from "@/lib/break_eternity";
import { Computable, GetComputableType, processComputable } from "@/util/computed"; import { Computable, GetComputableType, processComputable } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { Unsubscribe } from "nanoevents"; import { Unsubscribe } from "nanoevents";
import { computed, isRef, unref } from "vue"; import { computed, isRef, unref } from "vue";
@ -37,48 +37,46 @@ export type Reset<T extends ResetOptions> = Replace<
export type GenericReset = Reset<ResetOptions>; export type GenericReset = Reset<ResetOptions>;
export function createReset<T extends ResetOptions>(options: T & ThisType<Reset<T>>): Reset<T> { export function createReset<T extends ResetOptions>(
const reset: T & Partial<BaseReset> = options; optionsFunc: () => T & ThisType<Reset<T>>
): Reset<T> {
return createLazyProxy(() => {
const reset: T & Partial<BaseReset> = optionsFunc();
reset.id = getUniqueID("reset-"); reset.id = getUniqueID("reset-");
reset.type = ResetType; reset.type = ResetType;
reset.reset = function () { reset.reset = function () {
const handleObject = (obj: Record<string, unknown>) => { const handleObject = (obj: unknown) => {
Object.keys(obj).forEach(key => { if (obj && typeof obj === "object") {
const value = obj[key]; if (PersistentState in obj) {
if (value && typeof value === "object") { (obj as Persistent)[PersistentState].value = (obj as Persistent)[
if (SetupPersistence in value && isRef(value)) {
if (DefaultValue in value) {
(value as PersistentRef).value = (value as PersistentRef)[DefaultValue];
} else if (DefaultValue in obj) {
(value as PersistentRef).value = (obj as unknown as Persistent)[
DefaultValue DefaultValue
]; ];
} } else if (!(obj instanceof Decimal) && !isRef(obj)) {
} else { Object.values(obj).forEach(obj =>
handleObject(value as Record<string, unknown>); handleObject(obj as Record<string, unknown>)
);
} }
} }
});
}; };
unref(proxy.thingsToReset).forEach(handleObject); unref((reset as GenericReset).thingsToReset).forEach(handleObject);
globalBus.emit("reset", proxy); globalBus.emit("reset", reset as GenericReset);
proxy.onReset?.(); reset.onReset?.();
}; };
processComputable(reset as T, "thingsToReset"); processComputable(reset as T, "thingsToReset");
const proxy = createProxy(reset as unknown as Reset<T>); return reset as unknown as Reset<T>;
return proxy; });
} }
export function setupAutoReset( export function setupAutoReset(
layer: GenericLayer, layer: GenericLayer,
reset: GenericReset, reset: GenericReset,
autoActive: Computable<boolean> = true autoActive: Computable<boolean> = true
): void { ): Unsubscribe {
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive; const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
layer.on("update", () => { return layer.on("update", () => {
if (unref(isActive)) { if (unref(isActive)) {
reset.reset(); reset.reset();
} }

View file

@ -1,13 +1,13 @@
import { persistent, State } from "@/features/feature"; import { persistent, State } from "@/features/feature";
import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum";
import { computed, Ref, watch } from "vue"; import { computed, ComputedRef, ref, Ref, watch } from "vue";
import { globalBus } from "@/game/events"; import { globalBus } from "@/game/events";
export type Resource<T = DecimalSource> = Ref<T> & { export interface Resource<T = DecimalSource> extends Ref<T> {
displayName: string; displayName: string;
precision: number; precision: number;
small: boolean; small: boolean;
}; }
export function createResource<T extends State>( export function createResource<T extends State>(
defaultValue: T | Ref<T>, defaultValue: T | Ref<T>,
@ -22,27 +22,6 @@ export function createResource<T extends State>(
return resource as Resource<T>; return resource as Resource<T>;
} }
function softcap(value: DecimalSource, cap: DecimalSource, power: DecimalSource = 0.5): Decimal {
if (Decimal.lte(value, cap)) {
return new Decimal(value);
} else {
return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power)));
}
}
export function addSoftcap(
resource: Resource,
cap: DecimalSource,
power: DecimalSource = 0.5
): Resource {
return {
...resource,
get value() {
return softcap(resource.value, cap, power);
}
};
}
export function trackBest(resource: Resource): Ref<DecimalSource> { export function trackBest(resource: Resource): Ref<DecimalSource> {
const best = persistent(resource.value); const best = persistent(resource.value);
watch(resource, amount => { watch(resource, amount => {
@ -63,63 +42,68 @@ export function trackTotal(resource: Resource): Ref<DecimalSource> {
return total; return total;
} }
export function trackOOMPS(resource: Resource): Ref<string> { export function trackOOMPS(
let oomps: DecimalSource = 0; resource: Resource,
let oompsMag = 0; pointGain?: ComputedRef<DecimalSource>
let lastPoints: DecimalSource = 0; ): Ref<string> {
const oomps = ref<DecimalSource>(0);
const oompsMag = ref(0);
const lastPoints = ref<DecimalSource>(0);
globalBus.on("update", diff => { globalBus.on("update", diff => {
oompsMag.value = 0;
if (Decimal.lte(resource.value, 1e100)) { if (Decimal.lte(resource.value, 1e100)) {
lastPoints = resource.value; lastPoints.value = resource.value;
return; return;
} }
let curr = resource.value; let curr = resource.value;
let prev = lastPoints; let prev = lastPoints.value;
lastPoints = curr; lastPoints.value = curr;
if (Decimal.gt(curr, prev)) { if (Decimal.gt(curr, prev)) {
if (Decimal.gte(curr, "10^^8")) { if (Decimal.gte(curr, "10^^8")) {
curr = Decimal.slog(curr, 1e10); curr = Decimal.slog(curr, 1e10);
prev = Decimal.slog(prev, 1e10); prev = Decimal.slog(prev, 1e10);
oomps = curr.sub(prev).div(diff); oomps.value = curr.sub(prev).div(diff);
oompsMag = -1; oompsMag.value = -1;
} else { } else {
while ( while (
Decimal.div(curr, prev).log(10).div(diff).gte("100") && Decimal.div(curr, prev).log(10).div(diff).gte("100") &&
oompsMag <= 5 && oompsMag.value <= 5 &&
Decimal.gt(prev, 0) Decimal.gt(prev, 0)
) { ) {
curr = Decimal.log10(curr); curr = Decimal.log10(curr);
prev = Decimal.log10(prev); prev = Decimal.log10(prev);
oomps = curr.sub(prev).div(diff); oomps.value = curr.sub(prev).div(diff);
oompsMag++; oompsMag.value++;
} }
} }
} }
}); });
return computed(
() => const oompsString = computed(() => {
format(oomps) + if (oompsMag.value === 0) {
return pointGain
? format(pointGain.value, resource.precision, resource.small) +
" " +
resource.displayName +
"/s"
: "";
}
return (
format(oomps.value) +
" OOM" + " OOM" +
(oompsMag < 0 ? "^OOM" : oompsMag > 1 ? "^" + oompsMag : "") + (oompsMag.value < 0 ? "^OOM" : "^" + oompsMag.value) +
"s" "s/sec"
); );
});
return oompsString;
} }
export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string { export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string {
const amount = overrideAmount == null ? resource.value : overrideAmount; const amount = overrideAmount ?? resource.value;
if (Decimal.eq(resource.precision, 0)) { if (Decimal.eq(resource.precision, 0)) {
return formatWhole(amount); return formatWhole(amount);
} }
return format(amount, resource.precision, resource.small); return format(amount, resource.precision, resource.small);
} }
// unref may unwrap a resource too far, so this function properly unwraps it
export function unwrapResource<T extends State>(
resource: Resource<T> | Ref<Resource<T>>
): Resource<T> {
console.log(resource);
if ("displayName" in resource) {
return resource;
}
return resource.value;
}

View file

@ -1,7 +1,14 @@
import TabComponent from "@/components/features/Tab.vue"; import TabComponent from "@/components/features/Tab.vue";
import { Computable, GetComputableType } from "@/util/computed"; import { Computable, GetComputableType } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { CoercableComponent, Component, getUniqueID, Replace, StyleValue } from "./feature"; import {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
Replace,
StyleValue
} from "./feature";
export const TabType = Symbol("Tab"); export const TabType = Symbol("Tab");
@ -15,6 +22,7 @@ interface BaseTab {
id: string; id: string;
type: typeof TabType; type: typeof TabType;
[Component]: typeof TabComponent; [Component]: typeof TabComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Tab<T extends TabOptions> = Replace< export type Tab<T extends TabOptions> = Replace<
@ -28,12 +36,18 @@ export type Tab<T extends TabOptions> = Replace<
export type GenericTab = Tab<TabOptions>; export type GenericTab = Tab<TabOptions>;
export function createTab<T extends TabOptions>(options: T & ThisType<Tab<T>>): Tab<T> { export function createTab<T extends TabOptions>(optionsFunc: () => T & ThisType<Tab<T>>): Tab<T> {
const tab: T & Partial<BaseTab> = options; return createLazyProxy(() => {
const tab: T & Partial<BaseTab> = optionsFunc();
tab.id = getUniqueID("tab-"); tab.id = getUniqueID("tab-");
tab.type = TabType; tab.type = TabType;
tab[Component] = TabComponent; tab[Component] = TabComponent;
const proxy = createProxy(tab as unknown as Tab<T>); tab[GatherProps] = function (this: GenericTab) {
return proxy; const { display } = this;
return { display };
};
return tab as unknown as Tab<T>;
});
} }

View file

@ -7,15 +7,15 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
PersistentRef,
PersistentState, PersistentState,
Replace, Replace,
setDefault, setDefault,
@ -75,12 +75,14 @@ export function createTabButton<T extends TabButtonOptions>(
processComputable(tabButton as T, "style"); processComputable(tabButton as T, "style");
processComputable(tabButton as T, "glowColor"); processComputable(tabButton as T, "glowColor");
const proxy = createProxy(tabButton as unknown as TabButton<T>); return tabButton as unknown as TabButton<T>;
return proxy;
} }
export interface TabFamilyOptions { export interface TabFamilyOptions {
visibility?: Computable<Visibility>;
tabs: Computable<Record<string, GenericTabButton>>; tabs: Computable<Record<string, GenericTabButton>>;
classes?: Computable<Record<string, boolean>>;
style?: Computable<StyleValue>;
} }
interface BaseTabFamily extends Persistent<string> { interface BaseTabFamily extends Persistent<string> {
@ -89,39 +91,49 @@ interface BaseTabFamily extends Persistent<string> {
selected: Ref<string>; selected: Ref<string>;
type: typeof TabFamilyType; type: typeof TabFamilyType;
[Component]: typeof TabFamilyComponent; [Component]: typeof TabFamilyComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type TabFamily<T extends TabFamilyOptions> = Replace< export type TabFamily<T extends TabFamilyOptions> = Replace<
T & BaseTabFamily, T & BaseTabFamily,
{ {
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
tabs: GetComputableType<T["tabs"]>; tabs: GetComputableType<T["tabs"]>;
} }
>; >;
export type GenericTabFamily = TabFamily<TabFamilyOptions>; export type GenericTabFamily = Replace<
TabFamily<TabFamilyOptions>,
{
visibility: ProcessedComputable<Visibility>;
}
>;
export function createTabFamily<T extends TabFamilyOptions>( export function createTabFamily<T extends TabFamilyOptions>(
options: T & ThisType<TabFamily<T>> optionsFunc: () => T & ThisType<TabFamily<T>>
): TabFamily<T> { ): TabFamily<T> {
if (Object.keys(options.tabs).length === 0) { return createLazyProxy(() => {
console.warn("Cannot create tab family with 0 tabs", options); const tabFamily: T & Partial<BaseTabFamily> = optionsFunc();
if (Object.keys(tabFamily.tabs).length === 0) {
console.warn("Cannot create tab family with 0 tabs", tabFamily);
throw "Cannot create tab family with 0 tabs"; throw "Cannot create tab family with 0 tabs";
} }
const tabFamily: T & Partial<BaseTabFamily> = options;
tabFamily.id = getUniqueID("tabFamily-"); tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType; tabFamily.type = TabFamilyType;
tabFamily[Component] = TabFamilyComponent; tabFamily[Component] = TabFamilyComponent;
makePersistent<string>(tabFamily, Object.keys(options.tabs)[0]); makePersistent<string>(tabFamily, Object.keys(tabFamily.tabs)[0]);
tabFamily.selected = tabFamily[PersistentState]; tabFamily.selected = tabFamily[PersistentState];
tabFamily.activeTab = computed(() => { tabFamily.activeTab = computed(() => {
const tabs = unref(proxy.tabs); const tabs = unref((tabFamily as GenericTabFamily).tabs);
if ( if (
proxy[PersistentState].value in tabs && tabFamily[PersistentState].value in tabs &&
unref(tabs[proxy[PersistentState].value].visibility) === Visibility.Visible unref(tabs[(tabFamily as GenericTabFamily)[PersistentState].value].visibility) ===
Visibility.Visible
) { ) {
return unref(tabs[proxy[PersistentState].value].tab); return unref(tabs[(tabFamily as GenericTabFamily)[PersistentState].value].tab);
} }
const firstTab = Object.values(tabs).find( const firstTab = Object.values(tabs).find(
tab => unref(tab.visibility) === Visibility.Visible tab => unref(tab.visibility) === Visibility.Visible
@ -132,6 +144,16 @@ export function createTabFamily<T extends TabFamilyOptions>(
return null; return null;
}); });
const proxy = createProxy(tabFamily as unknown as TabFamily<T>); processComputable(tabFamily as T, "visibility");
return proxy; setDefault(tabFamily, "visibility", Visibility.Visible);
processComputable(tabFamily as T, "classes");
processComputable(tabFamily as T, "style");
tabFamily[GatherProps] = function (this: GenericTabFamily) {
const { visibility, activeTab, selected, tabs, style, classes } = this;
return { visibility, activeTab, selected, tabs, style, classes };
};
return tabFamily as unknown as TabFamily<T>;
});
} }

View file

@ -18,3 +18,8 @@ export interface Tooltip {
yoffset?: ProcessedComputable<string>; yoffset?: ProcessedComputable<string>;
force?: ProcessedComputable<boolean>; force?: ProcessedComputable<boolean>;
} }
export function gatherTooltipProps(tooltip: Tooltip) {
const { display, top, left, right, bottom, xoffset, yoffset, force } = tooltip;
return { display, top, left, right, bottom, xoffset, yoffset, force };
}

View file

@ -2,6 +2,7 @@ import TreeComponent from "@/components/features/tree/Tree.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
GatherProps,
getUniqueID, getUniqueID,
persistent, persistent,
Replace, Replace,
@ -20,7 +21,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, ref, Ref, unref } from "vue"; import { computed, ref, Ref, unref } from "vue";
import { Link } from "./links"; import { Link } from "./links";
import { GenericReset } from "./reset"; import { GenericReset } from "./reset";
@ -74,9 +75,10 @@ export type GenericTreeNode = Replace<
>; >;
export function createTreeNode<T extends TreeNodeOptions>( export function createTreeNode<T extends TreeNodeOptions>(
options: T & ThisType<TreeNode<T>> optionsFunc: () => T & ThisType<TreeNode<T>>
): TreeNode<T> { ): TreeNode<T> {
const treeNode: T & Partial<BaseTreeNode> = options; return createLazyProxy(() => {
const treeNode: T & Partial<BaseTreeNode> = optionsFunc();
treeNode.id = getUniqueID("treeNode-"); treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType; treeNode.type = TreeNodeType;
@ -99,8 +101,8 @@ export function createTreeNode<T extends TreeNodeOptions>(
processComputable(treeNode as T, "style"); processComputable(treeNode as T, "style");
processComputable(treeNode as T, "mark"); processComputable(treeNode as T, "mark");
const proxy = createProxy(treeNode as unknown as TreeNode<T>); return treeNode as unknown as TreeNode<T>;
return proxy; });
} }
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> { export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
@ -126,6 +128,7 @@ interface BaseTree {
resettingNode: Ref<GenericTreeNode | null>; resettingNode: Ref<GenericTreeNode | null>;
type: typeof TreeType; type: typeof TreeType;
[Component]: typeof TreeComponent; [Component]: typeof TreeComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Tree<T extends TreeOptions> = Replace< export type Tree<T extends TreeOptions> = Replace<
@ -146,8 +149,11 @@ export type GenericTree = Replace<
} }
>; >;
export function createTree<T extends TreeOptions>(options: T & ThisType<Tree<T>>): Tree<T> { export function createTree<T extends TreeOptions>(
const tree: T & Partial<BaseTree> = options; optionsFunc: () => T & ThisType<Tree<T>>
): Tree<T> {
return createLazyProxy(() => {
const tree: T & Partial<BaseTree> = optionsFunc();
tree.id = getUniqueID("tree-"); tree.id = getUniqueID("tree-");
tree.type = TreeType; tree.type = TreeType;
tree[Component] = TreeComponent; tree[Component] = TreeComponent;
@ -156,13 +162,18 @@ export function createTree<T extends TreeOptions>(options: T & ThisType<Tree<T>>
tree.resettingNode = ref(null); tree.resettingNode = ref(null);
tree.reset = function (node) { tree.reset = function (node) {
proxy.isResetting.value = true; const genericTree = tree as GenericTree;
proxy.resettingNode.value = node; genericTree.isResetting.value = true;
proxy.resetPropagation?.(proxy, node); genericTree.resettingNode.value = node;
proxy.isResetting.value = false; genericTree.resetPropagation?.(genericTree, node);
proxy.resettingNode.value = null; genericTree.onReset?.(node);
genericTree.isResetting.value = false;
genericTree.resettingNode.value = null;
}; };
tree.links = computed(() => (proxy.branches == null ? [] : unref(proxy.branches))); tree.links = computed(() => {
const genericTree = tree as GenericTree;
return unref(genericTree.branches) ?? [];
});
processComputable(tree as T, "visibility"); processComputable(tree as T, "visibility");
setDefault(tree, "visibility", Visibility.Visible); setDefault(tree, "visibility", Visibility.Visible);
@ -171,8 +182,13 @@ export function createTree<T extends TreeOptions>(options: T & ThisType<Tree<T>>
processComputable(tree as T, "rightSideNodes"); processComputable(tree as T, "rightSideNodes");
processComputable(tree as T, "branches"); processComputable(tree as T, "branches");
const proxy = createProxy(tree as unknown as Tree<T>); tree[GatherProps] = function (this: GenericTree) {
return proxy; const { nodes, leftSideNodes, rightSideNodes } = this;
return { nodes, leftSideNodes, rightSideNodes };
};
return tree as unknown as Tree<T>;
});
} }
export type ResetPropagation = { export type ResetPropagation = {
@ -213,17 +229,25 @@ export const branchedResetPropagation = function (
const nextNodes: GenericTreeNode[] = []; const nextNodes: GenericTreeNode[] = [];
currentNodes.forEach(node => { currentNodes.forEach(node => {
branches branches
.filter( .filter(branch => branch.startNode === node || branch.endNode === node)
branch => .map(branch => {
branch.startNode === node && if (branch.startNode === node) {
!visitedNodes.includes(unref(branch.endNode)) return branch.endNode;
) }
.forEach(branch => { return branch.startNode;
visitedNodes.push(branch.startNode); })
nextNodes.push(branch.endNode); .filter(node => !visitedNodes.includes(node))
.forEach(node => {
// Check here instead of in the filter because this check's results may
// change as we go through each node
if (!nextNodes.includes(node)) {
nextNodes.push(node);
node.reset?.reset();
}
}); });
}); });
currentNodes = nextNodes; currentNodes = nextNodes;
visitedNodes.push(...currentNodes);
} }
} }
}; };

View file

@ -3,6 +3,7 @@ import {
CoercableComponent, CoercableComponent,
Component, Component,
findFeatures, findFeatures,
GatherProps,
getUniqueID, getUniqueID,
makePersistent, makePersistent,
Persistent, Persistent,
@ -23,7 +24,7 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
export const UpgradeType = Symbol("Upgrade"); export const UpgradeType = Symbol("Upgrade");
@ -42,18 +43,19 @@ export interface UpgradeOptions {
>; >;
mark?: Computable<boolean | string>; mark?: Computable<boolean | string>;
cost?: Computable<DecimalSource>; cost?: Computable<DecimalSource>;
resource?: Computable<Resource>; resource?: Resource;
canPurchase?: Computable<boolean>; canAfford?: Computable<boolean>;
onPurchase?: VoidFunction; onPurchase?: VoidFunction;
} }
interface BaseUpgrade extends Persistent<boolean> { interface BaseUpgrade extends Persistent<boolean> {
id: string; id: string;
bought: Ref<boolean>; bought: Ref<boolean>;
canAfford: Ref<boolean>; canPurchase: Ref<boolean>;
purchase: VoidFunction; purchase: VoidFunction;
type: typeof UpgradeType; type: typeof UpgradeType;
[Component]: typeof UpgradeComponent; [Component]: typeof UpgradeComponent;
[GatherProps]: () => Record<string, unknown>;
} }
export type Upgrade<T extends UpgradeOptions> = Replace< export type Upgrade<T extends UpgradeOptions> = Replace<
@ -65,8 +67,7 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
display: GetComputableType<T["display"]>; display: GetComputableType<T["display"]>;
mark: GetComputableType<T["mark"]>; mark: GetComputableType<T["mark"]>;
cost: GetComputableType<T["cost"]>; cost: GetComputableType<T["cost"]>;
resource: GetComputableType<T["resource"]>; canAfford: GetComputableTypeWithDefault<T["canAfford"], Ref<boolean>>;
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
} }
>; >;
@ -79,9 +80,10 @@ export type GenericUpgrade = Replace<
>; >;
export function createUpgrade<T extends UpgradeOptions>( export function createUpgrade<T extends UpgradeOptions>(
options: T & ThisType<Upgrade<T>> optionsFunc: () => T & ThisType<Upgrade<T>>
): Upgrade<T> { ): Upgrade<T> {
const upgrade: T & Partial<BaseUpgrade> = options; return createLazyProxy(() => {
const upgrade: T & Partial<BaseUpgrade> = optionsFunc();
makePersistent<boolean>(upgrade, false); makePersistent<boolean>(upgrade, false);
upgrade.id = getUniqueID("upgrade-"); upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType; upgrade.type = UpgradeType;
@ -96,28 +98,36 @@ export function createUpgrade<T extends UpgradeOptions>(
upgrade.bought = upgrade[PersistentState]; upgrade.bought = upgrade[PersistentState];
if (upgrade.canAfford == null) { if (upgrade.canAfford == null) {
upgrade.canAfford = computed( upgrade.canAfford = computed(() => {
() => const genericUpgrade = upgrade as GenericUpgrade;
proxy.resource != null && return (
proxy.cost != null && genericUpgrade.resource != null &&
Decimal.gte(unref<Resource>(proxy.resource).value, unref(proxy.cost)) genericUpgrade.cost != null &&
Decimal.gte(genericUpgrade.resource.value, unref(genericUpgrade.cost))
); );
});
} else {
processComputable(upgrade as T, "canAfford");
} }
if (upgrade.canPurchase == null) { upgrade.canPurchase = computed(
upgrade.canPurchase = computed(() => unref(proxy.canAfford) && !unref(proxy.bought)); () =>
} unref((upgrade as GenericUpgrade).visibility) === Visibility.Visible &&
unref((upgrade as GenericUpgrade).canAfford) &&
!unref(upgrade.bought)
);
upgrade.purchase = function () { upgrade.purchase = function () {
if (!unref(proxy.canPurchase)) { const genericUpgrade = upgrade as GenericUpgrade;
if (!unref(genericUpgrade.canPurchase)) {
return; return;
} }
if (proxy.resource != null && proxy.cost != null) { if (genericUpgrade.resource != null && genericUpgrade.cost != null) {
proxy.resource.value = Decimal.sub( genericUpgrade.resource.value = Decimal.sub(
unref<Resource>(proxy.resource).value, genericUpgrade.resource.value,
unref(proxy.cost) unref(genericUpgrade.cost)
); );
} }
proxy[PersistentState].value = true; genericUpgrade[PersistentState].value = true;
proxy.onPurchase?.(); genericUpgrade.onPurchase?.();
}; };
processComputable(upgrade as T, "visibility"); processComputable(upgrade as T, "visibility");
@ -128,10 +138,38 @@ export function createUpgrade<T extends UpgradeOptions>(
processComputable(upgrade as T, "mark"); processComputable(upgrade as T, "mark");
processComputable(upgrade as T, "cost"); processComputable(upgrade as T, "cost");
processComputable(upgrade as T, "resource"); processComputable(upgrade as T, "resource");
processComputable(upgrade as T, "canPurchase");
const proxy = createProxy(upgrade as unknown as Upgrade<T>); upgrade[GatherProps] = function (this: GenericUpgrade) {
return proxy; const {
display,
visibility,
style,
classes,
resource,
cost,
canPurchase,
bought,
mark,
id,
purchase
} = this;
return {
display,
visibility,
style,
classes,
resource,
cost,
canPurchase,
bought,
mark,
id,
purchase
};
};
return upgrade as unknown as Upgrade<T>;
});
} }
export function setupAutoPurchase( export function setupAutoPurchase(

View file

@ -45,6 +45,10 @@ function update() {
diff = new Decimal(diff).max(0); diff = new Decimal(diff).max(0);
if (player.devSpeed === 0) {
return;
}
// Add offline time if any // Add offline time if any
if (player.offlineTime != undefined) { if (player.offlineTime != undefined) {
if (Decimal.gt(player.offlineTime, modInfo.offlineLimit * 3600)) { if (Decimal.gt(player.offlineTime, modInfo.offlineLimit * 3600)) {

View file

@ -15,9 +15,8 @@ import {
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "@/util/computed"; } from "@/util/computed";
import { createProxy } from "@/util/proxies"; import { createLazyProxy } from "@/util/proxies";
import { createNanoEvents, Emitter } from "nanoevents"; import { createNanoEvents, Emitter } from "nanoevents";
import { customRef, Ref } from "vue";
import { globalBus } from "./events"; import { globalBus } from "./events";
import player from "./player"; import player from "./player";
@ -33,6 +32,12 @@ export interface LayerEvents {
export const layers: Record<string, Readonly<GenericLayer> | undefined> = {}; export const layers: Record<string, Readonly<GenericLayer> | undefined> = {};
window.layers = layers; window.layers = layers;
declare module "@vue/runtime-dom" {
interface CSSProperties {
"--layer-color"?: string;
}
}
export interface Position { export interface Position {
x: number; x: number;
y: number; y: number;
@ -82,39 +87,26 @@ export type GenericLayer = Replace<
} }
>; >;
export function createLayer<T extends LayerOptions>(optionsFunc: () => T): Ref<Layer<T>> { export function createLayer<T extends LayerOptions>(optionsFunc: () => T): Layer<T> {
let layer: Layer<T> | null = null; return createLazyProxy(() => {
const layer = optionsFunc() as T & Partial<BaseLayer>;
const emitter = (layer.emitter = createNanoEvents<LayerEvents>());
layer.on = emitter.on.bind(emitter);
layer.emit = emitter.emit.bind(emitter);
return customRef(track => { layer.minimized = persistent(false);
return {
get() {
if (layer == undefined) {
const partialLayer = optionsFunc() as T & Partial<BaseLayer>;
const emitter = (partialLayer.emitter = createNanoEvents<LayerEvents>());
partialLayer.on = emitter.on.bind(emitter);
partialLayer.emit = emitter.emit.bind(emitter);
partialLayer.minimized = persistent(false); processComputable(layer as T, "color");
processComputable(layer as T, "display");
processComputable(layer as T, "name");
setDefault(layer, "name", layer.id);
processComputable(layer as T, "minWidth");
setDefault(layer, "minWidth", 600);
processComputable(layer as T, "minimizable");
setDefault(layer, "minimizable", true);
processComputable(layer as T, "links");
processComputable(partialLayer as T, "color"); return layer as unknown as Layer<T>;
processComputable(partialLayer as T, "display");
processComputable(partialLayer as T, "name");
setDefault(partialLayer, "name", partialLayer.id);
processComputable(partialLayer as T, "minWidth");
setDefault(partialLayer, "minWidth", 600);
processComputable(partialLayer as T, "minimizable");
setDefault(partialLayer, "minimizable", true);
processComputable(partialLayer as T, "links");
layer = createProxy(partialLayer as unknown as Layer<T>);
}
track();
return layer;
},
set() {
console.error("Layers are read-only!");
}
};
}); });
} }

View file

@ -3,3 +3,16 @@ import Toast from "vue-toastification";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
globalBus.on("setupVue", vue => vue.use(Toast)); globalBus.on("setupVue", vue => vue.use(Toast));
export function getNotifyStyle(color = "white", strength = "8px") {
return {
transform: "scale(1.05, 1.05)",
borderColor: "rgba(0, 0, 0, 0.125)",
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 ${strength} ${color}`,
zIndex: 1
};
}
export function getHighNotifyStyle() {
return getNotifyStyle("red", "20px");
}

View file

@ -1,7 +1,7 @@
import Decimal, { DecimalSource } from "@/util/bignum"; import Decimal, { DecimalSource } from "@/util/bignum";
import { isPlainObject } from "@/util/common"; import { isPlainObject } from "@/util/common";
import { ProxiedWithState, ProxyPath, ProxyState } from "@/util/proxies"; import { ProxiedWithState, ProxyPath, ProxyState } from "@/util/proxies";
import { shallowReactive, unref } from "vue"; import { reactive, unref } from "vue";
import transientState from "./state"; import transientState from "./state";
export interface PlayerData { export interface PlayerData {
@ -15,7 +15,6 @@ export interface PlayerData {
offlineTime: DecimalSource | null; offlineTime: DecimalSource | null;
timePlayed: DecimalSource; timePlayed: DecimalSource;
keepGoing: boolean; keepGoing: boolean;
minimized: Record<string, boolean>;
modID: string; modID: string;
modVersion: string; modVersion: string;
layers: Record<string, Record<string, unknown>>; layers: Record<string, Record<string, unknown>>;
@ -23,7 +22,7 @@ export interface PlayerData {
export type Player = ProxiedWithState<PlayerData>; export type Player = ProxiedWithState<PlayerData>;
const state = shallowReactive<PlayerData>({ const state = reactive<PlayerData>({
id: "", id: "",
devSpeed: null, devSpeed: null,
name: "", name: "",
@ -34,14 +33,13 @@ const state = shallowReactive<PlayerData>({
offlineTime: null, offlineTime: null,
timePlayed: new Decimal(0), timePlayed: new Decimal(0),
keepGoing: false, keepGoing: false,
minimized: {},
modID: "", modID: "",
modVersion: "", modVersion: "",
layers: {} layers: {}
}); });
export function stringifySave(player: PlayerData): string { export function stringifySave(player: PlayerData): string {
return JSON.stringify((player as Player)[ProxyState], (key, value) => unref(value)); return JSON.stringify(player, (key, value) => unref(value));
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -53,7 +51,7 @@ const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
} }
const value = target[ProxyState][key]; const value = target[ProxyState][key];
if (isPlainObject(value) && !(value instanceof Decimal)) { if (key !== "value" && isPlainObject(value) && !(value instanceof Decimal)) {
if (value !== target[key]?.[ProxyState]) { if (value !== target[key]?.[ProxyState]) {
const path = [...target[ProxyPath], key]; const path = [...target[ProxyPath], key];
target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler); target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler);
@ -109,9 +107,12 @@ const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
has(target: Record<PropertyKey, any>, key: string) { has(target: Record<PropertyKey, any>, key: string) {
return Reflect.has(target[ProxyState], key); return Reflect.has(target[ProxyState], key);
},
getOwnPropertyDescriptor(target, key) {
return Object.getOwnPropertyDescriptor(target[ProxyState], key);
} }
}; };
export default window.player = new Proxy( export default window.player = new Proxy(
{ [ProxyState]: state, [ProxyPath]: ["player"] }, { [ProxyState]: state, [ProxyPath]: ["player"] },
playerHandler playerHandler
) as PlayerData; ) as Player;

View file

@ -2,7 +2,7 @@ import modInfo from "@/data/modInfo.json";
import { Themes } from "@/data/themes"; import { Themes } from "@/data/themes";
import { globalBus } from "@/game/events"; import { globalBus } from "@/game/events";
import { hardReset } from "@/util/save"; import { hardReset } from "@/util/save";
import { shallowReactive, watch } from "vue"; import { reactive, watch } from "vue";
export interface Settings { export interface Settings {
active: string; active: string;
@ -12,7 +12,7 @@ export interface Settings {
unthrottled: boolean; unthrottled: boolean;
} }
const state = shallowReactive<Partial<Settings>>({ const state = reactive<Partial<Settings>>({
active: "", active: "",
saves: [], saves: [],
showTPS: true, showTPS: true,
@ -21,7 +21,7 @@ const state = shallowReactive<Partial<Settings>>({
}); });
watch( watch(
() => state, state,
state => state =>
localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(state))))), localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(state))))),
{ deep: true } { deep: true }

View file

@ -28,10 +28,16 @@ declare global {
toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string; toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string;
formatSmall: (x: DecimalSource, precision?: number) => string; formatSmall: (x: DecimalSource, precision?: number) => string;
invertOOM: (x: DecimalSource) => Decimal; invertOOM: (x: DecimalSource) => Decimal;
modInfo: typeof modInfo;
} }
} }
requestAnimationFrame(async () => { requestAnimationFrame(async () => {
console.log(
"%cMade in TMT-X%c\nLearn more at www.moddingtree.com",
"font-weight: bold; font-size: 24px; color: #A3BE8C; background: #2E3440; padding: 4px 8px; border-radius: 8px;",
"padding: 4px;"
);
await load(); await load();
const { globalBus, startGameLoop } = await require("./game/events"); const { globalBus, startGameLoop } = await require("./game/events");
@ -45,3 +51,5 @@ requestAnimationFrame(async () => {
startGameLoop(); startGameLoop();
}); });
window.modInfo = modInfo;

View file

@ -1,9 +1,13 @@
import { computed, Ref } from "vue"; import { computed, Ref } from "vue";
import { isFunction } from "./common"; import { isFunction } from "./common";
export const DoNotCache = Symbol("DoNotCache");
export type Computable<T> = T | Ref<T> | (() => T); export type Computable<T> = T | Ref<T> | (() => T);
export type ProcessedComputable<T> = T | Ref<T>; export type ProcessedComputable<T> = T | Ref<T>;
export type GetComputableType<T> = T extends () => infer S export type GetComputableType<T> = T extends { [DoNotCache]: true }
? T
: T extends () => infer S
? Ref<S> ? Ref<S>
: undefined extends T : undefined extends T
? undefined ? undefined
@ -27,7 +31,8 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
key: S key: S
): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } { ): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } {
const computable = obj[key]; const computable = obj[key];
if (isFunction(computable) && computable.length === 0) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isFunction(computable) && computable.length === 0 && !(computable as any)[DoNotCache]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
obj[key] = computed(computable.bind(obj)); obj[key] = computed(computable.bind(obj));
@ -35,8 +40,11 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
} }
export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> { export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> {
if (isFunction(obj)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isFunction(obj) && !(obj as any)[DoNotCache]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
obj = computed(obj); obj = computed(obj);
} }
return obj; return obj as ProcessedComputable<T>;
} }

View file

@ -1,15 +1,8 @@
import { isRef } from "vue";
import Decimal from "./bignum"; import Decimal from "./bignum";
import { isFunction, isPlainObject } from "./common";
export const ProxyState = Symbol("ProxyState"); export const ProxyState = Symbol("ProxyState");
export const ProxyPath = Symbol("ProxyPath"); export const ProxyPath = Symbol("ProxyPath");
export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown>
? {
[K in keyof T]: Proxied<T[K]>;
}
: T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any> export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any>
? NonNullable<T> extends Decimal ? NonNullable<T> extends Decimal
@ -22,38 +15,46 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
} }
: T; : T;
export function createProxy<T extends Record<string, unknown>>(object: T): T { // Takes a function that returns an object and pretends to be that object
if (object.isProxy) { // Note that the object is lazily calculated
console.warn( export function createLazyProxy<T extends object>(objectFunc: () => T): T {
"Creating a proxy out of a proxy! This may cause unintentional function calls and stack overflows." const obj: T | Record<string, never> = {};
); let calculated = false;
function calculateObj(): T {
if (!calculated) {
Object.assign(obj, objectFunc());
calculated = true;
} }
//return new Proxy(object, layerHandler) as T; return obj as T;
return object;
} }
return new Proxy(obj, {
get(target, key) {
if (key === ProxyState) {
return calculateObj();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const layerHandler: ProxyHandler<Record<PropertyKey, any>> = { return (calculateObj() as any)[key];
get(target, key, receiver: typeof Proxy) { },
if (key === "isProxy") { set() {
console.error("Layers and features are shallow readonly");
return false;
},
has(target, key) {
if (key === ProxyState) {
return true; return true;
} }
return Reflect.has(calculateObj(), key);
if ( },
target[key] == null || ownKeys() {
isRef(target[key]) || return Reflect.ownKeys(calculateObj());
target[key].isProxy || },
target[key] instanceof Decimal || getOwnPropertyDescriptor(target, key) {
typeof key === "symbol" || if (!calculated) {
typeof target[key].render === "function" Object.assign(obj, objectFunc());
) { calculated = true;
return target[key];
} else if (isPlainObject(target[key]) || Array.isArray(target[key])) {
target[key] = new Proxy(target[key], layerHandler);
return target[key];
} else if (isFunction(target[key])) {
return target[key].bind(receiver);
} }
return target[key]; return Object.getOwnPropertyDescriptor(target, key);
}
}) as T;
} }
};

View file

@ -2,6 +2,7 @@ import modInfo from "@/data/modInfo.json";
import player, { Player, PlayerData, stringifySave } from "@/game/player"; import player, { Player, PlayerData, stringifySave } from "@/game/player";
import settings, { loadSettings } from "@/game/settings"; import settings, { loadSettings } from "@/game/settings";
import Decimal from "./bignum"; import Decimal from "./bignum";
import { ProxyState } from "./proxies";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player { export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
return Object.assign( return Object.assign(
@ -24,7 +25,7 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
} }
export function save(): string { export function save(): string {
const stringifiedSave = btoa(unescape(encodeURIComponent(stringifySave(player)))); const stringifiedSave = btoa(unescape(encodeURIComponent(stringifySave(player[ProxyState]))));
localStorage.setItem(player.id, stringifiedSave); localStorage.setItem(player.id, stringifiedSave);
return stringifiedSave; return stringifiedSave;
} }
@ -55,7 +56,6 @@ export async function load(): Promise<void> {
export function newSave(): PlayerData { export function newSave(): PlayerData {
const id = getUniqueID(); const id = getUniqueID();
const player = setupInitialStore({ id }); const player = setupInitialStore({ id });
console.log(player);
localStorage.setItem(id, btoa(unescape(encodeURIComponent(stringifySave(player))))); localStorage.setItem(id, btoa(unescape(encodeURIComponent(stringifySave(player)))));
settings.saves.push(id); settings.saves.push(id);

View file

@ -3,26 +3,36 @@ import Row from "@/components/system/Row.vue";
import { import {
CoercableComponent, CoercableComponent,
Component as ComponentKey, Component as ComponentKey,
GenericComponent GatherProps,
GenericComponent,
JSXFunction
} from "@/features/feature"; } from "@/features/feature";
import { isArray } from "@vue/shared";
import { import {
Component, Component,
computed, computed,
ComputedRef, ComputedRef,
DefineComponent, DefineComponent,
defineComponent, defineComponent,
h, isRef,
PropType, PropType,
ref, ref,
Ref, Ref,
ShallowRef,
shallowRef,
unref, unref,
WritableComputedRef watchEffect
} from "vue"; } from "vue";
import { ProcessedComputable } from "./computed"; import { DoNotCache, ProcessedComputable } from "./computed";
export function coerceComponent(component: CoercableComponent, defaultWrapper = "span"): Component { export function coerceComponent(
component: CoercableComponent,
defaultWrapper = "span"
): DefineComponent {
if (typeof component === "function") {
return defineComponent({ render: component });
}
if (typeof component === "string") { if (typeof component === "string") {
if (component.length > 0) {
component = component.trim(); component = component.trim();
if (component.charAt(0) !== "<") { if (component.charAt(0) !== "<") {
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`; component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
@ -30,46 +40,33 @@ export function coerceComponent(component: CoercableComponent, defaultWrapper =
return defineComponent({ template: component }); return defineComponent({ template: component });
} }
return defineComponent({ render: () => ({}) });
}
return component; return component;
} }
export function render(object: { [ComponentKey]: GenericComponent }): DefineComponent { export type VueFeature = {
return defineComponent({ [ComponentKey]: GenericComponent;
render() { [GatherProps]: () => Record<string, unknown>;
const component = object[ComponentKey]; };
return h(component, object);
export function render(object: VueFeature | CoercableComponent): JSX.Element | DefineComponent {
if (isCoercableComponent(object)) {
if (typeof object === "function") {
return (object as JSXFunction)();
} }
}); return coerceComponent(object);
}
const Component = object[ComponentKey];
return <Component {...object[GatherProps]()} />;
} }
export function renderRow( export function renderRow(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
objects: { [ComponentKey]: GenericComponent }[], return <Row>{objects.map(obj => render(obj))}</Row>;
props: Record<string, unknown> | null = null
): DefineComponent {
return defineComponent({
render() {
return h(
Row as DefineComponent,
props,
objects.map(obj => h(obj[ComponentKey], obj))
);
}
});
} }
export function renderCol( export function renderCol(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
objects: { [ComponentKey]: GenericComponent }[], return <Col>{objects.map(obj => render(obj))}</Col>;
props: Record<string, unknown> | null = null
): DefineComponent {
return defineComponent({
render() {
return h(
Col as DefineComponent,
props,
objects.map(obj => h(obj[ComponentKey], obj))
);
}
});
} }
export function isCoercableComponent(component: unknown): component is CoercableComponent { export function isCoercableComponent(component: unknown): component is CoercableComponent {
@ -80,6 +77,9 @@ export function isCoercableComponent(component: unknown): component is Coercable
return false; return false;
} }
return "render" in component || "component" in component; return "render" in component || "component" in component;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} else if (typeof component === "function" && (component as any)[DoNotCache] === true) {
return true;
} }
return false; return false;
} }
@ -117,23 +117,25 @@ export function setupHoldToClick(
} }
export function computeComponent( export function computeComponent(
component: Ref<ProcessedComputable<CoercableComponent>> component: Ref<ProcessedComputable<CoercableComponent>>,
): ComputedRef<Component> { defaultWrapper = "div"
return computed(() => { ): ShallowRef<Component | JSXFunction | ""> {
return coerceComponent(unref(unref<ProcessedComputable<CoercableComponent>>(component))); const comp = shallowRef<Component | JSXFunction | "">();
watchEffect(() => {
comp.value = coerceComponent(unwrapRef(component), defaultWrapper);
}); });
return comp as ShallowRef<Component | JSXFunction | "">;
} }
export function computeOptionalComponent( export function computeOptionalComponent(
component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined> component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined>,
): ComputedRef<Component | undefined> { defaultWrapper = "div"
return computed(() => { ): ShallowRef<Component | JSXFunction | "" | null> {
let currComponent = unref<ProcessedComputable<CoercableComponent | undefined> | undefined>( const comp = shallowRef<Component | JSXFunction | "" | null>(null);
component watchEffect(() => {
); const currComponent = unwrapRef(component);
if (currComponent == null) return; comp.value = currComponent == null ? null : coerceComponent(currComponent, defaultWrapper);
currComponent = unref(currComponent);
return currComponent == null ? undefined : coerceComponent(currComponent);
}); });
return comp;
} }
export function wrapRef<T>(ref: Ref<ProcessedComputable<T>>): ComputedRef<T> { export function wrapRef<T>(ref: Ref<ProcessedComputable<T>>): ComputedRef<T> {
@ -141,7 +143,15 @@ export function wrapRef<T>(ref: Ref<ProcessedComputable<T>>): ComputedRef<T> {
} }
export function unwrapRef<T>(ref: Ref<ProcessedComputable<T>>): T { export function unwrapRef<T>(ref: Ref<ProcessedComputable<T>>): T {
return unref(unref<ProcessedComputable<T>>(ref)); return unref<T>(unref(ref));
}
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
if (isRef(ref.value)) {
ref.value.value = value;
} else {
ref.value = value;
}
} }
type PropTypes = type PropTypes =