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 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">
<title><%= htmlWebpackPlugin.options.title %></title>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,26 +1,26 @@
<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
v-for="(node, nodeIndex) in row"
:key="nodeIndex"
v-bind="node"
v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip"
/>
</span>
<span class="left-side-nodes" v-if="leftSideNodes">
<span class="left-side-nodes" v-if="unref(leftSideNodes)">
<TreeNode
v-for="(node, nodeIndex) in leftSideNodes"
v-for="(node, nodeIndex) in unref(leftSideNodes)"
:key="nodeIndex"
v-bind="node"
v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip"
small
/>
</span>
<span class="side-nodes" v-if="rightSideNodes">
<span class="side-nodes" v-if="unref(rightSideNodes)">
<TreeNode
v-for="(node, nodeIndex) in rightSideNodes"
v-for="(node, nodeIndex) in unref(rightSideNodes)"
:key="nodeIndex"
v-bind="node"
v-bind="gatherNodeProps(node)"
:force-tooltip="node.forceTooltip"
small
/>
@ -28,21 +28,60 @@
</template>
<script lang="ts">
import "@/components/common/table.css";
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";
export default defineComponent({
props: {
nodes: {
type: Array as PropType<GenericTreeNode[][]>,
type: processedPropType<GenericTreeNode[][]>(Array),
required: true
},
leftSideNodes: Array as PropType<GenericTreeNode[]>,
rightSideNodes: Array as PropType<GenericTreeNode[]>
leftSideNodes: processedPropType<GenericTreeNode[]>(Array),
rightSideNodes: processedPropType<GenericTreeNode[]>(Array)
},
components: { TreeNode },
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>

View file

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

View file

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

View file

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

View file

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

View file

@ -8,22 +8,22 @@
<script setup lang="ts">
import { CoercableComponent } from "@/features/feature";
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;
modelValue?: boolean;
}>();
const props = toRefs(_props);
const emit = defineEmits<{
(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({
get() {
return !!unref(props.modelValue);
return !!props.modelValue;
},
set(value: boolean) {
emit("update:modelValue", value);

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
<script setup lang="ts">
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 = toRefs(_props);
@ -24,6 +24,8 @@ if (register && unregister) {
register(newID, newNode);
}
});
onUnmounted(() => unregister(unref(props.id)));
}
</script>

View file

@ -19,11 +19,11 @@ import {
RegisterLinkNodeInjectionKey,
UnregisterLinkNodeInjectionKey
} 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";
const _props = defineProps<{ links: Link[] }>();
const { links } = toRefs(_props);
const _props = defineProps<{ links?: Link[] }>();
const links = toRef(_props, "links");
const observer = new MutationObserver(updateNodes);
const resizeObserver = new ResizeObserver(updateBounds);
@ -42,16 +42,17 @@ onMounted(() => {
updateNodes();
});
const validLinks = computed(() =>
links.value.filter(link => {
const n = nodes.value;
return (
n[link.startNode.id]?.x != undefined &&
n[link.startNode.id]?.y != undefined &&
n[link.endNode.id]?.x != undefined &&
n[link.endNode.id]?.y != undefined
);
})
const validLinks = computed(
() =>
links.value?.filter(link => {
const n = nodes.value;
return (
n[link.startNode.id]?.x != undefined &&
n[link.startNode.id]?.y != undefined &&
n[link.endNode.id]?.x != undefined &&
n[link.endNode.id]?.y != undefined
);
}) ?? []
);
const observerOptions = {

View file

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

View file

@ -3,7 +3,7 @@
<img v-if="banner" :src="banner" height="100%" :alt="title" />
<div v-else class="title">{{ title }}</div>
<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
>
</div>

View file

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

View file

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

View file

@ -1,9 +1,16 @@
<template>
<div class="table">
<div class="row">
<div class="row" :class="{ mergeAdjacent }">
<slot />
</div>
</div>
</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">
<Text v-model="newName" class="editname" @submit="changeName" />
</div>
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
<div v-else class="details error">Error: Failed to load save with id {{ save.id }}<br/>{{ save.error }}</div>
</div>
</template>
@ -88,7 +88,7 @@ const isEditing = ref(false);
const isConfirming = ref(false);
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 currentTime = computed(() =>

View file

@ -37,7 +37,7 @@
<Select
v-if="Object.keys(bank).length > 0"
:options="bank"
:modelValue="[]"
:modelValue="undefined"
@update:modelValue="preset => newFromPreset(preset as string)"
closeOnSelect
placeholder="Select preset"
@ -61,7 +61,15 @@ import Modal from "@/components/system/Modal.vue";
import player, { PlayerData } from "@/game/player";
import settings from "@/game/settings";
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 Text from "../fields/Text.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) {
if (!(id in cachedSaves)) {
if (cachedSaves[id] == null) {
const save = localStorage.getItem(id);
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 {
try {
cachedSaves[id] = JSON.parse(decodeURIComponent(escape(atob(save))));
cachedSaves[id].id = id;
cachedSaves[id] = { ...JSON.parse(decodeURIComponent(escape(atob(save)))), id };
} catch (error) {
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
watch(isOpen, isOpen => {
@ -187,6 +200,7 @@ function duplicateSave(id: string) {
function deleteSave(id: string) {
settings.saves = settings.saves.filter((save: string) => save !== id);
localStorage.removeItem(id);
cachedSaves[id] = undefined;
}
function openSave(id: string) {
@ -195,6 +209,8 @@ function openSave(id: string) {
save();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadSave(saves.value[id]!);
// Delete cached version in case of opening it again
cachedSaves[id] = undefined;
}
function newFromPreset(preset: string) {
@ -217,6 +233,7 @@ function editSave(id: string, newName: string) {
save();
} else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave)))));
cachedSaves[id] = undefined;
}
}
}

View file

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

View file

@ -1,11 +1,16 @@
<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>
<script setup lang="ts">
import state from "@/game/state";
import Decimal from "@/util/bignum";
import { computed } from "vue";
import Decimal, { DecimalSource, formatWhole } from "@/util/bignum";
import { computed, ref, watchEffect } from "vue";
const tps = computed(() =>
Decimal.div(
@ -13,6 +18,20 @@ const tps = computed(() =>
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>
<style scoped>
@ -22,4 +41,12 @@ const tps = computed(() =>
bottom: 10px;
z-index: 100;
}
.low {
color: var(--danger);
}
.fade-leave-to {
opacity: 0;
}
</style>

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import {
GenericClickable
} from "@/features/clickable";
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 {
createTreeNode,
@ -56,55 +56,61 @@ export type GenericResetButton = Replace<
>;
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
options: T
optionsFunc: () => T
): ResetButton<T> {
setDefault(options, "showNextAt", true);
if (options.resetDescription == null) {
options.resetDescription = computed(() =>
Decimal.lt(proxy.conversion.gainResource.value, 1e3) ? "Reset for " : ""
);
}
if (options.display == null) {
options.display = computed(() => {
const nextAt = unref(proxy.showNextAt) && (
<template>
<br />
<br />
Next:{" "}
{displayResource(
proxy.conversion.baseResource,
unref(proxy.conversion.nextAt)
)}{" "}
{proxy.conversion.baseResource.displayName}
</template>
return createClickable(() => {
const resetButton = optionsFunc();
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 " : ""
);
return (
} else {
processComputable(resetButton as T, "resetDescription");
}
if (resetButton.display == null) {
resetButton.display = jsx(() => (
<span>
{proxy.resetDescription}
{unref(resetButton.resetDescription as ProcessedComputable<string>)}
<b>
{displayResource(
proxy.conversion.gainResource,
unref(proxy.conversion.currentGain)
resetButton.conversion.gainResource,
unref(resetButton.conversion.currentGain)
)}
</b>
{proxy.conversion.gainResource.displayName}
{nextAt}
</b>{" "}
{resetButton.conversion.gainResource.displayName}
<div v-show={unref(resetButton.showNextAt)}>
<br />
Next:{" "}
{displayResource(
resetButton.conversion.baseResource,
unref(resetButton.conversion.nextAt)
)}{" "}
{resetButton.conversion.baseResource.displayName}
</div>
</span>
);
});
}
if (options.canClick == null) {
options.canClick = computed(() => Decimal.gt(unref(proxy.conversion.currentGain), 0));
}
const onClick = options.onClick;
options.onClick = function () {
proxy.conversion.convert();
proxy.tree.reset(proxy.treeNode);
onClick?.();
};
));
}
const proxy = createClickable(options) as unknown as ResetButton<T>;
return proxy;
if (resetButton.canClick == null) {
resetButton.canClick = computed(() =>
Decimal.gt(unref(resetButton.conversion.currentGain), 0)
);
}
const onClick = resetButton.onClick;
resetButton.onClick = function () {
resetButton.conversion.convert();
resetButton.tree.reset(resetButton.treeNode);
onClick?.();
};
return resetButton;
}) as unknown as ResetButton<T>;
}
export interface LayerTreeNodeOptions extends TreeNodeOptions {
@ -120,27 +126,28 @@ export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
>;
export type GenericLayerTreeNode = LayerTreeNode<LayerTreeNodeOptions>;
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(options: T): LayerTreeNode<T> {
processComputable(options as T, "append");
return createTreeNode({
...options,
display: options.layerID,
onClick:
options.append != null && options.append
? function () {
if (player.tabs.includes(options.layerID)) {
const index = player.tabs.lastIndexOf(options.layerID);
player.tabs = [
...player.tabs.slice(0, index),
...player.tabs.slice(index + 1)
];
} else {
player.tabs = [...player.tabs, options.layerID];
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
optionsFunc: () => T
): LayerTreeNode<T> {
return createTreeNode(() => {
const options = optionsFunc();
processComputable(options as T, "append");
return {
...options,
display: options.layerID,
onClick:
options.append != null && options.append
? function () {
if (player.tabs.includes(options.layerID)) {
const index = player.tabs.lastIndexOf(options.layerID);
player.tabs.splice(index, 1);
} else {
player.tabs.push(options.layerID);
}
}
}
: function () {
player.tabs.splice(1, 1, options.layerID);
}
: function () {
player.tabs.splice(1, 1, options.layerID);
}
};
}) 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 { main } from "@/data/mod";
import { createAchievement } from "@/features/achievement";
import { jsx } from "@/features/feature";
import { createGrid } from "@/features/grid";
import { createResource } from "@/features/resource";
import { createTreeNode } from "@/features/tree";
@ -8,6 +10,7 @@ import { createLayer } from "@/game/layers";
import { DecimalSource } from "@/lib/break_eternity";
import Decimal from "@/util/bignum";
import { render, renderRow } from "@/util/vue";
import { computed } from "vue";
import f from "./f";
const layer = createLayer(() => {
@ -16,59 +19,64 @@ const layer = createLayer(() => {
const name = "Achievements";
const points = createResource<DecimalSource>(0, "achievement power");
const treeNode = createTreeNode({
tooltip: "Achievements",
const treeNode = createTreeNode(() => ({
display: "A",
color,
tooltip: {
display: "Achievements",
right: true
},
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",
display: "Get me!",
tooltip() {
if (this.earned.value) {
tooltip: computed(() => {
if (ach1.earned.value) {
return "You did it!";
}
return "How did this happen?";
},
}),
shouldEarn: true
});
const ach2 = createAchievement({
}));
const ach2 = createAchievement(() => ({
display: "Impossible!",
tooltip() {
if (this.earned.value) {
tooltip: computed(() => {
if (ach2.earned.value) {
return "HOW????";
}
return "Mwahahaha!";
},
}),
style: { color: "#04e050" }
});
const ach3 = createAchievement({
}));
const ach3 = createAchievement(() => ({
display: "EIEIO",
tooltip:
"Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).",
shouldEarn: function () {
return Decimal.gte(f.value.points.value, 1);
return Decimal.gte(f.points.value, 1);
},
onComplete() {
console.log("Bork bork bork!");
}
});
}));
const achievements = [ach1, ach2, ach3];
const grid = createGrid({
const grid = createGrid(() => ({
rows: 2,
cols: 2,
getStartState(id) {
return id;
},
getStyle(id) {
return { backgroundColor: `#${(Number(id) * 1234) % 999999}` };
getStyle(id, state) {
return { backgroundColor: `#${(Number(state) * 1234) % 999999}` };
},
// TODO display should return an object
getTitle(id) {
let direction;
let direction = "";
if (id === "101") {
direction = "top";
} else if (id === "102") {
@ -78,29 +86,39 @@ const layer = createLayer(() => {
} else if (id === "202") {
direction = "right";
}
return (
<Tooltip display={JSON.stringify(this.cells[id].style)} {...{ direction }}>
return jsx(() => (
<Tooltip display={JSON.stringify(this.cells[id].style)} {...{ [direction]: true }}>
<h3>Gridable #{id}</h3>
</Tooltip>
);
));
},
getDisplay(id) {
return String(id);
getDisplay(id, state) {
return String(state);
},
getCanClick(): boolean {
return Decimal.eq(main.value.points.value, 10);
getCanClick() {
return Decimal.eq(main.points.value, 10);
},
onClick(id, state) {
this.cells[id].state = Number(state) + 1;
}
});
}));
const display = (
<template>
{renderRow(achievements)}
{render(grid)}
</template>
);
const display = jsx(() => (
<>
<Row>
<Tooltip display={ach1.tooltip} bottom>
{render(ach1)}
</Tooltip>
<Tooltip display={ach2.tooltip} bottom>
{render(ach2)}
</Tooltip>
<Tooltip display={ach3.tooltip} bottom>
{render(ach3)}
</Tooltip>
</Row>
{renderRow(grid)}
</>
));
return {
id,

View file

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

View file

@ -1,53 +1,60 @@
import Modal from "@/components/system/Modal.vue";
import Spacer from "@/components/system/Spacer.vue";
import { jsx } from "@/features/feature";
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 { createLayer, GenericLayer } from "@/game/layers";
import player, { PlayerData } from "@/game/player";
import { DecimalSource } from "@/lib/break_eternity";
import Decimal, { format, formatSmall, formatTime } from "@/util/bignum";
import Decimal, { format, formatTime } from "@/util/bignum";
import { render } from "@/util/vue";
import { computed, ref } from "vue";
import { computed, ref, toRaw } from "vue";
import a from "./layers/aca/a";
import c from "./layers/aca/c";
import f from "./layers/aca/f";
export const main = createLayer(() => {
const points = createResource<DecimalSource>(0);
const points = createResource<DecimalSource>(10);
const best = trackBest(points);
const total = trackTotal(points);
const oomps = trackOOMPS(points);
const showModal = ref(false);
const showAchievements = ref(false);
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);
if (c.value.lollipopMultiplierUpgrade.bought)
gain = gain.times(c.value.lollipopMultiplierEffect.value);
if (c.lollipopMultiplierUpgrade.bought.value)
gain = gain.times(c.lollipopMultiplierEffect.value);
return gain;
});
globalBus.on("update", 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
const tree = createTree({
nodes: [[c.value.treeNode], [f.value.treeNode, c.value.spook]],
leftSideNodes: [a.value.treeNode, c.value.h],
const tree = createTree(() => ({
nodes: [[c.treeNode], [f.treeNode, c.spook]],
leftSideNodes: [a.treeNode, c.h],
branches: [
{
startNode: f.value.treeNode,
endNode: c.value.treeNode,
startNode: f.treeNode,
endNode: c.treeNode,
stroke: "blue",
"stroke-width": "25px",
style: {
filter: "blur(5px)"
}
},
{ startNode: c.value.treeNode, endNode: c.value.g }
]
}) as GenericTree;
{ startNode: c.treeNode, endNode: c.g }
],
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,
// 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",
name: "Tree",
links: tree.links,
display: (
<template>
display: jsx(() => (
<>
<div v-show={player.devSpeed === 0}>Game Paused</div>
<div v-show={player.devSpeed && player.devSpeed !== 1}>
Dev Speed: {format(player.devSpeed || 0)}x
@ -70,37 +77,33 @@ export const main = createLayer(() => {
<h2>{format(points.value)}</h2>
<span v-show={Decimal.lt(points.value, "1e1e6")}> points</span>
</div>
<div v-show={Decimal.gt(pointGain.value, 0)}>
({oomps.value === "" ? formatSmall(pointGain.value) : oomps.value}/sec)
</div>
<div v-show={Decimal.gt(pointGain.value, 0)}>({oomps.value})</div>
<Spacer />
<button onClick={() => (showAchievements.value = true)}>open achievements</button>
<Modal
modelValue={showModal.value}
onUpdate:modelValue={value => (showModal.value = value)}
>
<svg style="height: 80vmin; width: 80vmin;">
<path d="M 32 222 Q 128 222, 128 0 Q 128 222, 224 222 L 224 224 L 32 224" />
<circle cx="64" cy="128" r="64" fill="#8da8b0" />
<circle cx="128" cy="64" r="64" fill="#71368a" />
<circle cx="192" cy="128" r="64" fill="#fa8508" />
</svg>
</Modal>
modelValue={showAchievements.value}
onUpdate:modelValue={value => (showAchievements.value = value)}
v-slots={{
header: () => <h2>Achievements</h2>,
body: a.display
}}
/>
{render(tree)}
</template>
),
</>
)),
points,
best,
total,
oomps,
tree
tree,
showAchievements
};
});
export const getInitialLayers = (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
player: Partial<PlayerData>
): Array<GenericLayer> => [main.value, f.value, c.value, a.value];
): Array<GenericLayer> => [main, f, c, a];
export const hasWon = computed(() => {
return false;

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import BarComponent from "@/components/features/Bar.vue";
import {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
Replace,
setDefault,
@ -16,7 +17,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
export const BarType = Symbol("Bar");
@ -48,6 +49,7 @@ interface BaseBar {
id: string;
type: typeof BarType;
[Component]: typeof BarComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Bar<T extends BarOptions> = Replace<
@ -76,27 +78,63 @@ export type GenericBar = Replace<
}
>;
export function createBar<T extends BarOptions>(options: T & ThisType<Bar<T>>): Bar<T> {
const bar: T & Partial<BaseBar> = options;
bar.id = getUniqueID("bar-");
bar.type = BarType;
bar[Component] = BarComponent;
export function createBar<T extends BarOptions>(optionsFunc: () => T & ThisType<Bar<T>>): Bar<T> {
return createLazyProxy(() => {
const bar: T & Partial<BaseBar> = optionsFunc();
bar.id = getUniqueID("bar-");
bar.type = BarType;
bar[Component] = BarComponent;
processComputable(bar as T, "visibility");
setDefault(bar, "visibility", Visibility.Visible);
processComputable(bar as T, "width");
processComputable(bar as T, "height");
processComputable(bar as T, "direction");
processComputable(bar as T, "style");
processComputable(bar as T, "classes");
processComputable(bar as T, "borderStyle");
processComputable(bar as T, "baseStyle");
processComputable(bar as T, "textStyle");
processComputable(bar as T, "fillStyle");
processComputable(bar as T, "progress");
processComputable(bar as T, "display");
processComputable(bar as T, "mark");
processComputable(bar as T, "visibility");
setDefault(bar, "visibility", Visibility.Visible);
processComputable(bar as T, "width");
processComputable(bar as T, "height");
processComputable(bar as T, "direction");
processComputable(bar as T, "style");
processComputable(bar as T, "classes");
processComputable(bar as T, "borderStyle");
processComputable(bar as T, "baseStyle");
processComputable(bar as T, "textStyle");
processComputable(bar as T, "fillStyle");
processComputable(bar as T, "progress");
processComputable(bar as T, "display");
processComputable(bar as T, "mark");
const proxy = createProxy(bar as unknown as Bar<T>);
return proxy;
bar[GatherProps] = function (this: GenericBar) {
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 {
Component,
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
@ -22,7 +23,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue";
import { Link } from "./links";
@ -177,6 +178,7 @@ interface BaseBoard extends Persistent<BoardData> {
selectedAction: Ref<GenericBoardNodeAction | null>;
type: typeof BoardType;
[Component]: typeof BoardComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Board<T extends BoardOptions> = Replace<
@ -198,101 +200,139 @@ export type GenericBoard = Replace<
}
>;
export function createBoard<T extends BoardOptions>(options: T & ThisType<Board<T>>): Board<T> {
const board: T & Partial<BaseBoard> = options;
makePersistent<BoardData>(board, {
nodes: [],
selectedNode: null,
selectedAction: null
});
board.id = getUniqueID("board-");
board.type = BoardType;
board[Component] = BoardComponent;
board.nodes = computed(() => proxy[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
proxy.nodes.value.find(node => node.id === proxy[PersistentState].value.selectedNode) ||
null
);
board.selectedAction = computed(() => {
if (proxy.selectedNode.value == null) {
return null;
}
const type = proxy.types[proxy.selectedNode.value.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === proxy[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (proxy.selectedAction.value == null) {
return null;
}
if (proxy.selectedAction.value.links && proxy.selectedNode.value) {
return getNodeProperty(proxy.selectedAction.value.links, proxy.selectedNode.value);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType, "title");
processComputable(nodeType, "label");
processComputable(nodeType, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType, "progress");
processComputable(nodeType, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType, "fillColor");
processComputable(nodeType, "outlineColor");
processComputable(nodeType, "titleColor");
processComputable(nodeType, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
proxy[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
proxy[PersistentState].value.selectedNode = node.id;
export function createBoard<T extends BoardOptions>(
optionsFunc: () => T & ThisType<Board<T>>
): Board<T> {
return createLazyProxy(() => {
const board: T & Partial<BaseBoard> = optionsFunc();
makePersistent<BoardData>(board, {
nodes: [],
selectedNode: null,
selectedAction: null
});
board.id = getUniqueID("board-");
board.type = BoardType;
board[Component] = BoardComponent;
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
processedBoard.nodes.value.find(
node => node.id === board[PersistentState].value.selectedNode
) || null
);
board.selectedAction = computed(() => {
const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null;
}
const type = processedBoard.types[selectedNode.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === processedBoard[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (processedBoard.selectedAction.value == null) {
return null;
}
if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
board[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
board[PersistentState].value.selectedNode = node.id;
});
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
}
}
}
board.types[type] = createProxy(nodeType as unknown as GenericNodeType);
}
board[GatherProps] = function (this: GenericBoard) {
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
};
};
const proxy = createProxy(board as unknown as Board<T>);
return proxy;
// 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 {

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import ClickableComponent from "@/components/features/Clickable.vue";
import {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
Replace,
setDefault,
@ -15,7 +16,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
export const ClickableType = Symbol("Clickable");
@ -41,6 +42,7 @@ interface BaseClickable {
id: string;
type: typeof ClickableType;
[Component]: typeof ClickableComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Clickable<T extends ClickableOptions> = Replace<
@ -64,21 +66,50 @@ export type GenericClickable = Replace<
>;
export function createClickable<T extends ClickableOptions>(
options: T & ThisType<Clickable<T>>
optionsFunc: () => T & ThisType<Clickable<T>>
): Clickable<T> {
const clickable: T & Partial<BaseClickable> = options;
clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType;
clickable[Component] = ClickableComponent;
return createLazyProxy(() => {
const clickable: T & Partial<BaseClickable> = optionsFunc();
clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType;
clickable[Component] = ClickableComponent;
processComputable(clickable as T, "visibility");
setDefault(clickable, "visibility", Visibility.Visible);
processComputable(clickable as T, "canClick");
processComputable(clickable as T, "classes");
processComputable(clickable as T, "style");
processComputable(clickable as T, "mark");
processComputable(clickable as T, "display");
processComputable(clickable as T, "visibility");
setDefault(clickable, "visibility", Visibility.Visible);
processComputable(clickable as T, "canClick");
setDefault(clickable, "canClick", true);
processComputable(clickable as T, "classes");
processComputable(clickable as T, "style");
processComputable(clickable as T, "mark");
processComputable(clickable as T, "display");
const proxy = createProxy(clickable as unknown as Clickable<T>);
return proxy;
clickable[GatherProps] = function (this: GenericClickable) {
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,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { computed, isRef, Ref, unref } from "vue";
import { Replace, setDefault } from "./feature";
import { Resource } from "./resource";
@ -48,39 +48,46 @@ export type GenericConversion = Replace<
>;
export function createConversion<T extends ConversionOptions>(
options: T & ThisType<Conversion<T>>
optionsFunc: () => T & ThisType<Conversion<T>>
): Conversion<T> {
const conversion: T = options;
return createLazyProxy(() => {
const conversion: T = optionsFunc();
if (conversion.convert == null) {
conversion.convert = function () {
unref<Resource>(proxy.gainResource).value = Decimal.add(
unref<Resource>(proxy.gainResource).value,
proxy.modifyGainAmount
? proxy.modifyGainAmount(unref(proxy.currentGain))
: unref(proxy.currentGain)
if (conversion.currentGain == null) {
conversion.currentGain = computed(() =>
conversion.scaling.currentGain(conversion as GenericConversion)
);
// TODO just subtract cost?
proxy.baseResource.value = 0;
};
}
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() =>
conversion.scaling.nextAt(conversion as GenericConversion)
);
}
if (conversion.currentGain == null) {
conversion.currentGain = computed(() => proxy.scaling.currentGain(proxy));
}
if (conversion.nextAt == null) {
conversion.nextAt = computed(() => proxy.scaling.nextAt(proxy));
}
if (conversion.convert == null) {
conversion.convert = function () {
conversion.gainResource.value = Decimal.add(
conversion.gainResource.value,
conversion.modifyGainAmount
? conversion.modifyGainAmount(
unref((conversion as GenericConversion).currentGain)
)
: unref((conversion as GenericConversion).currentGain)
);
// TODO just subtract cost?
conversion.baseResource.value = 0;
};
}
processComputable(conversion as T, "currentGain");
processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax");
setDefault(conversion, "buyMax", true);
processComputable(conversion as T, "roundUpCost");
setDefault(conversion, "roundUpCost", true);
processComputable(conversion as T, "currentGain");
processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax");
setDefault(conversion, "buyMax", true);
processComputable(conversion as T, "roundUpCost");
setDefault(conversion, "roundUpCost", true);
const proxy = createProxy(conversion as unknown as Conversion<T>);
return proxy;
return conversion as unknown as Conversion<T>;
});
}
export type ScalingFunction = {
@ -88,14 +95,22 @@ export type ScalingFunction = {
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(
base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource>
): ScalingFunction {
return {
currentGain(conversion) {
let gain = Decimal.sub(unref<Resource>(conversion.baseResource).value, unref(base))
.div(unref(coefficient))
if (Decimal.lt(conversion.baseResource.value, unref(base))) {
return 0;
}
let gain = Decimal.sub(conversion.baseResource.value, unref(base))
.sub(1)
.times(unref(coefficient))
.add(1)
.floor()
.max(0);
@ -115,8 +130,8 @@ export function createLinearScaling(
};
}
// Note: Not sure this actually should be described as exponential
// Gain formula is base * coefficient ^ (baseResource ^ exponent)
// Gain formula is (baseResource / base) ^ 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(
base: DecimalSource | Ref<DecimalSource>,
coefficient: DecimalSource | Ref<DecimalSource>,
@ -124,12 +139,15 @@ export function createExponentialScaling(
): ScalingFunction {
return {
currentGain(conversion) {
let gain = Decimal.div(unref<Resource>(conversion.baseResource).value, unref(base))
.log(unref(coefficient))
.pow(Decimal.div(1, unref(exponent)))
let gain = Decimal.div(conversion.baseResource.value, unref(base))
.pow(unref(exponent))
.floor()
.max(0);
if (gain.isNan()) {
return new Decimal(0);
}
if (!conversion.buyMax) {
gain = gain.min(1);
}
@ -147,51 +165,89 @@ export function createExponentialScaling(
}
export function createCumulativeConversion<S extends ConversionOptions>(
options: S & ThisType<Conversion<S>>
optionsFunc: () => S & ThisType<Conversion<S>>
): Conversion<S> {
return createConversion(options);
return createConversion(optionsFunc);
}
export function createIndependentConversion<S extends ConversionOptions>(
options: S & ThisType<Conversion<S>>
optionsFunc: () => S & ThisType<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) {
conversion.currentGain = computed(() =>
Decimal.sub(proxy.scaling.currentGain(proxy), unref<Resource>(proxy.gainResource).value)
.add(1)
.max(1)
);
}
setDefault(conversion, "convert", function () {
unref<Resource>(proxy.gainResource).value = proxy.modifyGainAmount
? proxy.modifyGainAmount(unref(proxy.currentGain))
: unref(proxy.currentGain);
// TODO just subtract cost?
// Maybe by adding a cost function to scaling and nextAt just calls the cost function
// with 1 + currentGain
proxy.baseResource.value = 0;
if (conversion.currentGain == null) {
conversion.currentGain = computed(() =>
Decimal.sub(
conversion.scaling.currentGain(conversion as GenericConversion),
conversion.gainResource.value
)
.add(1)
.max(1)
);
}
setDefault(conversion, "convert", function () {
conversion.gainResource.value = conversion.modifyGainAmount
? conversion.modifyGainAmount(unref((conversion as GenericConversion).currentGain))
: unref((conversion as GenericConversion).currentGain);
// TODO just subtract cost?
// Maybe by adding a cost function to scaling and nextAt just calls the cost function
// with 1 + currentGain
conversion.baseResource.value = 0;
});
return conversion;
});
const proxy = createConversion(conversion);
return proxy;
}
export function setupPassiveGeneration(
layer: GenericLayer,
conversion: GenericConversion,
rate: DecimalSource | Ref<DecimalSource> = 1
rate: ProcessedComputable<DecimalSource> = 1
): void {
layer.on("preUpdate", (diff: Decimal) => {
const currRate = isRef(rate) ? rate.value : rate;
if (Decimal.neq(currRate, 0)) {
conversion.gainResource.value = Decimal.add(
unref<Resource>(conversion.gainResource).value,
conversion.gainResource.value,
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 { GenericLayer } from "@/game/layers";
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 { ComponentOptions, CSSProperties, DefineComponent, isRef, ref, Ref } from "vue";
import { CSSProperties, DefineComponent, isRef, ref, Ref } from "vue";
export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue");
export const Component = Symbol("Component");
export const GatherProps = Symbol("GatherProps");
// Note: This is a union of things that should be safely stringifiable without needing
// special processes for knowing what to load them in as
@ -20,7 +22,8 @@ export type State =
| DecimalSource
| { [key: string]: 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 Persistent<T extends State = State> = {
@ -58,6 +61,11 @@ export enum Visibility {
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 {
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 as Record<string, unknown>).type === type) {
objects.push(value);
} else {
} else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>);
}
}
@ -135,8 +143,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
// Load previously saved value
if (savedValue != null) {
(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
const foundPersistentInChild = handleObject(value as Record<string, unknown>, [
...path,
@ -146,10 +158,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
// Show warning for persistent values inside arrays
// TODO handle arrays better
if (foundPersistentInChild) {
if (isArray(value)) {
if (isArray(value) && !isArray(obj)) {
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.",
obj,
ProxyState in obj
? (obj as Record<PropertyKey, unknown>)[ProxyState]
: obj,
key
);
} else {

View file

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

View file

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

View file

@ -2,6 +2,7 @@ import InfoboxComponent from "@/components/features/Infobox.vue";
import {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
@ -18,7 +19,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { Ref } from "vue";
export const InfoboxType = Symbol("Infobox");
@ -39,6 +40,7 @@ interface BaseInfobox extends Persistent<boolean> {
collapsed: Ref<boolean>;
type: typeof InfoboxType;
[Component]: typeof InfoboxComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Infobox<T extends InfoboxOptions> = Replace<
@ -63,26 +65,54 @@ export type GenericInfobox = Replace<
>;
export function createInfobox<T extends InfoboxOptions>(
options: T & ThisType<Infobox<T>>
optionsFunc: () => T & ThisType<Infobox<T>>
): Infobox<T> {
const infobox: T & Partial<BaseInfobox> = options;
makePersistent<boolean>(infobox, false);
infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent;
return createLazyProxy(() => {
const infobox: T & Partial<BaseInfobox> = optionsFunc();
makePersistent<boolean>(infobox, false);
infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent;
infobox.collapsed = infobox[PersistentState];
infobox.collapsed = infobox[PersistentState];
processComputable(infobox as T, "visibility");
setDefault(infobox, "visibility", Visibility.Visible);
processComputable(infobox as T, "color");
processComputable(infobox as T, "style");
processComputable(infobox as T, "titleStyle");
processComputable(infobox as T, "bodyStyle");
processComputable(infobox as T, "classes");
processComputable(infobox as T, "title");
processComputable(infobox as T, "display");
processComputable(infobox as T, "visibility");
setDefault(infobox, "visibility", Visibility.Visible);
processComputable(infobox as T, "color");
processComputable(infobox as T, "style");
processComputable(infobox as T, "titleStyle");
processComputable(infobox as T, "bodyStyle");
processComputable(infobox as T, "classes");
processComputable(infobox as T, "title");
processComputable(infobox as T, "display");
const proxy = createProxy(infobox as unknown as Infobox<T>);
return proxy;
infobox[GatherProps] = function (this: GenericInfobox) {
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,
Component,
findFeatures,
GatherProps,
getUniqueID,
makePersistent,
Persistent,
@ -22,7 +23,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { coerceComponent, isCoercableComponent } from "@/util/vue";
import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue";
@ -59,6 +60,7 @@ interface BaseMilestone extends Persistent<boolean> {
earned: Ref<boolean>;
type: typeof MilestoneType;
[Component]: typeof MilestoneComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Milestone<T extends MilestoneOptions> = Replace<
@ -80,52 +82,59 @@ export type GenericMilestone = Replace<
>;
export function createMilestone<T extends MilestoneOptions>(
options: T & ThisType<Milestone<T>>
optionsFunc: () => T & ThisType<Milestone<T>>
): Milestone<T> {
const milestone: T & Partial<BaseMilestone> = options;
makePersistent<boolean>(milestone, false);
milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent;
return createLazyProxy(() => {
const milestone: T & Partial<BaseMilestone> = optionsFunc();
makePersistent<boolean>(milestone, false);
milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent;
milestone.earned = milestone[PersistentState];
processComputable(milestone as T, "visibility");
setDefault(milestone, "visibility", Visibility.Visible);
const visibility = milestone.visibility as ProcessedComputable<Visibility>;
milestone.visibility = computed(() => {
switch (settings.msDisplay) {
default:
case MilestoneDisplay.All:
return unref(visibility);
case MilestoneDisplay.Configurable:
if (
unref(proxy.earned) &&
!(
proxy.display != null &&
typeof unref(proxy.display) == "object" &&
"optionsDisplay" in (unref(proxy.display) as Record<string, unknown>)
)
) {
milestone.earned = milestone[PersistentState];
processComputable(milestone as T, "visibility");
setDefault(milestone, "visibility", Visibility.Visible);
const visibility = milestone.visibility as ProcessedComputable<Visibility>;
milestone.visibility = computed(() => {
const display = unref((milestone as GenericMilestone).display);
switch (settings.msDisplay) {
default:
case MilestoneDisplay.All:
return unref(visibility);
case MilestoneDisplay.Configurable:
if (
unref(milestone.earned) &&
!(
display != null &&
typeof display == "object" &&
"optionsDisplay" in (display as Record<string, unknown>)
)
) {
return Visibility.None;
}
return unref(visibility);
case MilestoneDisplay.Incomplete:
if (unref(milestone.earned)) {
return Visibility.None;
}
return unref(visibility);
case MilestoneDisplay.None:
return Visibility.None;
}
return unref(visibility);
case MilestoneDisplay.Incomplete:
if (unref(proxy.earned)) {
return Visibility.None;
}
return unref(visibility);
case MilestoneDisplay.None:
return Visibility.None;
}
}
});
processComputable(milestone as T, "shouldEarn");
processComputable(milestone as T, "style");
processComputable(milestone as T, "classes");
processComputable(milestone as T, "display");
milestone[GatherProps] = function (this: GenericMilestone) {
const { visibility, display, style, classes, earned, id } = this;
return { visibility, display, style, classes, earned, id };
};
return milestone as unknown as Milestone<T>;
});
processComputable(milestone as T, "shouldEarn");
processComputable(milestone as T, "style");
processComputable(milestone as T, "classes");
processComputable(milestone as T, "display");
const proxy = createProxy(milestone as unknown as Milestone<T>);
return proxy;
}
const toast = useToast();
@ -150,14 +159,14 @@ globalBus.on("addLayer", layer => {
isCoercableComponent(display) ? display : display.requirement
);
toast(
<template>
<>
<h3>Milestone earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</template>
</>
);
}
}

View file

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

View file

@ -1,13 +1,13 @@
import { persistent, State } from "@/features/feature";
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";
export type Resource<T = DecimalSource> = Ref<T> & {
export interface Resource<T = DecimalSource> extends Ref<T> {
displayName: string;
precision: number;
small: boolean;
};
}
export function createResource<T extends State>(
defaultValue: T | Ref<T>,
@ -22,27 +22,6 @@ export function createResource<T extends State>(
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> {
const best = persistent(resource.value);
watch(resource, amount => {
@ -63,63 +42,68 @@ export function trackTotal(resource: Resource): Ref<DecimalSource> {
return total;
}
export function trackOOMPS(resource: Resource): Ref<string> {
let oomps: DecimalSource = 0;
let oompsMag = 0;
let lastPoints: DecimalSource = 0;
export function trackOOMPS(
resource: Resource,
pointGain?: ComputedRef<DecimalSource>
): Ref<string> {
const oomps = ref<DecimalSource>(0);
const oompsMag = ref(0);
const lastPoints = ref<DecimalSource>(0);
globalBus.on("update", diff => {
oompsMag.value = 0;
if (Decimal.lte(resource.value, 1e100)) {
lastPoints = resource.value;
lastPoints.value = resource.value;
return;
}
let curr = resource.value;
let prev = lastPoints;
lastPoints = curr;
let prev = lastPoints.value;
lastPoints.value = curr;
if (Decimal.gt(curr, prev)) {
if (Decimal.gte(curr, "10^^8")) {
curr = Decimal.slog(curr, 1e10);
prev = Decimal.slog(prev, 1e10);
oomps = curr.sub(prev).div(diff);
oompsMag = -1;
oomps.value = curr.sub(prev).div(diff);
oompsMag.value = -1;
} else {
while (
Decimal.div(curr, prev).log(10).div(diff).gte("100") &&
oompsMag <= 5 &&
oompsMag.value <= 5 &&
Decimal.gt(prev, 0)
) {
curr = Decimal.log10(curr);
prev = Decimal.log10(prev);
oomps = curr.sub(prev).div(diff);
oompsMag++;
oomps.value = curr.sub(prev).div(diff);
oompsMag.value++;
}
}
}
});
return computed(
() =>
format(oomps) +
const oompsString = computed(() => {
if (oompsMag.value === 0) {
return pointGain
? format(pointGain.value, resource.precision, resource.small) +
" " +
resource.displayName +
"/s"
: "";
}
return (
format(oomps.value) +
" OOM" +
(oompsMag < 0 ? "^OOM" : oompsMag > 1 ? "^" + oompsMag : "") +
"s"
);
(oompsMag.value < 0 ? "^OOM" : "^" + oompsMag.value) +
"s/sec"
);
});
return oompsString;
}
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)) {
return formatWhole(amount);
}
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 { Computable, GetComputableType } from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { CoercableComponent, Component, getUniqueID, Replace, StyleValue } from "./feature";
import { createLazyProxy } from "@/util/proxies";
import {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
Replace,
StyleValue
} from "./feature";
export const TabType = Symbol("Tab");
@ -15,6 +22,7 @@ interface BaseTab {
id: string;
type: typeof TabType;
[Component]: typeof TabComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Tab<T extends TabOptions> = Replace<
@ -28,12 +36,18 @@ export type Tab<T extends TabOptions> = Replace<
export type GenericTab = Tab<TabOptions>;
export function createTab<T extends TabOptions>(options: T & ThisType<Tab<T>>): Tab<T> {
const tab: T & Partial<BaseTab> = options;
tab.id = getUniqueID("tab-");
tab.type = TabType;
tab[Component] = TabComponent;
export function createTab<T extends TabOptions>(optionsFunc: () => T & ThisType<Tab<T>>): Tab<T> {
return createLazyProxy(() => {
const tab: T & Partial<BaseTab> = optionsFunc();
tab.id = getUniqueID("tab-");
tab.type = TabType;
tab[Component] = TabComponent;
const proxy = createProxy(tab as unknown as Tab<T>);
return proxy;
tab[GatherProps] = function (this: GenericTab) {
const { display } = this;
return { display };
};
return tab as unknown as Tab<T>;
});
}

View file

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

View file

@ -18,3 +18,8 @@ export interface Tooltip {
yoffset?: ProcessedComputable<string>;
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 {
CoercableComponent,
Component,
GatherProps,
getUniqueID,
persistent,
Replace,
@ -20,7 +21,7 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { computed, ref, Ref, unref } from "vue";
import { Link } from "./links";
import { GenericReset } from "./reset";
@ -74,33 +75,34 @@ export type GenericTreeNode = Replace<
>;
export function createTreeNode<T extends TreeNodeOptions>(
options: T & ThisType<TreeNode<T>>
optionsFunc: () => T & ThisType<TreeNode<T>>
): TreeNode<T> {
const treeNode: T & Partial<BaseTreeNode> = options;
treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType;
return createLazyProxy(() => {
const treeNode: T & Partial<BaseTreeNode> = optionsFunc();
treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType;
if (treeNode.tooltip) {
treeNode.forceTooltip = persistent(false);
} else {
// If we don't have a tooltip, no point in making this persistent
treeNode.forceTooltip = ref(false);
}
if (treeNode.tooltip) {
treeNode.forceTooltip = persistent(false);
} else {
// If we don't have a tooltip, no point in making this persistent
treeNode.forceTooltip = ref(false);
}
processComputable(treeNode as T, "visibility");
setDefault(treeNode, "visibility", Visibility.Visible);
processComputable(treeNode as T, "canClick");
setDefault(treeNode, "canClick", true);
processComputable(treeNode as T, "color");
processComputable(treeNode as T, "display");
processComputable(treeNode as T, "tooltip");
processComputable(treeNode as T, "glowColor");
processComputable(treeNode as T, "classes");
processComputable(treeNode as T, "style");
processComputable(treeNode as T, "mark");
processComputable(treeNode as T, "visibility");
setDefault(treeNode, "visibility", Visibility.Visible);
processComputable(treeNode as T, "canClick");
setDefault(treeNode, "canClick", true);
processComputable(treeNode as T, "color");
processComputable(treeNode as T, "display");
processComputable(treeNode as T, "tooltip");
processComputable(treeNode as T, "glowColor");
processComputable(treeNode as T, "classes");
processComputable(treeNode as T, "style");
processComputable(treeNode as T, "mark");
const proxy = createProxy(treeNode as unknown as TreeNode<T>);
return proxy;
return treeNode as unknown as TreeNode<T>;
});
}
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
@ -126,6 +128,7 @@ interface BaseTree {
resettingNode: Ref<GenericTreeNode | null>;
type: typeof TreeType;
[Component]: typeof TreeComponent;
[GatherProps]: () => Record<string, unknown>;
}
export type Tree<T extends TreeOptions> = Replace<
@ -146,33 +149,46 @@ export type GenericTree = Replace<
}
>;
export function createTree<T extends TreeOptions>(options: T & ThisType<Tree<T>>): Tree<T> {
const tree: T & Partial<BaseTree> = options;
tree.id = getUniqueID("tree-");
tree.type = TreeType;
tree[Component] = TreeComponent;
export function createTree<T extends TreeOptions>(
optionsFunc: () => T & ThisType<Tree<T>>
): Tree<T> {
return createLazyProxy(() => {
const tree: T & Partial<BaseTree> = optionsFunc();
tree.id = getUniqueID("tree-");
tree.type = TreeType;
tree[Component] = TreeComponent;
tree.isResetting = ref(false);
tree.resettingNode = ref(null);
tree.isResetting = ref(false);
tree.resettingNode = ref(null);
tree.reset = function (node) {
proxy.isResetting.value = true;
proxy.resettingNode.value = node;
proxy.resetPropagation?.(proxy, node);
proxy.isResetting.value = false;
proxy.resettingNode.value = null;
};
tree.links = computed(() => (proxy.branches == null ? [] : unref(proxy.branches)));
tree.reset = function (node) {
const genericTree = tree as GenericTree;
genericTree.isResetting.value = true;
genericTree.resettingNode.value = node;
genericTree.resetPropagation?.(genericTree, node);
genericTree.onReset?.(node);
genericTree.isResetting.value = false;
genericTree.resettingNode.value = null;
};
tree.links = computed(() => {
const genericTree = tree as GenericTree;
return unref(genericTree.branches) ?? [];
});
processComputable(tree as T, "visibility");
setDefault(tree, "visibility", Visibility.Visible);
processComputable(tree as T, "nodes");
processComputable(tree as T, "leftSideNodes");
processComputable(tree as T, "rightSideNodes");
processComputable(tree as T, "branches");
processComputable(tree as T, "visibility");
setDefault(tree, "visibility", Visibility.Visible);
processComputable(tree as T, "nodes");
processComputable(tree as T, "leftSideNodes");
processComputable(tree as T, "rightSideNodes");
processComputable(tree as T, "branches");
const proxy = createProxy(tree as unknown as Tree<T>);
return proxy;
tree[GatherProps] = function (this: GenericTree) {
const { nodes, leftSideNodes, rightSideNodes } = this;
return { nodes, leftSideNodes, rightSideNodes };
};
return tree as unknown as Tree<T>;
});
}
export type ResetPropagation = {
@ -213,17 +229,25 @@ export const branchedResetPropagation = function (
const nextNodes: GenericTreeNode[] = [];
currentNodes.forEach(node => {
branches
.filter(
branch =>
branch.startNode === node &&
!visitedNodes.includes(unref(branch.endNode))
)
.forEach(branch => {
visitedNodes.push(branch.startNode);
nextNodes.push(branch.endNode);
.filter(branch => branch.startNode === node || branch.endNode === node)
.map(branch => {
if (branch.startNode === node) {
return branch.endNode;
}
return branch.startNode;
})
.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;
visitedNodes.push(...currentNodes);
}
}
};

View file

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

View file

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

View file

@ -15,9 +15,8 @@ import {
processComputable,
ProcessedComputable
} from "@/util/computed";
import { createProxy } from "@/util/proxies";
import { createLazyProxy } from "@/util/proxies";
import { createNanoEvents, Emitter } from "nanoevents";
import { customRef, Ref } from "vue";
import { globalBus } from "./events";
import player from "./player";
@ -33,6 +32,12 @@ export interface LayerEvents {
export const layers: Record<string, Readonly<GenericLayer> | undefined> = {};
window.layers = layers;
declare module "@vue/runtime-dom" {
interface CSSProperties {
"--layer-color"?: string;
}
}
export interface Position {
x: number;
y: number;
@ -82,39 +87,26 @@ export type GenericLayer = Replace<
}
>;
export function createLayer<T extends LayerOptions>(optionsFunc: () => T): Ref<Layer<T>> {
let layer: Layer<T> | null = null;
export function createLayer<T extends LayerOptions>(optionsFunc: () => T): Layer<T> {
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 => {
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);
layer.minimized = persistent(false);
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");
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!");
}
};
return layer as unknown as Layer<T>;
});
}

View file

@ -3,3 +3,16 @@ import Toast from "vue-toastification";
import "vue-toastification/dist/index.css";
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 { isPlainObject } from "@/util/common";
import { ProxiedWithState, ProxyPath, ProxyState } from "@/util/proxies";
import { shallowReactive, unref } from "vue";
import { reactive, unref } from "vue";
import transientState from "./state";
export interface PlayerData {
@ -15,7 +15,6 @@ export interface PlayerData {
offlineTime: DecimalSource | null;
timePlayed: DecimalSource;
keepGoing: boolean;
minimized: Record<string, boolean>;
modID: string;
modVersion: string;
layers: Record<string, Record<string, unknown>>;
@ -23,7 +22,7 @@ export interface PlayerData {
export type Player = ProxiedWithState<PlayerData>;
const state = shallowReactive<PlayerData>({
const state = reactive<PlayerData>({
id: "",
devSpeed: null,
name: "",
@ -34,14 +33,13 @@ const state = shallowReactive<PlayerData>({
offlineTime: null,
timePlayed: new Decimal(0),
keepGoing: false,
minimized: {},
modID: "",
modVersion: "",
layers: {}
});
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
@ -53,7 +51,7 @@ const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
}
const value = target[ProxyState][key];
if (isPlainObject(value) && !(value instanceof Decimal)) {
if (key !== "value" && isPlainObject(value) && !(value instanceof Decimal)) {
if (value !== target[key]?.[ProxyState]) {
const path = [...target[ProxyPath], key];
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
has(target: Record<PropertyKey, any>, key: string) {
return Reflect.has(target[ProxyState], key);
},
getOwnPropertyDescriptor(target, key) {
return Object.getOwnPropertyDescriptor(target[ProxyState], key);
}
};
export default window.player = new Proxy(
{ [ProxyState]: state, [ProxyPath]: ["player"] },
playerHandler
) as PlayerData;
) as Player;

View file

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

View file

@ -28,10 +28,16 @@ declare global {
toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string;
formatSmall: (x: DecimalSource, precision?: number) => string;
invertOOM: (x: DecimalSource) => Decimal;
modInfo: typeof modInfo;
}
}
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();
const { globalBus, startGameLoop } = await require("./game/events");
@ -45,3 +51,5 @@ requestAnimationFrame(async () => {
startGameLoop();
});
window.modInfo = modInfo;

View file

@ -1,9 +1,13 @@
import { computed, Ref } from "vue";
import { isFunction } from "./common";
export const DoNotCache = Symbol("DoNotCache");
export type Computable<T> = T | Ref<T> | (() => 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>
: undefined extends T
? undefined
@ -27,7 +31,8 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
key: S
): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } {
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
// @ts-ignore
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> {
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);
}
return obj;
return obj as ProcessedComputable<T>;
}

View file

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

View file

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

View file

@ -3,73 +3,70 @@ import Row from "@/components/system/Row.vue";
import {
CoercableComponent,
Component as ComponentKey,
GenericComponent
GatherProps,
GenericComponent,
JSXFunction
} from "@/features/feature";
import { isArray } from "@vue/shared";
import {
Component,
computed,
ComputedRef,
DefineComponent,
defineComponent,
h,
isRef,
PropType,
ref,
Ref,
ShallowRef,
shallowRef,
unref,
WritableComputedRef
watchEffect
} 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") {
component = component.trim();
if (component.charAt(0) !== "<") {
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
}
if (component.length > 0) {
component = component.trim();
if (component.charAt(0) !== "<") {
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
}
return defineComponent({ template: component });
return defineComponent({ template: component });
}
return defineComponent({ render: () => ({}) });
}
return component;
}
export function render(object: { [ComponentKey]: GenericComponent }): DefineComponent {
return defineComponent({
render() {
const component = object[ComponentKey];
return h(component, object);
export type VueFeature = {
[ComponentKey]: GenericComponent;
[GatherProps]: () => Record<string, unknown>;
};
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(
objects: { [ComponentKey]: GenericComponent }[],
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 renderRow(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Row>{objects.map(obj => render(obj))}</Row>;
}
export function renderCol(
objects: { [ComponentKey]: GenericComponent }[],
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 renderCol(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Col>{objects.map(obj => render(obj))}</Col>;
}
export function isCoercableComponent(component: unknown): component is CoercableComponent {
@ -80,6 +77,9 @@ export function isCoercableComponent(component: unknown): component is Coercable
return false;
}
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;
}
@ -117,23 +117,25 @@ export function setupHoldToClick(
}
export function computeComponent(
component: Ref<ProcessedComputable<CoercableComponent>>
): ComputedRef<Component> {
return computed(() => {
return coerceComponent(unref(unref<ProcessedComputable<CoercableComponent>>(component)));
component: Ref<ProcessedComputable<CoercableComponent>>,
defaultWrapper = "div"
): ShallowRef<Component | JSXFunction | ""> {
const comp = shallowRef<Component | JSXFunction | "">();
watchEffect(() => {
comp.value = coerceComponent(unwrapRef(component), defaultWrapper);
});
return comp as ShallowRef<Component | JSXFunction | "">;
}
export function computeOptionalComponent(
component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined>
): ComputedRef<Component | undefined> {
return computed(() => {
let currComponent = unref<ProcessedComputable<CoercableComponent | undefined> | undefined>(
component
);
if (currComponent == null) return;
currComponent = unref(currComponent);
return currComponent == null ? undefined : coerceComponent(currComponent);
component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined>,
defaultWrapper = "div"
): ShallowRef<Component | JSXFunction | "" | null> {
const comp = shallowRef<Component | JSXFunction | "" | null>(null);
watchEffect(() => {
const currComponent = unwrapRef(component);
comp.value = currComponent == null ? null : coerceComponent(currComponent, defaultWrapper);
});
return comp;
}
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 {
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 =