- Removed `jsx()` and `JSXFunction`. You can now use `JSX.Element` like any other `Computable` value - `joinJSX` now always requires a joiner. Just pass the array of elements or wrap them in `<>` and `</>` if there's no joiner - Removed `coerceComponent`, `computeComponent`, and `computeOptionalComponent`; just use the `render` function now - It's recommended to now do `<MyComponent />` instead of `<component :is="myComponent" />` - All features no longer take the options as a type parameter, and all generic forms have been removed as a result - Fixed `forceHideGoBack` not being respected - Removed `deepUnref` as now things don't get unreffed before being passed into vue components by default - Moved MarkNode to new wrapper, and removed existing `mark` properties - Moved Tooltip to new wrapper, and made it take an options function instead of raw object - VueFeature component now wraps all vue features, and applies styling, classes, and visibility in the wrapping div. It also adds the Node component so features don't need to - `mergeAdjacent` now works with grids (perhaps should've used scss to reduce the amount of css this took) - `CoercableComponent` renamed to `Renderable` since it should be used with `render` - Replaced `isCoercableComponent` with `isJSXElement` - Replaced `Computable` and `ProcessedComputable` with the vue built-ins `MaybeRefOrGetter` and `MaybeRef` - `convertComputable` renamed to `processGetter` - Also removed `GetComputableTypeWithDefault` and `GetComputableType`, which can similarly be replaced - `dontMerge` is now a property on rows and columns rather than an undocumented css class you'd have to include on every feature within the row or column - Fixed saves manager not being imported in addiction warning component - Created `vueFeatureMixin` for simplifying the vue specific parts of a feature. Passes the component's properties in explicitly and directly from the feature itself - All features should now return an object that includes props typed to omit the options object and satisfies the feature. This will ensure type correctness and pass-through custom properties. (see existing features for more thorough examples of changes) - Replaced decorators with mixins, which won't require casting. Bonus amount decorators converted into generic bonus amount mixin. Removed effect decorator - All `render` functions now return `JSX.Element`. The `JSX` variants (e.g. `renderJSX`) (except `joinJSX`) have been removed - Moved all features that use the clickable component into the clickable folder - Removed `small` property from clickable, since its a single css rule (`min-height: unset`) (you could add a small css class and pass small to any vue feature's classes property, though) - Upgrades now use the clickable component - Added ConversionType symbol - Removed setDefault, just use `??=` - Added isType function that uses a type symbol to check - General cleanup
This commit is contained in:
parent
4987916900
commit
1657dffcfe
100 changed files with 8633 additions and 9367 deletions
13
src/App.vue
13
src/App.vue
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div v-if="appErrors.length > 0" class="error-container" :style="theme"><Error :errors="appErrors" /></div>
|
||||
<div v-if="appErrors.length > 0" class="error-container" :style="theme">
|
||||
<Error :errors="appErrors" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div id="modal-root" :style="theme" />
|
||||
<div class="app" :style="theme" :class="{ useHeader }">
|
||||
|
@ -10,7 +12,7 @@
|
|||
<GameOverScreen />
|
||||
<NaNScreen />
|
||||
<CloudSaveResolver />
|
||||
<component :is="gameComponent" />
|
||||
<GameComponent />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -22,9 +24,8 @@ import AddictionWarning from "components/modals/AddictionWarning.vue";
|
|||
import CloudSaveResolver from "components/modals/CloudSaveResolver.vue";
|
||||
import GameOverScreen from "components/modals/GameOverScreen.vue";
|
||||
import NaNScreen from "components/modals/NaNScreen.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import state from "game/state";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { render } from "util/vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import Game from "./components/Game.vue";
|
||||
|
@ -40,9 +41,7 @@ const theme = computed(() => themes[settings.theme].variables as CSSProperties);
|
|||
const showTPS = toRef(settings, "showTPS");
|
||||
const appErrors = toRef(state, "errors");
|
||||
|
||||
const gameComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
|
||||
});
|
||||
const GameComponent = () => gameComponents.map(c => render(c));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
>
|
||||
<Nav v-if="index === 0 && !useHeader" />
|
||||
<div class="inner-tab">
|
||||
<Layer
|
||||
<LayerVue
|
||||
v-if="layerKeys.includes(tab)"
|
||||
v-bind="gatherLayerProps(layers[tab]!)"
|
||||
v-bind="gatherLayerProps(layers[tab])"
|
||||
:index="index"
|
||||
@set-minimized="(value: boolean) => (layers[tab]!.minimized.value = value)"
|
||||
/>
|
||||
|
@ -23,28 +23,36 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import { layers } from "game/layers";
|
||||
import { type Layer, layers } from "game/layers";
|
||||
import player from "game/player";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import Layer from "./Layer.vue";
|
||||
import LayerVue from "./Layer.vue";
|
||||
import Nav from "./Nav.vue";
|
||||
import { deepUnref } from "util/vue";
|
||||
|
||||
const tabs = toRef(player, "tabs");
|
||||
const layerKeys = computed(() => Object.keys(layers));
|
||||
const useHeader = projInfo.useHeader;
|
||||
|
||||
function gatherLayerProps(layer: GenericLayer) {
|
||||
const { display, name, color, minimizable, minimizedDisplay } = deepUnref(layer);
|
||||
function gatherLayerProps(layer: Layer) {
|
||||
const {
|
||||
display,
|
||||
name,
|
||||
color,
|
||||
minimizable,
|
||||
minimizedDisplay,
|
||||
minimized,
|
||||
nodes,
|
||||
forceHideGoBack
|
||||
} = layer;
|
||||
return {
|
||||
display,
|
||||
name,
|
||||
color,
|
||||
minimizable,
|
||||
minimizedDisplay,
|
||||
minimized: layer.minimized,
|
||||
nodes: layer.nodes
|
||||
minimized,
|
||||
nodes,
|
||||
forceHideGoBack
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GenericHotkey } from "features/hotkey";
|
||||
import { Hotkey } from "features/hotkey";
|
||||
import { watchEffect } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
hotkey: GenericHotkey;
|
||||
hotkey: Hotkey;
|
||||
}>();
|
||||
|
||||
let key = "";
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
v-if="unref(minimized)"
|
||||
@click="$emit('setMinimized', false)"
|
||||
>
|
||||
<component v-if="minimizedComponent" :is="minimizedComponent" />
|
||||
<MinimizedComponent v-if="minimizedDisplay" />
|
||||
<div v-else>{{ unref(name) }}</div>
|
||||
</button>
|
||||
<div class="layer-tab" :class="{ showGoBack }" v-else>
|
||||
<Context @update-nodes="updateNodes">
|
||||
<component :is="component" />
|
||||
<Component />
|
||||
</Context>
|
||||
</div>
|
||||
|
||||
|
@ -25,29 +25,29 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { Layer, type FeatureNode } from "game/layers";
|
||||
import player from "game/player";
|
||||
import { computeComponent, computeOptionalComponent } from "util/vue";
|
||||
import { Ref, computed, onErrorCaptured, ref, toRef, unref } from "vue";
|
||||
import { render } from "util/vue";
|
||||
import { computed, onErrorCaptured, ref, unref } from "vue";
|
||||
import Context from "./Context.vue";
|
||||
import ErrorVue from "./Error.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
display: Layer["display"];
|
||||
minimizedDisplay: Layer["minimizedDisplay"];
|
||||
minimized: Layer["minimized"];
|
||||
name: Layer["name"];
|
||||
color: Layer["color"];
|
||||
minimizable: Layer["minimizable"];
|
||||
nodes: Layer["nodes"];
|
||||
forceHideGoBack: Layer["forceHideGoBack"];
|
||||
index: number;
|
||||
display: CoercableComponent;
|
||||
minimizedDisplay?: CoercableComponent;
|
||||
minimized: Ref<boolean>;
|
||||
name: string;
|
||||
color?: string;
|
||||
minimizable?: boolean;
|
||||
nodes: Ref<Record<string, FeatureNode | undefined>>;
|
||||
}>();
|
||||
|
||||
const component = computeComponent(toRef(props, "display"));
|
||||
const minimizedComponent = computeOptionalComponent(toRef(props, "minimizedDisplay"));
|
||||
const Component = () => render(props.display);
|
||||
const MinimizedComponent = () => props.minimizedDisplay == null ? undefined : render(props.minimizedDisplay);
|
||||
const showGoBack = computed(
|
||||
() => projInfo.allowGoBack && props.index > 0 && !unref(props.minimized)
|
||||
() => projInfo.allowGoBack && !unref(props.forceHideGoBack) && props.index > 0 && !unref(props.minimized)
|
||||
);
|
||||
|
||||
function goBack() {
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
<script setup lang="ts">
|
||||
import Changelog from "data/Changelog.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import Tooltip from "wrappers/tooltips/Tooltip.vue";
|
||||
import settings from "game/settings";
|
||||
import { Direction } from "util/common";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
.feature:not(li),
|
||||
.feature:not(li) button {
|
||||
.feature {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button.feature,
|
||||
.feature button {
|
||||
padding: 5px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid rgba(0, 0, 0, 0.125);
|
||||
|
@ -11,13 +14,13 @@
|
|||
transition: all 0.5s, z-index 0s 0.5s;
|
||||
}
|
||||
|
||||
.can,
|
||||
button.can,
|
||||
.can button {
|
||||
background-color: var(--layer-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.can:hover,
|
||||
button.can:hover,
|
||||
.can:hover button {
|
||||
transform: scale(1.15, 1.15);
|
||||
box-shadow: 0 0 20px var(--points);
|
||||
|
@ -25,13 +28,13 @@
|
|||
transition: all 0.5s, z-index 0s;
|
||||
}
|
||||
|
||||
.locked,
|
||||
button.locked,
|
||||
.locked button {
|
||||
background-color: var(--locked);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bought,
|
||||
button.bought,
|
||||
.bought button {
|
||||
background-color: var(--bought);
|
||||
cursor: default;
|
||||
|
|
|
@ -20,11 +20,6 @@
|
|||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.row > :not(.feature) {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
|
@ -34,28 +29,152 @@
|
|||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.row.mergeAdjacent > .feature:not(.dontMerge),
|
||||
.row.mergeAdjacent > .tooltip-container > .feature:not(.dontMerge) {
|
||||
.row.mergeAdjacent *,
|
||||
.row.mergeAdjacent button.feature,
|
||||
.row.mergeAdjacent .feature button {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.row.mergeAdjacent button.feature,
|
||||
.row.mergeAdjacent .feature button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.row.mergeAdjacent > .feature:not(.dontMerge):first-child,
|
||||
.row.mergeAdjacent > .tooltip-container:first-child > .feature:not(.dontMerge) {
|
||||
.row.mergeAdjacent > button.feature:first-child,
|
||||
.row.mergeAdjacent > .feature:first-child button,
|
||||
.row.mergeAdjacent > :first-child button.feature,
|
||||
.row.mergeAdjacent > :first-child .feature button {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
|
||||
.row.mergeAdjacent > .feature:not(.dontMerge):last-child,
|
||||
.row.mergeAdjacent > .tooltip-container:last-child > .feature:not(.dontMerge) {
|
||||
.row.mergeAdjacent > button.feature:last-child,
|
||||
.row.mergeAdjacent > .feature:last-child button,
|
||||
.row.mergeAdjacent > :last-child button.feature,
|
||||
.row.mergeAdjacent > :last-child .feature button {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
|
||||
.row.mergeAdjacent > .feature:not(.dontMerge):first-child:last-child,
|
||||
.row.mergeAdjacent > .tooltip-container:first-child:last-child > .feature:not(.dontMerge) {
|
||||
.row.mergeAdjacent > button.feature:first-child:last-child,
|
||||
.row.mergeAdjacent > .feature:first-child:last-child button,
|
||||
.row.mergeAdjacent > :first-child:last-child button.feature,
|
||||
.row.mergeAdjacent > :first-child:last-child .feature button {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.col.mergeAdjacent *,
|
||||
.col.mergeAdjacent button.feature,
|
||||
.col.mergeAdjacent .feature button {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent button.feature,
|
||||
.col.mergeAdjacent .feature button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > button.feature:first-child,
|
||||
.col.mergeAdjacent > .feature:first-child button,
|
||||
.col.mergeAdjacent > :first-child button.feature,
|
||||
.col.mergeAdjacent > :first-child .feature button {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > button.feature:last-child,
|
||||
.col.mergeAdjacent > .feature:last-child button,
|
||||
.col.mergeAdjacent > :last-child button.feature,
|
||||
.col.mergeAdjacent > :last-child .feature button {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > button.feature:first-child:last-child,
|
||||
.col.mergeAdjacent > .feature:first-child:last-child button,
|
||||
.col.mergeAdjacent > :first-child:last-child button.feature,
|
||||
.col.mergeAdjacent > :first-child:last-child .feature button {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:first-child > button.feature:not(:first-child):not(:last-child),
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:first-child > .feature:not(:first-child):not(:last-child) button,
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:first-child > :not(:first-child):not(:last-child) button.feature,
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:first-child > :not(:first-child):not(:last-child) .feature button,
|
||||
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:last-child > button.feature:not(:first-child):not(:last-child),
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:last-child > .feature:not(:first-child):not(:last-child) button,
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:last-child > :not(:first-child):not(:last-child) button.feature,
|
||||
.col.mergeAdjacent > .table > .row.mergeAdjacent:last-child > :not(:first-child):not(:last-child) .feature button
|
||||
|
||||
.col.mergeAdjacent > .table:not(:first-child):not(:last-child) > .row.mergeAdjacent > button.feature,
|
||||
.col.mergeAdjacent > .table:not(:first-child):not(:last-child) > .row.mergeAdjacent > .feature button,
|
||||
.col.mergeAdjacent > .table:not(:first-child):not(:last-child) > .row.mergeAdjacent > * button.feature,
|
||||
.col.mergeAdjacent > .table:not(:first-child):not(:last-child) > .row.mergeAdjacent > * .feature button
|
||||
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:first-child > button.feature:not(:first-child):not(:last-child),
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:first-child > .feature:not(:first-child):not(:last-child) button,
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:first-child > :not(:first-child):not(:last-child) button.feature,
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:first-child > :not(:first-child):not(:last-child) .feature button,
|
||||
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:last-child > button.feature:not(:first-child):not(:last-child),
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:last-child > .feature:not(:first-child):not(:last-child) button,
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:last-child > :not(:first-child):not(:last-child) button.feature,
|
||||
.row.mergeAdjacent > .table > .col.mergeAdjacent:last-child > :not(:first-child):not(:last-child) .feature button
|
||||
|
||||
.row.mergeAdjacent > .table:not(:first-child):not(:last-child) > .col.mergeAdjacent > button.feature,
|
||||
.row.mergeAdjacent > .table:not(:first-child):not(:last-child) > .col.mergeAdjacent > .feature button,
|
||||
.row.mergeAdjacent > .table:not(:first-child):not(:last-child) > .col.mergeAdjacent > * button.feature,
|
||||
.row.mergeAdjacent > .table:not(:first-child):not(:last-child) > .col.mergeAdjacent > * .feature button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > button.feature:first-child,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > .feature:first-child button,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > :first-child button.feature,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > :first-child .feature button,
|
||||
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > button.feature:first-child,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > .feature:first-child button,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > :first-child button.feature,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > :first-child .feature button {
|
||||
border-radius: var(--border-radius) 0 0 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > button.feature:last-child,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > .feature:last-child button,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > :last-child button.feature,
|
||||
.col.mergeAdjacent > .table:first-child > .row.mergeAdjacent > :last-child .feature button,
|
||||
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > button.feature:last-child,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > .feature:last-child button,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > :last-child button.feature,
|
||||
.row.mergeAdjacent > .table:first-child > .col.mergeAdjacent > :last-child .feature button {
|
||||
border-radius: 0 var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > button.feature:last-child,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > .feature:last-child button,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > :last-child button.feature,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > :last-child .feature button,
|
||||
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > button.feature:last-child,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > .feature:last-child button,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > :last-child button.feature,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > :last-child .feature button {
|
||||
border-radius: 0 0 var(--border-radius) 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > button.feature:first-child,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > .feature:first-child button,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > :first-child button.feature,
|
||||
.col.mergeAdjacent > .table:last-child > .row.mergeAdjacent > :first-child .feature button,
|
||||
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > button.feature:first-child,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > .feature:first-child button,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > :first-child button.feature,
|
||||
.row.mergeAdjacent > .table:last-child > .col.mergeAdjacent > :first-child .feature button {
|
||||
border-radius: 0 0 0 var(--border-radius);
|
||||
}
|
||||
|
||||
.row-grid.mergeAdjacent > .feature:not(.dontMerge),
|
||||
.row-grid.mergeAdjacent > .tooltip-container > .feature:not(.dontMerge) {
|
||||
margin-left: 0;
|
||||
|
@ -91,38 +210,3 @@
|
|||
.table-grid > .row-grid.mergeAdjacent:last-child > .feature:not(.dontMerge):last-child {
|
||||
border-radius: 0 0 var(--border-radius) 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
TODO how to implement mergeAdjacent for grids?
|
||||
.row.mergeAdjacent + .row.mergeAdjacent > .feature:not(.dontMerge) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
.col.mergeAdjacent .feature:not(.dontMerge) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent .feature:not(.dontMerge):first-child {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.col.mergeAdjacent .feature:not(.dontMerge):last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.col.mergeAdjacent .feature:not(.dontMerge):first-child:last-child {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/*
|
||||
TODO how to implement mergeAdjacent for grids?
|
||||
.col.mergeAdjacent + .col.mergeAdjacent > .feature:not(.dontMerge) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
<template>
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="titleComponent"><component :is="titleComponent" /></span>
|
||||
<span class="field-title" v-if="title"><Title /></span>
|
||||
<VueNextSelect
|
||||
:options="options"
|
||||
v-model="value"
|
||||
@update:model-value="onUpdate"
|
||||
:min="1"
|
||||
label-by="label"
|
||||
:placeholder="placeholder"
|
||||
:close-on-select="closeOnSelect"
|
||||
@update:model-value="onUpdate"
|
||||
label-by="label"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import "components/common/fields.css";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import { ref, toRef, unref, watch } from "vue";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import { MaybeRef, ref, toRef, unref, 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<{
|
||||
title?: CoercableComponent;
|
||||
title?: MaybeRef<Renderable>;
|
||||
modelValue?: unknown;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
|
@ -34,7 +33,7 @@ const emit = defineEmits<{
|
|||
(e: "update:modelValue", value: unknown): void;
|
||||
}>();
|
||||
|
||||
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
|
||||
const Title = () => props.title ? render(props.title, el => <span>{el}</span>) : <></>;
|
||||
|
||||
const value = ref<SelectOption | null>(
|
||||
props.options.find(option => option.value === props.modelValue) ?? null
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import "components/common/fields.css";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import Tooltip from "wrappers/tooltips/Tooltip.vue";
|
||||
import { Direction } from "util/common";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<span class="field-title" v-if="titleComponent"
|
||||
><component :is="titleComponent"
|
||||
/></span>
|
||||
<span class="field-title" v-if="title">
|
||||
<Title />
|
||||
</span>
|
||||
<VueTextareaAutosize
|
||||
v-if="textArea"
|
||||
v-model="value"
|
||||
|
@ -25,15 +25,14 @@
|
|||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import "components/common/fields.css";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import { computed, onMounted, shallowRef, toRef, unref } from "vue";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import { computed, MaybeRef, onMounted, shallowRef, unref } from "vue";
|
||||
import VueTextareaAutosize from "vue-textarea-autosize";
|
||||
|
||||
const props = defineProps<{
|
||||
title?: CoercableComponent;
|
||||
title?: MaybeRef<Renderable>;
|
||||
modelValue?: string;
|
||||
textArea?: boolean;
|
||||
placeholder?: string;
|
||||
|
@ -46,7 +45,7 @@ const emit = defineEmits<{
|
|||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
|
||||
const Title = () => props.title == null ? <></> : render(props.title, el => <span>{el}</span>);
|
||||
|
||||
const field = shallowRef<HTMLElement | null>(null);
|
||||
onMounted(() => {
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
<template>
|
||||
<label class="field">
|
||||
<input type="checkbox" class="toggle" v-model="value" />
|
||||
<component :is="component" />
|
||||
<Component />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import "components/common/fields.css";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { coerceComponent } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import { computed, MaybeRef } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
title?: CoercableComponent;
|
||||
title?: MaybeRef<Renderable>;
|
||||
modelValue?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const component = computed(() => coerceComponent(unref(props.title) ?? "<span></span>", "span"));
|
||||
const Component = () => render(props.title ?? "", el => <span>{el}</span>);
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
<template>
|
||||
<Col class="collapsible-container">
|
||||
<button @click="collapsed.value = !collapsed.value" class="feature collapsible-toggle">
|
||||
<component :is="displayComponent" />
|
||||
<Display />
|
||||
</button>
|
||||
<component v-if="!collapsed.value" :is="contentComponent" />
|
||||
<Content v-if="!collapsed.value" />
|
||||
</Col>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { computeComponent } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { toRef } from "vue";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import type { MaybeRef, Ref } from "vue";
|
||||
import Col from "./Column.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
collapsed: Ref<boolean>;
|
||||
display: CoercableComponent;
|
||||
content: CoercableComponent;
|
||||
display: MaybeRef<Renderable>;
|
||||
content: MaybeRef<Renderable>;
|
||||
}>();
|
||||
|
||||
const displayComponent = computeComponent(toRef(props, "display"));
|
||||
const contentComponent = computeComponent(toRef(props, "content"));
|
||||
const Display = () => render(props.display);
|
||||
const Content = () => render(props.content);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -12,5 +12,10 @@ import themes from "data/themes";
|
|||
import settings from "game/settings";
|
||||
import { computed } from "vue";
|
||||
|
||||
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
|
||||
const props = defineProps<{
|
||||
dontMerge?: boolean
|
||||
}>();
|
||||
|
||||
const mergeAdjacent = computed(() =>
|
||||
themes[settings.theme].mergeAdjacent && props.dontMerge !== true);
|
||||
</script>
|
||||
|
|
|
@ -12,5 +12,10 @@ import themes from "data/themes";
|
|||
import settings from "game/settings";
|
||||
import { computed } from "vue";
|
||||
|
||||
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
|
||||
const props = defineProps<{
|
||||
dontMerge?: boolean
|
||||
}>();
|
||||
|
||||
const mergeAdjacent = computed(() =>
|
||||
themes[settings.theme].mergeAdjacent && props.dontMerge !== true);
|
||||
</script>
|
||||
|
|
|
@ -44,6 +44,7 @@ import settings from "game/settings";
|
|||
import state from "game/state";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
watchEffect(() => {
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
<br />
|
||||
<div>Time Played: {{ timePlayed }}</div>
|
||||
<component :is="infoComponent" />
|
||||
<Info />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@ -62,11 +62,10 @@
|
|||
<script setup lang="tsx">
|
||||
import type Changelog from "data/Changelog.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { jsx } from "features/feature";
|
||||
import player from "game/player";
|
||||
import { infoComponents } from "game/settings";
|
||||
import { formatTime } from "util/bignum";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { render } from "util/vue";
|
||||
import { computed, ref } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
|
@ -78,9 +77,7 @@ const isOpen = ref(false);
|
|||
|
||||
const timePlayed = computed(() => formatTime(player.timePlayed));
|
||||
|
||||
const infoComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => (<>{infoComponents.map(render)}</>)));
|
||||
});
|
||||
const Info = () => infoComponents.map(f => render(f));
|
||||
|
||||
defineExpose({
|
||||
open() {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
<div v-if="isTab('appearance')">
|
||||
<Select :title="themeTitle" :options="themes" v-model="theme" />
|
||||
<component :is="settingFieldsComponent" />
|
||||
<SettingFields />
|
||||
<Toggle :title="showTPSTitle" v-model="showTPS" />
|
||||
<Toggle :title="alignModifierUnitsTitle" v-model="alignUnits" />
|
||||
</div>
|
||||
|
@ -31,14 +31,13 @@
|
|||
<script setup lang="tsx">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import rawThemes from "data/themes";
|
||||
import { jsx } from "features/feature";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import player from "game/player";
|
||||
import settings, { settingFields } from "game/settings";
|
||||
import { camelToTitle, Direction } from "util/common";
|
||||
import { save } from "util/save";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { render } from "util/vue";
|
||||
import { computed, ref, toRefs } from "vue";
|
||||
import Tooltip from "wrappers/tooltips/Tooltip.vue";
|
||||
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
|
@ -69,9 +68,7 @@ const themes = Object.keys(rawThemes).map(theme => ({
|
|||
value: theme
|
||||
}));
|
||||
|
||||
const settingFieldsComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
||||
});
|
||||
const SettingFields = () => settingFields.map(f => render(f));
|
||||
|
||||
const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
|
||||
const { autosave, offlineProd } = toRefs(player);
|
||||
|
@ -84,54 +81,38 @@ const isPaused = computed({
|
|||
}
|
||||
});
|
||||
|
||||
const unthrottledTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Unthrottled
|
||||
<desc>Allow the game to run as fast as possible. Not battery friendly.</desc>
|
||||
</span>
|
||||
));
|
||||
const offlineProdTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const showHealthWarningTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Show videogame addiction warning
|
||||
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
|
||||
</span>
|
||||
));
|
||||
const autosaveTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Automatically save the game every second or when the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const isPausedTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Stop everything from moving.</desc>
|
||||
</span>
|
||||
));
|
||||
const themeTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Theme
|
||||
<desc>How the game looks.</desc>
|
||||
</span>
|
||||
));
|
||||
const showTPSTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Show TPS
|
||||
<desc>Show TPS meter at the bottom-left corner of the page.</desc>
|
||||
</span>
|
||||
));
|
||||
const alignModifierUnitsTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Align modifier units
|
||||
<desc>Align numbers to the beginning of the unit in modifier view.</desc>
|
||||
</span>
|
||||
));
|
||||
const unthrottledTitle = <span class="option-title">
|
||||
Unthrottled
|
||||
<desc>Allow the game to run as fast as possible. Not battery friendly.</desc>
|
||||
</span>;
|
||||
const offlineProdTitle = <span class="option-title">
|
||||
Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||
</span>;
|
||||
const showHealthWarningTitle = <span class="option-title">
|
||||
Show videogame addiction warning
|
||||
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
|
||||
</span>;
|
||||
const autosaveTitle = <span class="option-title">
|
||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Automatically save the game every second or when the game is closed.</desc>
|
||||
</span>;
|
||||
const isPausedTitle = <span class="option-title">
|
||||
Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
<desc>Stop everything from moving.</desc>
|
||||
</span>;
|
||||
const themeTitle = <span class="option-title">
|
||||
Theme
|
||||
<desc>How the game looks.</desc>
|
||||
</span>;
|
||||
const showTPSTitle = <span class="option-title">
|
||||
Show TPS
|
||||
<desc>Show TPS meter at the bottom-left corner of the page.</desc>
|
||||
</span>;
|
||||
const alignModifierUnitsTitle = <span class="option-title">
|
||||
Align modifier units
|
||||
<desc>Align numbers to the beginning of the unit in modifier view.</desc>
|
||||
</span>;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import Tooltip from "wrappers/tooltips/Tooltip.vue";
|
||||
import player from "game/player";
|
||||
import { Direction } from "util/common";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
|
|
|
@ -136,9 +136,7 @@ let bank = ref(
|
|||
acc.push({
|
||||
// .slice(2, -4) strips the leading ./ and the trailing .txt
|
||||
label: curr.split("/").slice(-1)[0].slice(0, -4),
|
||||
// Have to perform this unholy cast because globEager's typing doesn't appear to know
|
||||
// adding { as: "raw" } will make the object contain strings rather than modules
|
||||
value: bankContext[curr] as unknown as string
|
||||
value: bankContext[curr] as string
|
||||
});
|
||||
return acc;
|
||||
}, [])
|
||||
|
@ -207,7 +205,7 @@ function deleteSave(id: string) {
|
|||
if (galaxy.value?.loggedIn === true) {
|
||||
galaxy.value.getSaveList().then(list => {
|
||||
const slot = Object.keys(list).find(slot => {
|
||||
const content = list[slot as unknown as number].content;
|
||||
const content = list[parseInt(slot)].content;
|
||||
try {
|
||||
if (JSON.parse(content).id === id) {
|
||||
return true;
|
||||
|
|
|
@ -1,32 +1,25 @@
|
|||
import Collapsible from "components/layout/Collapsible.vue";
|
||||
import { GenericAchievement } from "features/achievements/achievement";
|
||||
import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable";
|
||||
import { Achievement } from "features/achievements/achievement";
|
||||
import type { Clickable, ClickableOptions } from "features/clickables/clickable";
|
||||
import { createClickable } from "features/clickables/clickable";
|
||||
import type { GenericConversion } from "features/conversion";
|
||||
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
||||
import { jsx, setDefault } from "features/feature";
|
||||
import { Resource, displayResource } from "features/resources/resource";
|
||||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||
import { Conversion } from "features/conversion";
|
||||
import { getFirstFeature, type OptionsFunc, type Replace } from "features/feature";
|
||||
import { displayResource, Resource } from "features/resources/resource";
|
||||
import type { Tree, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||
import { createTreeNode } from "features/trees/tree";
|
||||
import type { GenericFormula } from "game/formulas/types";
|
||||
import { BaseLayer } from "game/layers";
|
||||
import type { Modifier } from "game/modifiers";
|
||||
import { Modifier } from "game/modifiers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatSmall, formatTime } from "util/bignum";
|
||||
import { WithRequired, camelToTitle } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { convertComputable, processComputable } from "util/computed";
|
||||
import { getFirstFeature, renderColJSX, renderJSX } from "util/vue";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { WithRequired } from "util/common";
|
||||
import { processGetter } from "util/computed";
|
||||
import { render, Renderable, renderCol } from "util/vue";
|
||||
import type { ComputedRef, MaybeRef, MaybeRefOrGetter } from "vue";
|
||||
import { computed, ref, unref } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
import "./common.css";
|
||||
|
@ -34,32 +27,32 @@ import "./common.css";
|
|||
/** An object that configures a {@link ResetButton} */
|
||||
export interface ResetButtonOptions extends ClickableOptions {
|
||||
/** The conversion the button uses to calculate how much resources will be gained on click */
|
||||
conversion: GenericConversion;
|
||||
conversion: Conversion;
|
||||
/** The tree this reset button is apart of */
|
||||
tree: GenericTree;
|
||||
tree: Tree;
|
||||
/** The specific tree node associated with this reset button */
|
||||
treeNode: GenericTreeNode;
|
||||
treeNode: TreeNode;
|
||||
/**
|
||||
* Text to display on low conversion amounts, describing what "resetting" is in this context.
|
||||
* Defaults to "Reset for ".
|
||||
*/
|
||||
resetDescription?: Computable<string>;
|
||||
resetDescription?: MaybeRefOrGetter<string>;
|
||||
/** Whether or not to show how much currency would be required to make the gain amount increase. */
|
||||
showNextAt?: Computable<boolean>;
|
||||
showNextAt?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* The content to display on the button.
|
||||
* By default, this includes the reset description, and amount of currency to be gained.
|
||||
*/
|
||||
display?: Computable<CoercableComponent>;
|
||||
display?: MaybeRefOrGetter<Renderable>;
|
||||
/**
|
||||
* Whether or not this button can currently be clicked.
|
||||
* Defaults to checking the current gain amount is greater than {@link minimumGain}
|
||||
*/
|
||||
canClick?: Computable<boolean>;
|
||||
canClick?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* When {@link canClick} is left to its default, minimumGain is used to only enable the reset button when a sufficient amount of currency to gain is available.
|
||||
*/
|
||||
minimumGain?: Computable<DecimalSource>;
|
||||
minimumGain?: MaybeRefOrGetter<DecimalSource>;
|
||||
/** A persistent ref to track how much time has passed since the last time this tree node was reset. */
|
||||
resetTime?: Persistent<DecimalSource>;
|
||||
}
|
||||
|
@ -69,27 +62,12 @@ export interface ResetButtonOptions extends ClickableOptions {
|
|||
* It will show how much can be converted currently, and can show when that amount will go up, as well as handle only being clickable when a sufficient amount of currency can be gained.
|
||||
* Assumes this button is associated with a specific node on a tree, and triggers that tree's reset propagation.
|
||||
*/
|
||||
export type ResetButton<T extends ResetButtonOptions> = Replace<
|
||||
Clickable<T>,
|
||||
export type ResetButton = Replace<
|
||||
Clickable,
|
||||
{
|
||||
resetDescription: GetComputableTypeWithDefault<T["resetDescription"], Ref<string>>;
|
||||
showNextAt: GetComputableTypeWithDefault<T["showNextAt"], true>;
|
||||
display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>;
|
||||
minimumGain: GetComputableTypeWithDefault<T["minimumGain"], 1>;
|
||||
onClick: (event?: MouseEvent | TouchEvent) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link ResetButton} object. */
|
||||
export type GenericResetButton = Replace<
|
||||
GenericClickable & ResetButton<ResetButtonOptions>,
|
||||
{
|
||||
resetDescription: ProcessedComputable<string>;
|
||||
showNextAt: ProcessedComputable<boolean>;
|
||||
display: ProcessedComputable<CoercableComponent>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
minimumGain: ProcessedComputable<DecimalSource>;
|
||||
resetDescription: MaybeRef<string>;
|
||||
showNextAt: MaybeRef<boolean>;
|
||||
minimumGain: MaybeRef<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -99,78 +77,87 @@ export type GenericResetButton = Replace<
|
|||
*/
|
||||
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ResetButton<T> {
|
||||
return createClickable(feature => {
|
||||
const resetButton = optionsFunc.call(feature, feature);
|
||||
) {
|
||||
const resetButton = createClickable(feature => {
|
||||
const options = optionsFunc.call(feature, feature);
|
||||
const {
|
||||
conversion,
|
||||
tree,
|
||||
treeNode,
|
||||
resetTime,
|
||||
resetDescription,
|
||||
showNextAt,
|
||||
minimumGain,
|
||||
display,
|
||||
canClick,
|
||||
onClick,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
processComputable(resetButton as T, "showNextAt");
|
||||
setDefault(resetButton, "showNextAt", true);
|
||||
setDefault(resetButton, "minimumGain", 1);
|
||||
|
||||
if (resetButton.resetDescription == null) {
|
||||
resetButton.resetDescription = computed(() =>
|
||||
Decimal.lt(resetButton.conversion.gainResource.value, 1e3) ? "Reset for " : ""
|
||||
);
|
||||
} else {
|
||||
processComputable(resetButton as T, "resetDescription");
|
||||
}
|
||||
|
||||
if (resetButton.display == null) {
|
||||
resetButton.display = jsx(() => (
|
||||
<span>
|
||||
{unref(resetButton.resetDescription as ProcessedComputable<string>)}
|
||||
<b>
|
||||
{displayResource(
|
||||
resetButton.conversion.gainResource,
|
||||
Decimal.max(
|
||||
unref(resetButton.conversion.actualGain),
|
||||
unref(resetButton.minimumGain as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
)}
|
||||
</b>{" "}
|
||||
{resetButton.conversion.gainResource.displayName}
|
||||
{unref(resetButton.showNextAt as ProcessedComputable<boolean>) != null ? (
|
||||
<div>
|
||||
<br />
|
||||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||
return {
|
||||
...(props as Omit<typeof props, keyof ResetButtonOptions>),
|
||||
conversion,
|
||||
tree,
|
||||
treeNode,
|
||||
resetTime,
|
||||
resetDescription:
|
||||
processGetter(resetDescription) ??
|
||||
computed((): string =>
|
||||
Decimal.lt(conversion.gainResource.value, 1e3) ? "Reset for " : ""
|
||||
),
|
||||
showNextAt: processGetter(showNextAt) ?? true,
|
||||
minimumGain: processGetter(minimumGain) ?? 1,
|
||||
canClick:
|
||||
processGetter(canClick) ??
|
||||
computed((): boolean =>
|
||||
Decimal.gte(unref(conversion.actualGain), unref(resetButton.minimumGain))
|
||||
),
|
||||
display:
|
||||
processGetter(display) ??
|
||||
computed(() => (
|
||||
<span>
|
||||
{unref(resetButton.resetDescription)}
|
||||
<b>
|
||||
{displayResource(
|
||||
resetButton.conversion.baseResource,
|
||||
!unref(resetButton.conversion.buyMax) &&
|
||||
Decimal.gte(unref(resetButton.conversion.actualGain), 1)
|
||||
? unref(resetButton.conversion.currentAt)
|
||||
: unref(resetButton.conversion.nextAt)
|
||||
)}{" "}
|
||||
{resetButton.conversion.baseResource.displayName}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
||||
if (resetButton.canClick == null) {
|
||||
resetButton.canClick = computed(() =>
|
||||
Decimal.gte(
|
||||
unref(resetButton.conversion.actualGain),
|
||||
unref(resetButton.minimumGain as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const onClick = resetButton.onClick;
|
||||
resetButton.onClick = function (event?: MouseEvent | TouchEvent) {
|
||||
if (unref(resetButton.canClick) === false) {
|
||||
return;
|
||||
conversion.gainResource,
|
||||
Decimal.max(
|
||||
unref(conversion.actualGain),
|
||||
unref(resetButton.minimumGain)
|
||||
)
|
||||
)}
|
||||
</b>{" "}
|
||||
{conversion.gainResource.displayName}
|
||||
{unref(resetButton.showNextAt as MaybeRef<boolean>) != null ? (
|
||||
<div>
|
||||
<br />
|
||||
{unref<boolean>(conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||
{displayResource(
|
||||
conversion.baseResource,
|
||||
!unref<boolean>(conversion.buyMax) &&
|
||||
Decimal.gte(unref(conversion.actualGain), 1)
|
||||
? unref(conversion.currentAt)
|
||||
: unref(conversion.nextAt)
|
||||
)}{" "}
|
||||
{conversion.baseResource.displayName}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
)),
|
||||
onClick: function (e) {
|
||||
if (unref(resetButton.canClick) === false) {
|
||||
return;
|
||||
}
|
||||
conversion.convert();
|
||||
tree.reset(resetButton.treeNode);
|
||||
if (resetTime) {
|
||||
resetTime.value = resetTime[DefaultValue];
|
||||
}
|
||||
onClick?.call(resetButton, e);
|
||||
}
|
||||
resetButton.conversion.convert();
|
||||
resetButton.tree.reset(resetButton.treeNode);
|
||||
if (resetButton.resetTime) {
|
||||
resetButton.resetTime.value = resetButton.resetTime[DefaultValue];
|
||||
}
|
||||
onClick?.(event);
|
||||
};
|
||||
}) satisfies ResetButton;
|
||||
|
||||
return resetButton;
|
||||
}) as unknown as ResetButton<T>;
|
||||
return resetButton;
|
||||
}
|
||||
|
||||
/** An object that configures a {@link LayerTreeNode} */
|
||||
|
@ -178,27 +165,20 @@ export interface LayerTreeNodeOptions extends TreeNodeOptions {
|
|||
/** The ID of the layer this tree node is associated with */
|
||||
layerID: string;
|
||||
/** The color to display this tree node as */
|
||||
color: Computable<string>; // marking as required
|
||||
color: MaybeRefOrGetter<string>; // marking as required
|
||||
/** Whether or not to append the layer to the tabs list.
|
||||
* If set to false, then the tree node will instead always remove all tabs to its right and then add the layer tab.
|
||||
* Defaults to true.
|
||||
*/
|
||||
append?: Computable<boolean>;
|
||||
append?: MaybeRefOrGetter<boolean>;
|
||||
}
|
||||
|
||||
/** A tree node that is associated with a given layer, and which opens the layer when clicked. */
|
||||
export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
|
||||
TreeNode<T>,
|
||||
export type LayerTreeNode = Replace<
|
||||
TreeNode,
|
||||
{
|
||||
display: GetComputableTypeWithDefault<T["display"], T["layerID"]>;
|
||||
append: GetComputableType<T["append"]>;
|
||||
}
|
||||
>;
|
||||
/** A type that matches any valid {@link LayerTreeNode} object. */
|
||||
export type GenericLayerTreeNode = Replace<
|
||||
LayerTreeNode<LayerTreeNodeOptions>,
|
||||
{
|
||||
display: ProcessedComputable<CoercableComponent>;
|
||||
append?: ProcessedComputable<boolean>;
|
||||
layerID: string;
|
||||
append: MaybeRef<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -206,47 +186,50 @@ export type GenericLayerTreeNode = Replace<
|
|||
* Lazily creates a tree node that's associated with a specific layer, with the given options.
|
||||
* @param optionsFunc A function that returns the options object for this tree node.
|
||||
*/
|
||||
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): LayerTreeNode<T> {
|
||||
return createTreeNode(feature => {
|
||||
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(optionsFunc: OptionsFunc<T>) {
|
||||
const layerTreeNode = createTreeNode(feature => {
|
||||
const options = optionsFunc.call(feature, feature);
|
||||
setDefault(options, "display", camelToTitle(options.layerID));
|
||||
processComputable(options as T, "append");
|
||||
const { display, append, layerID, ...props } = options;
|
||||
|
||||
return {
|
||||
...options,
|
||||
onClick: unref((options as unknown as GenericLayerTreeNode).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);
|
||||
}
|
||||
...(props as Omit<typeof props, keyof LayerTreeNodeOptions>),
|
||||
layerID,
|
||||
display: processGetter(display) ?? layerID,
|
||||
append: processGetter(append) ?? true,
|
||||
onClick() {
|
||||
if (unref<boolean>(layerTreeNode.append)) {
|
||||
if (player.tabs.includes(layerID)) {
|
||||
const index = player.tabs.lastIndexOf(layerID);
|
||||
player.tabs.splice(index, 1);
|
||||
} else {
|
||||
player.tabs.push(layerID);
|
||||
}
|
||||
} else {
|
||||
player.tabs.splice(1, 1, layerID);
|
||||
}
|
||||
}
|
||||
};
|
||||
}) as unknown as LayerTreeNode<T>;
|
||||
}) satisfies LayerTreeNode;
|
||||
|
||||
return layerTreeNode;
|
||||
}
|
||||
|
||||
/** An option object for a modifier display as a single section. **/
|
||||
export interface Section {
|
||||
/** The header for this modifier. **/
|
||||
title: Computable<string>;
|
||||
title: MaybeRefOrGetter<string>;
|
||||
/** A subtitle for this modifier, e.g. to explain the context for the modifier. **/
|
||||
subtitle?: Computable<string>;
|
||||
subtitle?: MaybeRefOrGetter<string>;
|
||||
/** The modifier to be displaying in this section. **/
|
||||
modifier: WithRequired<Modifier, "description">;
|
||||
/** The base value being modified. **/
|
||||
base?: Computable<DecimalSource>;
|
||||
base?: MaybeRefOrGetter<DecimalSource>;
|
||||
/** The unit of measurement for the base. **/
|
||||
unit?: string;
|
||||
/** The label to call the base amount. Defaults to "Base". **/
|
||||
baseText?: Computable<CoercableComponent>;
|
||||
baseText?: MaybeRefOrGetter<Renderable>;
|
||||
/** Whether or not this section should be currently visible to the player. **/
|
||||
visible?: Computable<boolean>;
|
||||
visible?: MaybeRefOrGetter<boolean>;
|
||||
/** Determines if numbers larger or smaller than the base should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
@ -258,33 +241,33 @@ export interface Section {
|
|||
*/
|
||||
export function createCollapsibleModifierSections(
|
||||
sectionsFunc: () => Section[]
|
||||
): [JSXFunction, Persistent<Record<number, boolean>>] {
|
||||
): [MaybeRef<Renderable>, Persistent<Record<number, boolean>>] {
|
||||
const sections: Section[] = [];
|
||||
const processed:
|
||||
| {
|
||||
base: ProcessedComputable<DecimalSource | undefined>[];
|
||||
baseText: ProcessedComputable<CoercableComponent | undefined>[];
|
||||
visible: ProcessedComputable<boolean | undefined>[];
|
||||
title: ProcessedComputable<string | undefined>[];
|
||||
subtitle: ProcessedComputable<string | undefined>[];
|
||||
base: MaybeRef<DecimalSource | undefined>[];
|
||||
baseText: (MaybeRef<Renderable> | undefined)[];
|
||||
visible: MaybeRef<boolean | undefined>[];
|
||||
title: MaybeRef<string | undefined>[];
|
||||
subtitle: MaybeRef<string | undefined>[];
|
||||
}
|
||||
| Record<string, never> = {};
|
||||
let calculated = false;
|
||||
function calculateSections() {
|
||||
if (!calculated) {
|
||||
sections.push(...sectionsFunc());
|
||||
processed.base = sections.map(s => convertComputable(s.base));
|
||||
processed.baseText = sections.map(s => convertComputable(s.baseText));
|
||||
processed.visible = sections.map(s => convertComputable(s.visible));
|
||||
processed.title = sections.map(s => convertComputable(s.title));
|
||||
processed.subtitle = sections.map(s => convertComputable(s.subtitle));
|
||||
processed.base = sections.map(s => processGetter(s.base));
|
||||
processed.baseText = sections.map(s => processGetter(s.baseText));
|
||||
processed.visible = sections.map(s => processGetter(s.visible));
|
||||
processed.title = sections.map(s => processGetter(s.title));
|
||||
processed.subtitle = sections.map(s => processGetter(s.subtitle));
|
||||
calculated = true;
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
const collapsed = persistent<Record<number, boolean>>({}, false);
|
||||
const jsxFunc = jsx(() => {
|
||||
const jsxFunc = computed(() => {
|
||||
const sections = calculateSections();
|
||||
|
||||
let firstVisibleSection = true;
|
||||
|
@ -311,16 +294,14 @@ export function createCollapsibleModifierSections(
|
|||
<>
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">
|
||||
{renderJSX(unref(processed.baseText[i]) ?? "Base")}
|
||||
{render(unref(processed.baseText[i]) ?? "Base")}
|
||||
</span>
|
||||
<span class="modifier-amount">
|
||||
{format(unref(processed.base[i]) ?? 1)}
|
||||
{s.unit}
|
||||
</span>
|
||||
</div>
|
||||
{s.modifier.description == null
|
||||
? null
|
||||
: renderJSX(unref(s.modifier.description))}
|
||||
{s.modifier.description == null ? null : render(unref(s.modifier.description))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -383,7 +364,7 @@ export function colorText(textToColor: string, color = "var(--accent2)"): JSX.El
|
|||
* Creates a collapsible display of a list of achievements
|
||||
* @param achievements A dictionary of the achievements to display, inserted in the order from easiest to hardest
|
||||
*/
|
||||
export function createCollapsibleAchievements(achievements: Record<string, GenericAchievement>) {
|
||||
export function createCollapsibleAchievements(achievements: Record<string, Achievement>) {
|
||||
// Achievements are typically defined from easiest to hardest, and we want to show hardest first
|
||||
const orderedAchievements = Object.values(achievements).reverse();
|
||||
const collapseAchievements = persistent<boolean>(true, false);
|
||||
|
@ -394,25 +375,23 @@ export function createCollapsibleAchievements(achievements: Record<string, Gener
|
|||
orderedAchievements,
|
||||
m => m.earned.value
|
||||
);
|
||||
const display = jsx(() => {
|
||||
const display = computed(() => {
|
||||
const achievementsToDisplay = [...lockedAchievements.value];
|
||||
if (firstFeature.value) {
|
||||
achievementsToDisplay.push(firstFeature.value);
|
||||
}
|
||||
return renderColJSX(
|
||||
return renderCol(
|
||||
...achievementsToDisplay,
|
||||
jsx(() => (
|
||||
<Collapsible
|
||||
collapsed={collapseAchievements}
|
||||
content={collapsedContent}
|
||||
display={
|
||||
collapseAchievements.value
|
||||
? "Show other completed achievements"
|
||||
: "Hide other completed achievements"
|
||||
}
|
||||
v-show={unref(hasCollapsedContent)}
|
||||
/>
|
||||
))
|
||||
<Collapsible
|
||||
collapsed={collapseAchievements}
|
||||
content={collapsedContent}
|
||||
display={
|
||||
collapseAchievements.value
|
||||
? "Show other completed achievements"
|
||||
: "Hide other completed achievements"
|
||||
}
|
||||
v-show={unref(hasCollapsedContent)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return {
|
||||
|
@ -429,11 +408,11 @@ export function createCollapsibleAchievements(achievements: Record<string, Gener
|
|||
*/
|
||||
export function estimateTime(
|
||||
resource: Resource,
|
||||
rate: Computable<DecimalSource>,
|
||||
target: Computable<DecimalSource>
|
||||
rate: MaybeRefOrGetter<DecimalSource>,
|
||||
target: MaybeRefOrGetter<DecimalSource>
|
||||
) {
|
||||
const processedRate = convertComputable(rate);
|
||||
const processedTarget = convertComputable(target);
|
||||
const processedRate = processGetter(rate);
|
||||
const processedTarget = processGetter(target);
|
||||
return computed(() => {
|
||||
const currRate = unref(processedRate);
|
||||
const currTarget = unref(processedTarget);
|
||||
|
@ -455,15 +434,15 @@ export function estimateTime(
|
|||
*/
|
||||
export function createFormulaPreview(
|
||||
formula: GenericFormula,
|
||||
showPreview: Computable<boolean>,
|
||||
previewAmount: Computable<DecimalSource> = 1
|
||||
showPreview: MaybeRefOrGetter<boolean>,
|
||||
previewAmount: MaybeRefOrGetter<DecimalSource> = 1
|
||||
) {
|
||||
const processedShowPreview = convertComputable(showPreview);
|
||||
const processedPreviewAmount = convertComputable(previewAmount);
|
||||
const processedShowPreview = processGetter(showPreview);
|
||||
const processedPreviewAmount = processGetter(previewAmount);
|
||||
if (!formula.hasVariable()) {
|
||||
console.error("Cannot create formula preview if the formula does not have a variable");
|
||||
}
|
||||
return jsx(() => {
|
||||
return computed(() => {
|
||||
if (unref(processedShowPreview)) {
|
||||
const curr = formatSmall(formula.evaluate());
|
||||
const preview = formatSmall(
|
||||
|
|
|
@ -4,16 +4,21 @@
|
|||
*/
|
||||
import { main } from "data/projEntry";
|
||||
import { createCumulativeConversion } from "features/conversion";
|
||||
import { jsx } from "features/feature";
|
||||
import { createHotkey } from "features/hotkey";
|
||||
import { bonusAmountMixin } from "mixins/bonusDecorator";
|
||||
import { createRepeatable } from "features/clickables/repeatable";
|
||||
import { createReset } from "features/reset";
|
||||
import MainDisplay from "features/resources/MainDisplay.vue";
|
||||
import { createResource } from "features/resources/resource";
|
||||
import { addTooltip } from "features/tooltips/tooltip";
|
||||
import { addTooltip } from "wrappers/tooltips/tooltip";
|
||||
import { createResourceTooltip } from "features/trees/tree";
|
||||
import { createUpgrade } from "features/clickables/upgrade";
|
||||
import { BaseLayer, createLayer } from "game/layers";
|
||||
import { noPersist, persistent } from "game/persistence";
|
||||
import { createCostRequirement } from "game/requirements";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { render } from "util/vue";
|
||||
import { render, renderCol, renderRow } from "util/vue";
|
||||
import { computed } from "vue";
|
||||
import { createLayerTreeNode, createResetButton } from "../common";
|
||||
|
||||
const id = "p";
|
||||
|
@ -37,10 +42,10 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
color,
|
||||
reset
|
||||
}));
|
||||
const tooltip = addTooltip(treeNode, {
|
||||
const tooltip = addTooltip(treeNode, () => ({
|
||||
display: createResourceTooltip(points),
|
||||
pinnable: true
|
||||
});
|
||||
}));
|
||||
|
||||
const resetButton = createResetButton(() => ({
|
||||
conversion,
|
||||
|
@ -51,7 +56,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
const hotkey = createHotkey(() => ({
|
||||
description: "Reset for prestige points",
|
||||
key: "p",
|
||||
onPress: resetButton.onClick
|
||||
onPress: resetButton.onClick!
|
||||
}));
|
||||
|
||||
return {
|
||||
|
@ -59,12 +64,12 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
color,
|
||||
points,
|
||||
tooltip,
|
||||
display: jsx(() => (
|
||||
display: () => (
|
||||
<>
|
||||
<MainDisplay resource={points} color={color} />
|
||||
{render(resetButton)}
|
||||
</>
|
||||
)),
|
||||
),
|
||||
treeNode,
|
||||
hotkey
|
||||
};
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import Node from "components/Node.vue";
|
||||
import Spacer from "components/layout/Spacer.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource";
|
||||
import type { GenericTree } from "features/trees/tree";
|
||||
import { branchedResetPropagation, createTree } from "features/trees/tree";
|
||||
import { branchedResetPropagation, createTree, Tree } from "features/trees/tree";
|
||||
import { globalBus } from "game/events";
|
||||
import type { BaseLayer, GenericLayer } from "game/layers";
|
||||
import type { BaseLayer, Layer } from "game/layers";
|
||||
import { createLayer } from "game/layers";
|
||||
import type { Player } from "game/player";
|
||||
import player from "game/player";
|
||||
|
@ -42,12 +40,12 @@ export const main = createLayer("main", function (this: BaseLayer) {
|
|||
total.value = points.value;
|
||||
},
|
||||
resetPropagation: branchedResetPropagation
|
||||
})) as GenericTree;
|
||||
})) as Tree;
|
||||
|
||||
return {
|
||||
name: "Tree",
|
||||
links: tree.links,
|
||||
display: jsx(() => (
|
||||
display: () => (
|
||||
<>
|
||||
{player.devSpeed === 0 ? (
|
||||
<div>
|
||||
|
@ -81,7 +79,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
|
|||
<Spacer />
|
||||
{render(tree)}
|
||||
</>
|
||||
)),
|
||||
),
|
||||
points,
|
||||
best,
|
||||
total,
|
||||
|
@ -97,7 +95,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
|
|||
export const getInitialLayers = (
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
player: Partial<Player>
|
||||
): Array<GenericLayer> => [main, prestige];
|
||||
): Array<Layer> => [main, prestige];
|
||||
|
||||
/**
|
||||
* A computed ref whose value is true whenever the game is over.
|
||||
|
|
40
src/features/VueFeature.vue
Normal file
40
src/features/VueFeature.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div v-if="isVisible(unref(visibility))"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(unref(visibility)) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style)
|
||||
]"
|
||||
:class="{ feature: true, ...unref(classes) }"
|
||||
>
|
||||
<Components />
|
||||
<Node :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import Node from "components/Node.vue";
|
||||
import type { Visibility } from "features/feature";
|
||||
import { isHidden, isVisible } from "features/feature";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import { MaybeRef, unref, type CSSProperties } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
id: string;
|
||||
components: MaybeRef<Renderable>[];
|
||||
wrappers: ((el: () => Renderable) => Renderable)[];
|
||||
visibility?: MaybeRef<Visibility | boolean>;
|
||||
style?: MaybeRef<CSSProperties>;
|
||||
classes?: MaybeRef<Record<string, boolean>>;
|
||||
}>(), {
|
||||
visibility: true,
|
||||
style: () => ({}),
|
||||
classes: () => ({})
|
||||
});
|
||||
|
||||
const Components = () => props.wrappers.reduce<() => Renderable>(
|
||||
(acc, curr) => (() => curr(acc)),
|
||||
() => <>{props.components.map(el => render(el))}</>)();
|
||||
</script>
|
|
@ -1,85 +1,63 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined,
|
||||
backgroundImage: (earned && image && `url(${image})`) || ''
|
||||
},
|
||||
unref(style) ?? []
|
||||
]"
|
||||
:style="{
|
||||
backgroundImage: (unref(earned) && unref(image) && `url(${image})`) || ''
|
||||
}"
|
||||
:class="{
|
||||
feature: true,
|
||||
achievement: true,
|
||||
locked: !unref(earned),
|
||||
done: unref(earned),
|
||||
small: unref(small),
|
||||
...unref(classes)
|
||||
}"
|
||||
>
|
||||
<component v-if="comp" :is="comp" />
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
<Component />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import { displayRequirements, Requirements } from "game/requirements";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import { Component, shallowRef, StyleValue, unref, UnwrapRef, watchEffect } from "vue";
|
||||
import { GenericAchievement } from "./achievement";
|
||||
import { isJSXElement, render } from "util/vue";
|
||||
import { Component, isRef, unref } from "vue";
|
||||
import { Achievement } from "./achievement";
|
||||
import { displayRequirements } from "game/requirements";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
display?: UnwrapRef<GenericAchievement["display"]>;
|
||||
earned: boolean;
|
||||
requirements?: Requirements;
|
||||
image?: string;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
mark?: boolean | string;
|
||||
small?: boolean;
|
||||
id: string;
|
||||
display: Achievement["display"];
|
||||
earned: Achievement["earned"];
|
||||
requirements: Achievement["requirements"];
|
||||
image: Achievement["image"];
|
||||
small: Achievement["small"];
|
||||
}>();
|
||||
|
||||
const comp = shallowRef<Component | string>("");
|
||||
|
||||
watchEffect(() => {
|
||||
const currDisplay = props.display;
|
||||
if (currDisplay == null) {
|
||||
comp.value = "";
|
||||
return;
|
||||
}
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
comp.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const Requirement = coerceComponent(currDisplay.requirement ? currDisplay.requirement :
|
||||
jsx(() => displayRequirements(props.requirements ?? [])), "h3");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
|
||||
const OptionsDisplay = props.earned ?
|
||||
coerceComponent(currDisplay.optionsDisplay || "", "span") :
|
||||
"";
|
||||
comp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
const Component = () => {
|
||||
if (props.display == null) {
|
||||
return null;
|
||||
} else if (
|
||||
isRef(props.display) ||
|
||||
typeof props.display === "string" ||
|
||||
isJSXElement(props.display)
|
||||
) {
|
||||
return render(props.display);
|
||||
} else {
|
||||
const { requirement, effectDisplay, optionsDisplay } = props.display;
|
||||
return (
|
||||
<span>
|
||||
<Requirement />
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
{requirement ?
|
||||
render(requirement, el => <h3>{el}</h3>) :
|
||||
displayRequirements(props.requirements ?? [])}
|
||||
{effectDisplay ? (
|
||||
<div>
|
||||
<EffectDisplay />
|
||||
{render(effectDisplay, el => <b>{el}</b>)}
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.optionsDisplay != null ? (
|
||||
{optionsDisplay != null ? (
|
||||
<div class="equal-spaced">
|
||||
<OptionsDisplay />
|
||||
{render(optionsDisplay)}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
))
|
||||
);
|
||||
});
|
||||
</span>);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,45 +1,32 @@
|
|||
import { computed } from "vue";
|
||||
import Select from "components/fields/Select.vue";
|
||||
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
GatherProps,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue,
|
||||
Visibility,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
setDefault
|
||||
} from "features/feature";
|
||||
import { OptionsFunc, Replace, Visibility } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import "game/notifications";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import {
|
||||
Requirements,
|
||||
createBooleanRequirement,
|
||||
createVisibilityRequirement,
|
||||
displayRequirements,
|
||||
Requirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import settings, { registerSettingField } from "game/settings";
|
||||
import { camelToTitle } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import { unref, watchEffect } from "vue";
|
||||
import {
|
||||
isJSXElement,
|
||||
render,
|
||||
Renderable,
|
||||
VueFeature,
|
||||
vueFeatureMixin,
|
||||
VueFeatureOptions
|
||||
} from "util/vue";
|
||||
import { computed, isRef, MaybeRef, MaybeRefOrGetter, unref, watchEffect } from "vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
import Achievement from "./Achievement.vue";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
|
@ -58,35 +45,26 @@ export enum AchievementDisplay {
|
|||
/**
|
||||
* An object that configures an {@link Achievement}.
|
||||
*/
|
||||
export interface AchievementOptions {
|
||||
/** Whether this achievement should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
export interface AchievementOptions extends VueFeatureOptions {
|
||||
/** The requirement(s) to earn this achievement. Can be left null if using {@link BaseAchievement.complete}. */
|
||||
requirements?: Requirements;
|
||||
/** The display to use for this achievement. */
|
||||
display?: Computable<
|
||||
| CoercableComponent
|
||||
display?:
|
||||
| MaybeRefOrGetter<Renderable>
|
||||
| {
|
||||
/** Description of the requirement(s) for this achievement. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
|
||||
requirement?: CoercableComponent;
|
||||
requirement?: MaybeRefOrGetter<Renderable>;
|
||||
/** Description of what will change (if anything) for achieving this. */
|
||||
effectDisplay?: CoercableComponent;
|
||||
effectDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
/** Any additional things to display on this achievement, such as a toggle for it's effect. */
|
||||
optionsDisplay?: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
optionsDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
};
|
||||
/** Toggles a smaller design for the feature. */
|
||||
small?: Computable<boolean>;
|
||||
small?: MaybeRefOrGetter<boolean>;
|
||||
/** An image to display as the background for this achievement. */
|
||||
image?: Computable<string>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
image?: MaybeRefOrGetter<string>;
|
||||
/** Whether or not to display a notification popup when this achievement is earned. */
|
||||
showPopups?: Computable<boolean>;
|
||||
showPopups?: MaybeRefOrGetter<boolean>;
|
||||
/** A function that is called when the achievement is completed. */
|
||||
onComplete?: VoidFunction;
|
||||
}
|
||||
|
@ -94,41 +72,28 @@ export interface AchievementOptions {
|
|||
/**
|
||||
* The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
|
||||
*/
|
||||
export interface BaseAchievement {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
export interface BaseAchievement extends VueFeature {
|
||||
/** Whether or not this achievement has been earned. */
|
||||
earned: Persistent<boolean>;
|
||||
/** A function to complete this achievement. */
|
||||
complete: VoidFunction;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof AchievementType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */
|
||||
export type Achievement<T extends AchievementOptions> = Replace<
|
||||
T & BaseAchievement,
|
||||
export type Achievement = Replace<
|
||||
Replace<AchievementOptions, BaseAchievement>,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
image: GetComputableType<T["image"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
showPopups: GetComputableTypeWithDefault<T["showPopups"], true>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Achievement} object. */
|
||||
export type GenericAchievement = Replace<
|
||||
Achievement<AchievementOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
showPopups: ProcessedComputable<boolean>;
|
||||
display?:
|
||||
| MaybeRef<Renderable>
|
||||
| {
|
||||
requirement?: MaybeRef<Renderable>;
|
||||
effectDisplay?: MaybeRef<Renderable>;
|
||||
optionsDisplay?: MaybeRef<Renderable>;
|
||||
};
|
||||
image: ProcessedRefOrGetter<AchievementOptions["image"]>;
|
||||
showPopups: MaybeRef<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -137,156 +102,116 @@ export type GenericAchievement = Replace<
|
|||
* @param optionsFunc Achievement options.
|
||||
*/
|
||||
export function createAchievement<T extends AchievementOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Achievement<T> {
|
||||
optionsFunc?: OptionsFunc<T, BaseAchievement, Achievement>
|
||||
) {
|
||||
const earned = persistent<boolean>(false, false);
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const achievement =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
achievement.id = getUniqueID("achievement-");
|
||||
achievement.type = AchievementType;
|
||||
achievement[Component] = AchievementComponent as GenericComponent;
|
||||
const options = optionsFunc?.call(feature, feature as Achievement) ?? ({} as T);
|
||||
const { requirements, display, small, image, showPopups, onComplete, ...props } = options;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(achievement);
|
||||
}
|
||||
const vueFeature = vueFeatureMixin("achievement", options, () => (
|
||||
<Achievement
|
||||
display={achievement.display}
|
||||
earned={achievement.earned}
|
||||
requirements={achievement.requirements}
|
||||
image={achievement.image}
|
||||
small={achievement.small}
|
||||
/>
|
||||
));
|
||||
|
||||
achievement.earned = earned;
|
||||
achievement.complete = function () {
|
||||
if (earned.value) {
|
||||
return;
|
||||
}
|
||||
earned.value = true;
|
||||
const genericAchievement = achievement as GenericAchievement;
|
||||
genericAchievement.onComplete?.();
|
||||
if (
|
||||
genericAchievement.display != null &&
|
||||
unref(genericAchievement.showPopups) === true
|
||||
) {
|
||||
const display = unref(genericAchievement.display);
|
||||
let Display;
|
||||
if (isCoercableComponent(display)) {
|
||||
Display = coerceComponent(display);
|
||||
} else if (display.requirement != null) {
|
||||
Display = coerceComponent(display.requirement);
|
||||
} else {
|
||||
Display = displayRequirements(genericAchievement.requirements ?? []);
|
||||
const achievement = {
|
||||
type: AchievementType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof AchievementOptions>),
|
||||
...vueFeature,
|
||||
visibility: computed(() => {
|
||||
const display = unref((achievement as Achievement).display);
|
||||
switch (settings.msDisplay) {
|
||||
default:
|
||||
case AchievementDisplay.All:
|
||||
return unref(vueFeature.visibility) ?? true;
|
||||
case AchievementDisplay.Configurable:
|
||||
if (
|
||||
unref(earned) &&
|
||||
!(
|
||||
display != null &&
|
||||
typeof display === "object" &&
|
||||
"optionsDisplay" in display
|
||||
)
|
||||
) {
|
||||
return Visibility.None;
|
||||
}
|
||||
return unref(vueFeature.visibility) ?? true;
|
||||
case AchievementDisplay.Incomplete:
|
||||
if (unref(earned)) {
|
||||
return Visibility.None;
|
||||
}
|
||||
return unref(vueFeature.visibility) ?? true;
|
||||
case AchievementDisplay.None:
|
||||
return Visibility.None;
|
||||
}
|
||||
toast.info(
|
||||
<div>
|
||||
<h3>Achievement earned!</h3>
|
||||
}),
|
||||
earned,
|
||||
onComplete,
|
||||
small: processGetter(small),
|
||||
image: processGetter(image),
|
||||
showPopups: processGetter(showPopups) ?? true,
|
||||
display:
|
||||
display == null
|
||||
? undefined
|
||||
: isRef(display) ||
|
||||
typeof display === "string" ||
|
||||
typeof display === "function" ||
|
||||
isJSXElement(display)
|
||||
? processGetter(display)
|
||||
: {
|
||||
requirement: processGetter(display.requirement),
|
||||
effectDisplay: processGetter(display.effectDisplay),
|
||||
optionsDisplay: processGetter(display.optionsDisplay)
|
||||
},
|
||||
requirements:
|
||||
requirements == null
|
||||
? undefined
|
||||
: [
|
||||
createVisibilityRequirement(vueFeature.visibility ?? true),
|
||||
createBooleanRequirement(() => !earned.value),
|
||||
...(Array.isArray(requirements) ? requirements : [requirements])
|
||||
],
|
||||
complete() {
|
||||
if (earned.value) {
|
||||
return;
|
||||
}
|
||||
earned.value = true;
|
||||
achievement.onComplete?.();
|
||||
if (achievement.display != null && unref(achievement.showPopups) === true) {
|
||||
const display = achievement.display;
|
||||
let Display;
|
||||
if (isRef(display) || typeof display === "string" || isJSXElement(display)) {
|
||||
Display = () => render(display);
|
||||
} else if (display.requirement != null) {
|
||||
Display = () => render(display.requirement!);
|
||||
} else {
|
||||
Display = () => displayRequirements(achievement.requirements ?? []);
|
||||
}
|
||||
toast.info(
|
||||
<div>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<Display />
|
||||
<h3>Achievement earned!</h3>
|
||||
<div>{Display}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
} satisfies Achievement;
|
||||
|
||||
Object.assign(achievement, decoratedData);
|
||||
|
||||
processComputable(achievement as T, "visibility");
|
||||
setDefault(achievement, "visibility", Visibility.Visible);
|
||||
const visibility = achievement.visibility as ProcessedComputable<Visibility | boolean>;
|
||||
achievement.visibility = computed(() => {
|
||||
const display = unref((achievement as GenericAchievement).display);
|
||||
switch (settings.msDisplay) {
|
||||
default:
|
||||
case AchievementDisplay.All:
|
||||
return unref(visibility);
|
||||
case AchievementDisplay.Configurable:
|
||||
if (
|
||||
unref(achievement.earned) &&
|
||||
!(
|
||||
display != null &&
|
||||
typeof display === "object" &&
|
||||
"optionsDisplay" in (display as Record<string, unknown>)
|
||||
)
|
||||
) {
|
||||
return Visibility.None;
|
||||
}
|
||||
return unref(visibility);
|
||||
case AchievementDisplay.Incomplete:
|
||||
if (unref(achievement.earned)) {
|
||||
return Visibility.None;
|
||||
}
|
||||
return unref(visibility);
|
||||
case AchievementDisplay.None:
|
||||
return Visibility.None;
|
||||
}
|
||||
});
|
||||
|
||||
processComputable(achievement as T, "display");
|
||||
processComputable(achievement as T, "mark");
|
||||
processComputable(achievement as T, "small");
|
||||
processComputable(achievement as T, "image");
|
||||
processComputable(achievement as T, "style");
|
||||
processComputable(achievement as T, "classes");
|
||||
processComputable(achievement as T, "showPopups");
|
||||
setDefault(achievement, "showPopups", true);
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(achievement);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(achievement)),
|
||||
{}
|
||||
);
|
||||
achievement[GatherProps] = function (this: GenericAchievement) {
|
||||
const {
|
||||
visibility,
|
||||
display,
|
||||
requirements,
|
||||
earned,
|
||||
image,
|
||||
style,
|
||||
classes,
|
||||
mark,
|
||||
small,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
visibility,
|
||||
display,
|
||||
requirements,
|
||||
earned,
|
||||
image,
|
||||
style: unref(style),
|
||||
classes,
|
||||
mark,
|
||||
small,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
if (achievement.requirements) {
|
||||
const genericAchievement = achievement as GenericAchievement;
|
||||
const requirements = [
|
||||
createVisibilityRequirement(genericAchievement),
|
||||
createBooleanRequirement(() => !genericAchievement.earned.value),
|
||||
...(Array.isArray(achievement.requirements)
|
||||
? achievement.requirements
|
||||
: [achievement.requirements])
|
||||
];
|
||||
if (achievement.requirements != null) {
|
||||
watchEffect(() => {
|
||||
if (settings.active !== player.id) return;
|
||||
if (requirementsMet(requirements)) {
|
||||
genericAchievement.complete();
|
||||
if (requirementsMet(achievement.requirements ?? [])) {
|
||||
achievement.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return achievement as unknown as Achievement<T>;
|
||||
return achievement;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -297,7 +222,7 @@ declare module "game/settings" {
|
|||
}
|
||||
|
||||
globalBus.on("loadSettings", settings => {
|
||||
setDefault(settings, "msDisplay", AchievementDisplay.All);
|
||||
settings.msDisplay ??= AchievementDisplay.All;
|
||||
});
|
||||
|
||||
const msDisplayOptions = Object.values(AchievementDisplay).map(option => ({
|
||||
|
@ -306,19 +231,17 @@ const msDisplayOptions = Object.values(AchievementDisplay).map(option => ({
|
|||
}));
|
||||
|
||||
globalBus.on("setupVue", () =>
|
||||
registerSettingField(
|
||||
jsx(() => (
|
||||
<Select
|
||||
title={jsx(() => (
|
||||
<span class="option-title">
|
||||
Show achievements
|
||||
<desc>Select which achievements to display based on criterias.</desc>
|
||||
</span>
|
||||
))}
|
||||
options={msDisplayOptions}
|
||||
onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
|
||||
modelValue={settings.msDisplay}
|
||||
/>
|
||||
))
|
||||
)
|
||||
registerSettingField(() => (
|
||||
<Select
|
||||
title={
|
||||
<span class="option-title">
|
||||
Show achievements
|
||||
<desc>Select which achievements to display based on criterias.</desc>
|
||||
</span>
|
||||
}
|
||||
options={msDisplayOptions}
|
||||
onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
|
||||
modelValue={settings.msDisplay}
|
||||
/>
|
||||
))
|
||||
);
|
||||
|
|
|
@ -1,292 +0,0 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import {
|
||||
Component,
|
||||
findFeatures,
|
||||
GatherProps,
|
||||
GenericComponent,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
JSXFunction,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
setDefault,
|
||||
StyleValue,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import { persistent } from "game/persistence";
|
||||
import Decimal, { DecimalSource } from "lib/break_eternity";
|
||||
import { Unsubscribe } from "nanoevents";
|
||||
import { Direction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent, render } from "util/vue";
|
||||
import { computed, Ref, ref, unref } from "vue";
|
||||
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
||||
import { ClickableOptions } from "./clickables/clickable";
|
||||
import { GenericDecorator } from "./decorators/common";
|
||||
|
||||
/** A symbol used to identify {@link Action} features. */
|
||||
export const ActionType = Symbol("Action");
|
||||
|
||||
/**
|
||||
* An object that configures an {@link Action}.
|
||||
*/
|
||||
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
|
||||
/** The cooldown during which the action cannot be performed again, in seconds. */
|
||||
duration: Computable<DecimalSource>;
|
||||
/** Whether or not the action should perform automatically when the cooldown is finished. */
|
||||
autoStart?: Computable<boolean>;
|
||||
/** A function that is called when the action is clicked. */
|
||||
onClick: (amount: DecimalSource) => void;
|
||||
/** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
|
||||
barOptions?: Partial<BarOptions>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link ActionOptions} to create an {@link Action}.
|
||||
*/
|
||||
export interface BaseAction {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ActionType;
|
||||
/** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
|
||||
isHolding: Ref<boolean>;
|
||||
/** The current amount of progress through the cooldown. */
|
||||
progress: Ref<DecimalSource>;
|
||||
/** The bar used to display the current cooldown progress. */
|
||||
progressBar: GenericBar;
|
||||
/** Update the cooldown the specified number of seconds */
|
||||
update: (diff: number) => void;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be clicked upon, and then has a cooldown before it can be clicked again. */
|
||||
export type Action<T extends ActionOptions> = Replace<
|
||||
T & BaseAction,
|
||||
{
|
||||
duration: GetComputableType<T["duration"]>;
|
||||
autoStart: GetComputableTypeWithDefault<T["autoStart"], false>;
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], true>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
display: JSXFunction;
|
||||
onClick: VoidFunction;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Action} object. */
|
||||
export type GenericAction = Replace<
|
||||
Action<ActionOptions>,
|
||||
{
|
||||
autoStart: ProcessedComputable<boolean>;
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an action with the given options.
|
||||
* @param optionsFunc Action options.
|
||||
*/
|
||||
export function createAction<T extends ActionOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Action<T> {
|
||||
const progress = persistent<DecimalSource>(0);
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const action =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
action.id = getUniqueID("action-");
|
||||
action.type = ActionType;
|
||||
action[Component] = ClickableComponent as GenericComponent;
|
||||
|
||||
// Required because of display changing types
|
||||
const genericAction = action as unknown as GenericAction;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(action);
|
||||
}
|
||||
|
||||
action.isHolding = ref(false);
|
||||
action.progress = progress;
|
||||
Object.assign(action, decoratedData);
|
||||
|
||||
processComputable(action as T, "visibility");
|
||||
setDefault(action, "visibility", Visibility.Visible);
|
||||
processComputable(action as T, "duration");
|
||||
processComputable(action as T, "autoStart");
|
||||
setDefault(action, "autoStart", false);
|
||||
processComputable(action as T, "canClick");
|
||||
setDefault(action, "canClick", true);
|
||||
processComputable(action as T, "classes");
|
||||
processComputable(action as T, "style");
|
||||
processComputable(action as T, "mark");
|
||||
processComputable(action as T, "display");
|
||||
|
||||
const style = action.style as ProcessedComputable<StyleValue | undefined>;
|
||||
action.style = computed(() => {
|
||||
const currStyle: StyleValue[] = [
|
||||
{
|
||||
cursor: Decimal.gte(
|
||||
progress.value,
|
||||
unref(action.duration as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
? "pointer"
|
||||
: "progress",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}
|
||||
];
|
||||
const originalStyle = unref(style);
|
||||
if (Array.isArray(originalStyle)) {
|
||||
currStyle.push(...originalStyle);
|
||||
} else if (originalStyle != null) {
|
||||
currStyle.push(originalStyle);
|
||||
}
|
||||
return currStyle as StyleValue;
|
||||
});
|
||||
|
||||
action.progressBar = createBar(() => ({
|
||||
direction: Direction.Right,
|
||||
width: 100,
|
||||
height: 10,
|
||||
borderStyle: "border-color: black",
|
||||
baseStyle: "margin-top: -1px",
|
||||
progress: () => Decimal.div(progress.value, unref(genericAction.duration)),
|
||||
...action.barOptions
|
||||
}));
|
||||
|
||||
const canClick = action.canClick as ProcessedComputable<boolean>;
|
||||
action.canClick = computed(
|
||||
() =>
|
||||
unref(canClick) &&
|
||||
Decimal.gte(
|
||||
progress.value,
|
||||
unref(action.duration as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
);
|
||||
|
||||
const display = action.display as GetComputableType<ClickableOptions["display"]>;
|
||||
action.display = jsx(() => {
|
||||
const currDisplay = unref(display);
|
||||
let Comp: GenericComponent | undefined;
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
Comp = coerceComponent(currDisplay);
|
||||
} else if (currDisplay != null) {
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
Comp = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
</span>
|
||||
))
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div style="flex-grow: 1" />
|
||||
{Comp == null ? null : <Comp />}
|
||||
<div style="flex-grow: 1" />
|
||||
{render(genericAction.progressBar)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const onClick = action.onClick.bind(action);
|
||||
action.onClick = function () {
|
||||
if (unref(action.canClick as ProcessedComputable<boolean>) === false) {
|
||||
return;
|
||||
}
|
||||
const amount = Decimal.div(progress.value, unref(genericAction.duration));
|
||||
onClick?.(amount);
|
||||
progress.value = 0;
|
||||
};
|
||||
|
||||
action.update = function (diff) {
|
||||
const duration = unref(genericAction.duration);
|
||||
if (Decimal.gte(progress.value, duration)) {
|
||||
progress.value = duration;
|
||||
} else {
|
||||
progress.value = Decimal.add(progress.value, diff);
|
||||
if (genericAction.isHolding.value || unref(genericAction.autoStart)) {
|
||||
genericAction.onClick();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(action);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(action)),
|
||||
{}
|
||||
);
|
||||
action[GatherProps] = function (this: GenericAction) {
|
||||
const {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
onClick,
|
||||
isHolding,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
isHolding,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return action as unknown as Action<T>;
|
||||
});
|
||||
}
|
||||
|
||||
const listeners: Record<string, Unsubscribe | undefined> = {};
|
||||
globalBus.on("addLayer", layer => {
|
||||
const actions: GenericAction[] = findFeatures(layer, ActionType) as GenericAction[];
|
||||
listeners[layer.id] = layer.on("postUpdate", diff => {
|
||||
actions.forEach(action => action.update(diff));
|
||||
});
|
||||
});
|
||||
globalBus.on("removeLayer", layer => {
|
||||
// unsubscribe from postUpdate
|
||||
listeners[layer.id]?.();
|
||||
listeners[layer.id] = undefined;
|
||||
});
|
|
@ -1,18 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
width: unref(width) + 'px',
|
||||
height: unref(height) + 'px',
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
:class="{
|
||||
bar: true,
|
||||
...unref(classes)
|
||||
:style="{
|
||||
width: unref(width) + 'px',
|
||||
height: unref(height) + 'px',
|
||||
}"
|
||||
class="bar"
|
||||
>
|
||||
<div
|
||||
class="overlayTextContainer border"
|
||||
|
@ -21,50 +13,41 @@
|
|||
unref(borderStyle) ?? {}
|
||||
]"
|
||||
>
|
||||
<span v-if="component" class="overlayText" :style="unref(textStyle)">
|
||||
<component :is="component" />
|
||||
<span v-if="display" class="overlayText" :style="unref(textStyle)">
|
||||
<Component />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="border"
|
||||
:style="[
|
||||
{ width: unref(width) + 'px', height: unref(height) + 'px' },
|
||||
unref(style) ?? {},
|
||||
unref(baseStyle) ?? {},
|
||||
unref(borderStyle) ?? {}
|
||||
]"
|
||||
>
|
||||
<div class="fill" :style="[barStyle, unref(style) ?? {}, unref(fillStyle) ?? {}]" />
|
||||
<div class="fill" :style="[barStyle, unref(fillStyle) ?? {}]" />
|
||||
</div>
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CoercableComponent, isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import type { CSSProperties, StyleValue } from "vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import { render } from "util/vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { Bar } from "./bar";
|
||||
|
||||
const props = defineProps<{
|
||||
progress: DecimalSource;
|
||||
width: number;
|
||||
height: number;
|
||||
direction: Direction;
|
||||
display?: CoercableComponent;
|
||||
visibility: Visibility | boolean;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
borderStyle?: StyleValue;
|
||||
textStyle?: StyleValue;
|
||||
baseStyle?: StyleValue;
|
||||
fillStyle?: StyleValue;
|
||||
mark?: boolean | string;
|
||||
id: string;
|
||||
width: Bar["width"];
|
||||
height: Bar["height"];
|
||||
direction: Bar["direction"];
|
||||
borderStyle: Bar["borderStyle"];
|
||||
baseStyle: Bar["baseStyle"];
|
||||
textStyle: Bar["textStyle"];
|
||||
fillStyle: Bar["fillStyle"];
|
||||
progress: Bar["progress"];
|
||||
display: Bar["display"];
|
||||
}>();
|
||||
|
||||
const normalizedProgress = computed(() => {
|
||||
|
@ -77,17 +60,17 @@ const normalizedProgress = computed(() => {
|
|||
|
||||
const barStyle = computed(() => {
|
||||
const barStyle: Partial<CSSProperties> = {
|
||||
width: props.width + 0.5 + "px",
|
||||
height: props.height + 0.5 + "px"
|
||||
width: unref(props.width) + 0.5 + "px",
|
||||
height: unref(props.height) + 0.5 + "px"
|
||||
};
|
||||
switch (props.direction) {
|
||||
case Direction.Up:
|
||||
barStyle.clipPath = `inset(${normalizedProgress.value}% 0% 0% 0%)`;
|
||||
barStyle.width = props.width + 1 + "px";
|
||||
barStyle.width = unref(props.width) + 1 + "px";
|
||||
break;
|
||||
case Direction.Down:
|
||||
barStyle.clipPath = `inset(0% 0% ${normalizedProgress.value}% 0%)`;
|
||||
barStyle.width = props.width + 1 + "px";
|
||||
barStyle.width = unref(props.width) + 1 + "px";
|
||||
break;
|
||||
case Direction.Right:
|
||||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||
|
@ -102,7 +85,7 @@ const barStyle = computed(() => {
|
|||
return barStyle;
|
||||
});
|
||||
|
||||
const component = computeOptionalComponent(toRef(props, "display"));
|
||||
const Component = () => props.display ? render(props.display) : null;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,185 +0,0 @@
|
|||
import BarComponent from "features/bars/Bar.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, Visibility, getUniqueID, setDefault } from "features/feature";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Bar} features. */
|
||||
export const BarType = Symbol("Bar");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Bar}.
|
||||
*/
|
||||
export interface BarOptions {
|
||||
/** Whether this bar should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The width of the bar. */
|
||||
width: Computable<number>;
|
||||
/** The height of the bar. */
|
||||
height: Computable<number>;
|
||||
/** The direction in which the bar progresses. */
|
||||
direction: Computable<Direction>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to the bar's border. */
|
||||
borderStyle?: Computable<StyleValue>;
|
||||
/** CSS to apply to the bar's base. */
|
||||
baseStyle?: Computable<StyleValue>;
|
||||
/** CSS to apply to the bar's text. */
|
||||
textStyle?: Computable<StyleValue>;
|
||||
/** CSS to apply to the bar's fill. */
|
||||
fillStyle?: Computable<StyleValue>;
|
||||
/** The progress value of the bar, from 0 to 1. */
|
||||
progress: Computable<DecimalSource>;
|
||||
/** The display to use for this bar. */
|
||||
display?: Computable<CoercableComponent>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link BarOptions} to create a {@link Bar}.
|
||||
*/
|
||||
export interface BaseBar {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof BarType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that displays some sort of progress or completion or resource with a cap. */
|
||||
export type Bar<T extends BarOptions> = Replace<
|
||||
T & BaseBar,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
width: GetComputableType<T["width"]>;
|
||||
height: GetComputableType<T["height"]>;
|
||||
direction: GetComputableType<T["direction"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
borderStyle: GetComputableType<T["borderStyle"]>;
|
||||
baseStyle: GetComputableType<T["baseStyle"]>;
|
||||
textStyle: GetComputableType<T["textStyle"]>;
|
||||
fillStyle: GetComputableType<T["fillStyle"]>;
|
||||
progress: GetComputableType<T["progress"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Bar} object. */
|
||||
export type GenericBar = Replace<
|
||||
Bar<BarOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a bar with the given options.
|
||||
* @param optionsFunc Bar options.
|
||||
*/
|
||||
export function createBar<T extends BarOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseBar, GenericBar>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Bar<T> {
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const bar = optionsFunc.call(feature, feature);
|
||||
bar.id = getUniqueID("bar-");
|
||||
bar.type = BarType;
|
||||
bar[Component] = BarComponent as GenericComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(bar);
|
||||
}
|
||||
|
||||
Object.assign(bar, decoratedData);
|
||||
|
||||
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");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(bar);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(bar)),
|
||||
{}
|
||||
);
|
||||
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: unref(style),
|
||||
classes,
|
||||
borderStyle,
|
||||
textStyle,
|
||||
baseStyle,
|
||||
fillStyle,
|
||||
mark,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return bar as unknown as Bar<T>;
|
||||
});
|
||||
}
|
110
src/features/bars/bar.tsx
Normal file
110
src/features/bars/bar.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
import Bar from "features/bars/Bar.vue";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import { Direction } from "util/common";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import { CSSProperties, MaybeRef, MaybeRefOrGetter } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Bar} features. */
|
||||
export const BarType = Symbol("Bar");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Bar}.
|
||||
*/
|
||||
export interface BarOptions extends VueFeatureOptions {
|
||||
/** The width of the bar. */
|
||||
width: MaybeRefOrGetter<number>;
|
||||
/** The height of the bar. */
|
||||
height: MaybeRefOrGetter<number>;
|
||||
/** The direction in which the bar progresses. */
|
||||
direction: MaybeRefOrGetter<Direction>;
|
||||
/** CSS to apply to the bar's border. */
|
||||
borderStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** CSS to apply to the bar's base. */
|
||||
baseStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** CSS to apply to the bar's text. */
|
||||
textStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** CSS to apply to the bar's fill. */
|
||||
fillStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** The progress value of the bar, from 0 to 1. */
|
||||
progress: MaybeRefOrGetter<DecimalSource>;
|
||||
/** The display to use for this bar. */
|
||||
display?: MaybeRefOrGetter<Renderable>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link BarOptions} to create a {@link Bar}.
|
||||
*/
|
||||
export interface BaseBar extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof BarType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that displays some sort of progress or completion or resource with a cap. */
|
||||
export type Bar = Replace<
|
||||
Replace<BarOptions, BaseBar>,
|
||||
{
|
||||
width: ProcessedRefOrGetter<BarOptions["width"]>;
|
||||
height: ProcessedRefOrGetter<BarOptions["height"]>;
|
||||
direction: ProcessedRefOrGetter<BarOptions["direction"]>;
|
||||
borderStyle: ProcessedRefOrGetter<BarOptions["borderStyle"]>;
|
||||
baseStyle: ProcessedRefOrGetter<BarOptions["baseStyle"]>;
|
||||
textStyle: ProcessedRefOrGetter<BarOptions["textStyle"]>;
|
||||
fillStyle: ProcessedRefOrGetter<BarOptions["fillStyle"]>;
|
||||
progress: ProcessedRefOrGetter<BarOptions["progress"]>;
|
||||
display?: MaybeRef<Renderable>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a bar with the given options.
|
||||
* @param optionsFunc Bar options.
|
||||
*/
|
||||
export function createBar<T extends BarOptions>(optionsFunc: OptionsFunc<T, BaseBar, Bar>) {
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as Bar);
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
direction,
|
||||
borderStyle,
|
||||
baseStyle,
|
||||
textStyle,
|
||||
fillStyle,
|
||||
progress,
|
||||
display,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
const bar = {
|
||||
type: BarType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof BarOptions>),
|
||||
...vueFeatureMixin("bar", options, () => (
|
||||
<Bar
|
||||
width={bar.width}
|
||||
height={bar.height}
|
||||
direction={bar.direction}
|
||||
borderStyle={bar.borderStyle}
|
||||
baseStyle={bar.baseStyle}
|
||||
textStyle={bar.textStyle}
|
||||
fillStyle={bar.fillStyle}
|
||||
progress={bar.progress}
|
||||
display={bar.display}
|
||||
/>
|
||||
)),
|
||||
width: processGetter(width),
|
||||
height: processGetter(height),
|
||||
direction: processGetter(direction),
|
||||
borderStyle: processGetter(borderStyle),
|
||||
baseStyle: processGetter(baseStyle),
|
||||
textStyle: processGetter(textStyle),
|
||||
fillStyle: processGetter(fillStyle),
|
||||
progress: processGetter(progress),
|
||||
display: processGetter(display)
|
||||
} satisfies Bar;
|
||||
|
||||
return bar;
|
||||
});
|
||||
}
|
|
@ -1,20 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
notifyStyle,
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
:style="notifyStyle"
|
||||
:class="{
|
||||
feature: true,
|
||||
challenge: true,
|
||||
done: unref(completed),
|
||||
canStart: unref(canStart) && !unref(maxed),
|
||||
maxed: unref(maxed),
|
||||
...unref(classes)
|
||||
maxed: unref(maxed)
|
||||
}"
|
||||
>
|
||||
<button
|
||||
|
@ -24,56 +15,43 @@
|
|||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
<component v-if="unref(comp)" :is="unref(comp)" />
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
<Component v-if="props.display" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { GenericChallenge } from "features/challenges/challenge";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
||||
import { displayRequirements, Requirements } from "game/requirements";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Component, UnwrapRef } from "vue";
|
||||
import { computed, shallowRef, unref, watchEffect } from "vue";
|
||||
import { displayRequirements } from "game/requirements";
|
||||
import { render } from "util/vue";
|
||||
import type { Component } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { Challenge } from "./challenge";
|
||||
|
||||
const props = defineProps<{
|
||||
active: boolean;
|
||||
maxed: boolean;
|
||||
canComplete: boolean;
|
||||
display?: UnwrapRef<GenericChallenge["display"]>;
|
||||
requirements?: Requirements;
|
||||
visibility: Visibility | boolean;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
completed: boolean;
|
||||
canStart: boolean;
|
||||
mark?: boolean | string;
|
||||
id: string;
|
||||
toggle: VoidFunction;
|
||||
active: Challenge["active"];
|
||||
maxed: Challenge["maxed"];
|
||||
canComplete: Challenge["canComplete"];
|
||||
display: Challenge["display"];
|
||||
requirements: Challenge["requirements"];
|
||||
completed: Challenge["completed"];
|
||||
canStart: Challenge["canStart"];
|
||||
toggle: Challenge["toggle"];
|
||||
}>();
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (props.active) {
|
||||
return props.canComplete ? "Finish" : "Exit Early";
|
||||
if (unref(props.active)) {
|
||||
return unref(props.canComplete) ? "Finish" : "Exit Early";
|
||||
}
|
||||
if (props.maxed) {
|
||||
if (unref(props.maxed)) {
|
||||
return "Completed";
|
||||
}
|
||||
return "Start";
|
||||
});
|
||||
|
||||
const comp = shallowRef<Component | string>("");
|
||||
|
||||
const notifyStyle = computed(() => {
|
||||
const currActive = props.active;
|
||||
const currCanComplete = props.canComplete;
|
||||
const currActive = unref(props.active);
|
||||
const currCanComplete = unref(props.canComplete);
|
||||
if (currActive) {
|
||||
if (currCanComplete) {
|
||||
return getHighNotifyStyle();
|
||||
|
@ -83,49 +61,34 @@ const notifyStyle = computed(() => {
|
|||
return {};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const currDisplay = props.display;
|
||||
if (currDisplay == null) {
|
||||
comp.value = "";
|
||||
return;
|
||||
const Component = () => {
|
||||
if (props.display == null) {
|
||||
return null;
|
||||
}
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
comp.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
const Goal = coerceComponent(currDisplay.goal != null ? currDisplay.goal : jsx(() => displayRequirements(props.requirements ?? [])), "h3");
|
||||
const Reward = coerceComponent(currDisplay.reward || "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||
comp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
if (typeof props.display === "object" && "description" in props.display) {
|
||||
const { title, description, goal, reward, effectDisplay } = props.display;
|
||||
return <span>
|
||||
{title != null ? (<div>{render(title, el => <h3>{el}</h3>)}</div>) : null}
|
||||
{render(description, el => <div>{el}</div>)}
|
||||
<div>
|
||||
<br />
|
||||
Goal: {goal == null ? displayRequirements(props.requirements) : render(goal, el => <h3>{el}</h3>)}
|
||||
</div>
|
||||
{reward != null ? (
|
||||
<div>
|
||||
<br />
|
||||
Goal: <Goal />
|
||||
Reward: {render(reward)}
|
||||
</div>
|
||||
{currDisplay.reward != null ? (
|
||||
<div>
|
||||
<br />
|
||||
Reward: <Reward />
|
||||
</div>
|
||||
) : null}
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
))
|
||||
);
|
||||
});
|
||||
) : null}
|
||||
{effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: {render(effectDisplay)}
|
||||
</div>
|
||||
) : null}
|
||||
</span>;
|
||||
}
|
||||
return render(props.display);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,23 +1,7 @@
|
|||
import Toggle from "components/fields/Toggle.vue";
|
||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
Visibility,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
jsx,
|
||||
setDefault
|
||||
} from "features/feature";
|
||||
import type { GenericReset } from "features/reset";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { isVisible } from "features/feature";
|
||||
import type { Reset } from "features/reset";
|
||||
import { globalBus } from "game/events";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
|
@ -25,16 +9,12 @@ import { Requirements, maxRequirementsMet } from "game/requirements";
|
|||
import settings, { registerSettingField } from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref, WatchStopHandle } from "vue";
|
||||
import { Renderable, VueFeature, VueFeatureOptions, vueFeatureMixin } from "util/vue";
|
||||
import type { MaybeRef, MaybeRefOrGetter, Ref, WatchStopHandle } from "vue";
|
||||
import { computed, unref, watch } from "vue";
|
||||
import Challenge from "./Challenge.vue";
|
||||
|
||||
/** A symbol used to identify {@link Challenge} features. */
|
||||
export const ChallengeType = Symbol("Challenge");
|
||||
|
@ -42,39 +22,30 @@ export const ChallengeType = Symbol("Challenge");
|
|||
/**
|
||||
* An object that configures a {@link Challenge}.
|
||||
*/
|
||||
export interface ChallengeOptions {
|
||||
/** Whether this challenge should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
export interface ChallengeOptions extends VueFeatureOptions {
|
||||
/** Whether this challenge can be started. */
|
||||
canStart?: Computable<boolean>;
|
||||
canStart?: MaybeRefOrGetter<boolean>;
|
||||
/** The reset function for this challenge. */
|
||||
reset?: GenericReset;
|
||||
reset?: Reset;
|
||||
/** The requirement(s) to complete this challenge. */
|
||||
requirements: Requirements;
|
||||
/** The maximum number of times the challenge can be completed. */
|
||||
completionLimit?: Computable<DecimalSource>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
completionLimit?: MaybeRefOrGetter<DecimalSource>;
|
||||
/** The display to use for this challenge. */
|
||||
display?: Computable<
|
||||
| CoercableComponent
|
||||
display?:
|
||||
| MaybeRefOrGetter<Renderable>
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
title?: MaybeRefOrGetter<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
description: CoercableComponent;
|
||||
description: MaybeRefOrGetter<Renderable>;
|
||||
/** A description of the current goal for this challenge. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
|
||||
goal?: CoercableComponent;
|
||||
goal?: MaybeRefOrGetter<Renderable>;
|
||||
/** A description of what will change upon completing this challenge. */
|
||||
reward?: CoercableComponent;
|
||||
reward?: MaybeRefOrGetter<Renderable>;
|
||||
/** A description of the current effect of this challenge. */
|
||||
effectDisplay?: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
effectDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
};
|
||||
/** A function that is called when the challenge is completed. */
|
||||
onComplete?: VoidFunction;
|
||||
/** A function that is called when the challenge is exited. */
|
||||
|
@ -86,9 +57,7 @@ export interface ChallengeOptions {
|
|||
/**
|
||||
* The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
|
||||
*/
|
||||
export interface BaseChallenge {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
export interface BaseChallenge extends VueFeature {
|
||||
/** The current amount of times this challenge can be completed. */
|
||||
canComplete: Ref<DecimalSource>;
|
||||
/** The current number of times this challenge has been completed. */
|
||||
|
@ -108,35 +77,23 @@ export interface BaseChallenge {
|
|||
complete: (remainInChallenge?: boolean) => void;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ChallengeType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be entered and exited, and have one or more completions with scaling requirements. */
|
||||
export type Challenge<T extends ChallengeOptions> = Replace<
|
||||
T & BaseChallenge,
|
||||
export type Challenge = Replace<
|
||||
Replace<ChallengeOptions, BaseChallenge>,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
||||
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Challenge} object. */
|
||||
export type GenericChallenge = Replace<
|
||||
Challenge<ChallengeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canStart: ProcessedComputable<boolean>;
|
||||
completionLimit: ProcessedComputable<DecimalSource>;
|
||||
mark: ProcessedComputable<boolean>;
|
||||
canStart: MaybeRef<boolean>;
|
||||
completionLimit: MaybeRef<DecimalSource>;
|
||||
display?:
|
||||
| MaybeRef<Renderable>
|
||||
| {
|
||||
title?: MaybeRef<Renderable>;
|
||||
description: MaybeRef<Renderable>;
|
||||
goal?: MaybeRef<Renderable>;
|
||||
reward?: MaybeRef<Renderable>;
|
||||
effectDisplay?: MaybeRef<Renderable>;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -145,166 +102,123 @@ export type GenericChallenge = Replace<
|
|||
* @param optionsFunc Challenge options.
|
||||
*/
|
||||
export function createChallenge<T extends ChallengeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Challenge<T> {
|
||||
const completions = persistent(0);
|
||||
const active = persistent(false, false);
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
optionsFunc: OptionsFunc<T, BaseChallenge, Challenge>
|
||||
) {
|
||||
const completions = persistent<DecimalSource>(0);
|
||||
const active = persistent<boolean>(false, false);
|
||||
return createLazyProxy(feature => {
|
||||
const challenge = optionsFunc.call(feature, feature);
|
||||
const options = optionsFunc.call(feature, feature as Challenge);
|
||||
const {
|
||||
requirements,
|
||||
canStart,
|
||||
completionLimit,
|
||||
display,
|
||||
reset,
|
||||
onComplete,
|
||||
onEnter,
|
||||
onExit,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
challenge.id = getUniqueID("challenge-");
|
||||
challenge.type = ChallengeType;
|
||||
challenge[Component] = ChallengeComponent as GenericComponent;
|
||||
const vueFeature = vueFeatureMixin("challenge", options, () => (
|
||||
<Challenge
|
||||
active={challenge.active}
|
||||
maxed={challenge.maxed}
|
||||
canComplete={challenge.canComplete}
|
||||
display={challenge.display}
|
||||
requirements={challenge.requirements}
|
||||
completed={challenge.completed}
|
||||
canStart={challenge.canStart}
|
||||
toggle={challenge.toggle}
|
||||
/>
|
||||
));
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(challenge);
|
||||
}
|
||||
|
||||
challenge.completions = completions;
|
||||
challenge.active = active;
|
||||
Object.assign(challenge, decoratedData);
|
||||
|
||||
challenge.completed = computed(() =>
|
||||
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
||||
);
|
||||
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 (
|
||||
Decimal.gt(unref(genericChallenge.canComplete), 0) &&
|
||||
!genericChallenge.maxed.value
|
||||
const challenge = {
|
||||
type: ChallengeType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof ChallengeOptions>),
|
||||
...vueFeature,
|
||||
completions,
|
||||
active,
|
||||
completed: computed(() => Decimal.gt(completions.value, 0)),
|
||||
canComplete: computed(() => maxRequirementsMet(requirements)),
|
||||
maxed: computed((): boolean =>
|
||||
Decimal.gte(completions.value, unref(challenge.completionLimit))
|
||||
),
|
||||
canStart: processGetter(canStart) ?? true,
|
||||
completionLimit: processGetter(completionLimit) ?? 1,
|
||||
requirements,
|
||||
reset,
|
||||
onComplete,
|
||||
onEnter,
|
||||
onExit,
|
||||
display:
|
||||
display == null
|
||||
? undefined
|
||||
: typeof display === "object" && "description" in display
|
||||
? {
|
||||
title: processGetter(display.title),
|
||||
description: processGetter(display.description),
|
||||
goal: processGetter(display.goal),
|
||||
reward: processGetter(display.reward),
|
||||
effectDisplay: processGetter(display.effectDisplay)
|
||||
}
|
||||
: processGetter(display),
|
||||
toggle: function () {
|
||||
if (active.value) {
|
||||
if (
|
||||
Decimal.gt(unref(challenge.canComplete), 0) &&
|
||||
!unref<boolean>(challenge.maxed)
|
||||
) {
|
||||
const newCompletions = unref(challenge.canComplete);
|
||||
completions.value = Decimal.min(
|
||||
Decimal.add(challenge.completions.value, newCompletions),
|
||||
unref(challenge.completionLimit)
|
||||
);
|
||||
onComplete?.();
|
||||
}
|
||||
active.value = false;
|
||||
onExit?.();
|
||||
reset?.reset();
|
||||
} else if (
|
||||
unref<boolean>(challenge.canStart) &&
|
||||
isVisible(unref(challenge.visibility) ?? true) &&
|
||||
!unref<boolean>(challenge.maxed)
|
||||
) {
|
||||
const completions = unref(genericChallenge.canComplete);
|
||||
genericChallenge.completions.value = Decimal.min(
|
||||
Decimal.add(genericChallenge.completions.value, completions),
|
||||
unref(genericChallenge.completionLimit)
|
||||
challenge.reset?.reset();
|
||||
active.value = true;
|
||||
onEnter?.();
|
||||
}
|
||||
},
|
||||
complete: function (remainInChallenge?: boolean) {
|
||||
const newCompletions = unref(challenge.canComplete);
|
||||
if (
|
||||
active.value &&
|
||||
Decimal.gt(newCompletions, 0) &&
|
||||
!unref<boolean>(challenge.maxed)
|
||||
) {
|
||||
completions.value = Decimal.min(
|
||||
Decimal.add(challenge.completions.value, newCompletions),
|
||||
unref(challenge.completionLimit)
|
||||
);
|
||||
genericChallenge.onComplete?.();
|
||||
}
|
||||
genericChallenge.active.value = false;
|
||||
genericChallenge.onExit?.();
|
||||
genericChallenge.reset?.reset();
|
||||
} else if (
|
||||
unref(genericChallenge.canStart) &&
|
||||
isVisible(genericChallenge.visibility) &&
|
||||
!genericChallenge.maxed.value
|
||||
) {
|
||||
genericChallenge.reset?.reset();
|
||||
genericChallenge.active.value = true;
|
||||
genericChallenge.onEnter?.();
|
||||
}
|
||||
};
|
||||
challenge.canComplete = computed(() =>
|
||||
maxRequirementsMet((challenge as GenericChallenge).requirements)
|
||||
);
|
||||
challenge.complete = function (remainInChallenge?: boolean) {
|
||||
const genericChallenge = challenge as GenericChallenge;
|
||||
const completions = unref(genericChallenge.canComplete);
|
||||
if (
|
||||
genericChallenge.active.value &&
|
||||
Decimal.gt(completions, 0) &&
|
||||
!genericChallenge.maxed.value
|
||||
) {
|
||||
genericChallenge.completions.value = Decimal.min(
|
||||
Decimal.add(genericChallenge.completions.value, completions),
|
||||
unref(genericChallenge.completionLimit)
|
||||
);
|
||||
genericChallenge.onComplete?.();
|
||||
if (remainInChallenge !== true) {
|
||||
genericChallenge.active.value = false;
|
||||
genericChallenge.onExit?.();
|
||||
genericChallenge.reset?.reset();
|
||||
onComplete?.();
|
||||
if (remainInChallenge !== true) {
|
||||
active.value = false;
|
||||
onExit?.();
|
||||
reset?.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
processComputable(challenge as T, "visibility");
|
||||
setDefault(challenge, "visibility", Visibility.Visible);
|
||||
const visibility = challenge.visibility as ProcessedComputable<Visibility | boolean>;
|
||||
challenge.visibility = computed(() => {
|
||||
if (settings.hideChallenges === true && unref(challenge.maxed)) {
|
||||
return Visibility.None;
|
||||
}
|
||||
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");
|
||||
setDefault(challenge, "canStart", true);
|
||||
processComputable(challenge as T, "completionLimit");
|
||||
setDefault(challenge, "completionLimit", 1);
|
||||
processComputable(challenge as T, "mark");
|
||||
processComputable(challenge as T, "classes");
|
||||
processComputable(challenge as T, "style");
|
||||
processComputable(challenge as T, "display");
|
||||
} satisfies Challenge;
|
||||
|
||||
if (challenge.reset != null) {
|
||||
globalBus.on("reset", currentReset => {
|
||||
if (currentReset === challenge.reset && (challenge.active as Ref<boolean>).value) {
|
||||
(challenge.toggle as VoidFunction)();
|
||||
if (currentReset === challenge.reset && active.value) {
|
||||
challenge.toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(challenge);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(challenge)),
|
||||
{}
|
||||
);
|
||||
challenge[GatherProps] = function (this: GenericChallenge) {
|
||||
const {
|
||||
active,
|
||||
maxed,
|
||||
canComplete,
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
completed,
|
||||
canStart,
|
||||
mark,
|
||||
id,
|
||||
toggle,
|
||||
requirements
|
||||
} = this;
|
||||
return {
|
||||
active,
|
||||
maxed,
|
||||
canComplete,
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
completed,
|
||||
canStart,
|
||||
mark,
|
||||
id,
|
||||
toggle,
|
||||
requirements,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return challenge as unknown as Challenge<T>;
|
||||
return challenge;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -315,8 +229,8 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
* @param exitOnComplete Whether or not to exit the challenge after auto-completion
|
||||
*/
|
||||
export function setupAutoComplete(
|
||||
challenge: GenericChallenge,
|
||||
autoActive: Computable<boolean> = true,
|
||||
challenge: Challenge,
|
||||
autoActive: MaybeRefOrGetter<boolean> = true,
|
||||
exitOnComplete = true
|
||||
): WatchStopHandle {
|
||||
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
|
@ -334,9 +248,7 @@ export function setupAutoComplete(
|
|||
* Utility for taking an array of challenges where only one may be active at a time, and giving a ref to the one currently active (or null if none are active)
|
||||
* @param challenges The list of challenges that are mutually exclusive
|
||||
*/
|
||||
export function createActiveChallenge(
|
||||
challenges: GenericChallenge[]
|
||||
): Ref<GenericChallenge | null> {
|
||||
export function createActiveChallenge(challenges: Challenge[]): Ref<Challenge | null> {
|
||||
return computed(() => challenges.find(challenge => challenge.active.value) ?? null);
|
||||
}
|
||||
|
||||
|
@ -345,12 +257,12 @@ export function createActiveChallenge(
|
|||
* @param challenges List of challenges that are mutually exclusive
|
||||
*/
|
||||
export function isAnyChallengeActive(
|
||||
challenges: GenericChallenge[] | Ref<GenericChallenge | null>
|
||||
challenges: Challenge[] | Ref<Challenge | null>
|
||||
): Ref<boolean> {
|
||||
if (Array.isArray(challenges)) {
|
||||
challenges = createActiveChallenge(challenges);
|
||||
}
|
||||
return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
|
||||
return computed(() => (challenges as Ref<Challenge | null>).value != null);
|
||||
}
|
||||
|
||||
declare module "game/settings" {
|
||||
|
@ -360,22 +272,20 @@ declare module "game/settings" {
|
|||
}
|
||||
|
||||
globalBus.on("loadSettings", settings => {
|
||||
setDefault(settings, "hideChallenges", false);
|
||||
settings.hideChallenges ??= false;
|
||||
});
|
||||
|
||||
globalBus.on("setupVue", () =>
|
||||
registerSettingField(
|
||||
jsx(() => (
|
||||
<Toggle
|
||||
title={jsx(() => (
|
||||
<span class="option-title">
|
||||
Hide maxed challenges
|
||||
<desc>Hide challenges that have been fully completed.</desc>
|
||||
</span>
|
||||
))}
|
||||
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
||||
modelValue={settings.hideChallenges}
|
||||
/>
|
||||
))
|
||||
)
|
||||
registerSettingField(() => (
|
||||
<Toggle
|
||||
title={
|
||||
<span class="option-title">
|
||||
Hide maxed challenges
|
||||
<desc>Hide challenges that have been fully completed.</desc>
|
||||
</span>
|
||||
}
|
||||
onUpdate:modelValue={value => (settings.hideChallenges = value)}
|
||||
modelValue={settings.hideChallenges}
|
||||
/>
|
||||
))
|
||||
);
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{ visibility: isHidden(visibility) ? 'hidden' : undefined },
|
||||
unref(style) ?? []
|
||||
]"
|
||||
@click="onClick"
|
||||
@mousedown="start"
|
||||
@mouseleave="stop"
|
||||
|
@ -13,75 +8,34 @@
|
|||
@touchend.passive="stop"
|
||||
@touchcancel.passive="stop"
|
||||
:class="{
|
||||
feature: true,
|
||||
clickable: true,
|
||||
can: unref(canClick),
|
||||
locked: !unref(canClick),
|
||||
small,
|
||||
...unref(classes)
|
||||
locked: !unref(canClick)
|
||||
}"
|
||||
:disabled="!unref(canClick)"
|
||||
>
|
||||
<component v-if="unref(comp)" :is="unref(comp)" />
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
<Component />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { GenericClickable } from "features/clickables/clickable";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import type { Clickable } from "features/clickables/clickable";
|
||||
import {
|
||||
coerceComponent,
|
||||
isCoercableComponent,
|
||||
render,
|
||||
setupHoldToClick
|
||||
} from "util/vue";
|
||||
import type { Component, UnwrapRef } from "vue";
|
||||
import { shallowRef, toRef, unref, watchEffect } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import { toRef, unref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
display: UnwrapRef<GenericClickable["display"]>;
|
||||
visibility: Visibility | boolean;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
onHold?: VoidFunction;
|
||||
canClick: boolean;
|
||||
small?: boolean;
|
||||
mark?: boolean | string;
|
||||
id: string;
|
||||
canClick: Clickable["canClick"];
|
||||
onClick: Clickable["onClick"];
|
||||
onHold?: Clickable["onHold"];
|
||||
display: Clickable["display"];
|
||||
}>();
|
||||
|
||||
const comp = shallowRef<Component | string>("");
|
||||
|
||||
watchEffect(() => {
|
||||
const currDisplay = props.display;
|
||||
if (currDisplay == null) {
|
||||
comp.value = "";
|
||||
return;
|
||||
}
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
comp.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
comp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
</span>
|
||||
))
|
||||
);
|
||||
});
|
||||
const Component = () => props.display == null ? <></> : render(props.display);
|
||||
|
||||
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||
</script>
|
||||
|
@ -93,10 +47,6 @@ const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "
|
|||
font-size: 10px;
|
||||
}
|
||||
|
||||
.clickable.small {
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.clickable > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
189
src/features/clickables/action.tsx
Normal file
189
src/features/clickables/action.tsx
Normal file
|
@ -0,0 +1,189 @@
|
|||
import ClickableVue from "features/clickables/Clickable.vue";
|
||||
import { findFeatures, OptionsFunc, Replace } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import { persistent } from "game/persistence";
|
||||
import Decimal, { DecimalSource } from "lib/break_eternity";
|
||||
import { Unsubscribe } from "nanoevents";
|
||||
import { Direction } from "util/common";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { render, VueFeature, vueFeatureMixin } from "util/vue";
|
||||
import { computed, MaybeRef, MaybeRefOrGetter, Ref, ref, unref } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
import { Bar, BarOptions, createBar } from "../bars/bar";
|
||||
import { type Clickable, ClickableOptions } from "./clickable";
|
||||
|
||||
/** A symbol used to identify {@link Action} features. */
|
||||
export const ActionType = Symbol("Action");
|
||||
|
||||
/**
|
||||
* An object that configures an {@link Action}.
|
||||
*/
|
||||
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
|
||||
/** The cooldown during which the action cannot be performed again, in seconds. */
|
||||
duration: MaybeRefOrGetter<DecimalSource>;
|
||||
/** Whether or not the action should perform automatically when the cooldown is finished. */
|
||||
autoStart?: MaybeRefOrGetter<boolean>;
|
||||
/** A function that is called when the action is clicked. */
|
||||
onClick: (amount: DecimalSource) => void;
|
||||
/** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
|
||||
barOptions?: Partial<BarOptions>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link ActionOptions} to create an {@link Action}.
|
||||
*/
|
||||
export interface BaseAction extends VueFeature {
|
||||
/** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
|
||||
isHolding: Ref<boolean>;
|
||||
/** The current amount of progress through the cooldown. */
|
||||
progress: Ref<DecimalSource>;
|
||||
/** The bar used to display the current cooldown progress. */
|
||||
progressBar: Bar;
|
||||
/** Update the cooldown the specified number of seconds */
|
||||
update: (diff: number) => void;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ActionType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be clicked upon, and then has a cooldown before it can be clicked again. */
|
||||
export type Action = Replace<
|
||||
Replace<ActionOptions, BaseAction>,
|
||||
{
|
||||
duration: ProcessedRefOrGetter<ActionOptions["duration"]>;
|
||||
autoStart: MaybeRef<boolean>;
|
||||
canClick: MaybeRef<boolean>;
|
||||
display: ProcessedRefOrGetter<ActionOptions["display"]>;
|
||||
onClick: VoidFunction;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an action with the given options.
|
||||
* @param optionsFunc Action options.
|
||||
*/
|
||||
export function createAction<T extends ActionOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseAction, Action>
|
||||
) {
|
||||
const progress = persistent<DecimalSource>(0);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as Action) ?? ({} as T);
|
||||
const { style, duration, canClick, autoStart, display, barOptions, onClick, ...props } =
|
||||
options;
|
||||
|
||||
const processedCanClick = processGetter(canClick) ?? true;
|
||||
const processedStyle = processGetter(style);
|
||||
|
||||
const progressBar = createBar(() => ({
|
||||
direction: Direction.Right,
|
||||
width: 100,
|
||||
height: 10,
|
||||
borderStyle: { borderColor: "black" },
|
||||
baseStyle: { marginTop: "-1px" },
|
||||
progress: (): DecimalSource => Decimal.div(progress.value, unref(action.duration)),
|
||||
...(barOptions as Omit<typeof barOptions, keyof VueFeature>)
|
||||
}));
|
||||
|
||||
let Component: () => JSX.Element;
|
||||
if (typeof display === "object" && "description" in display) {
|
||||
const title = processGetter(display.title);
|
||||
const description = processGetter(display.description);
|
||||
|
||||
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
|
||||
const Description = () => render(description, el => <div>{el}</div>);
|
||||
|
||||
Component = () => {
|
||||
return (
|
||||
<span>
|
||||
{title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
} else if (display != null) {
|
||||
const processedDisplay = processGetter(display);
|
||||
Component = () => render(processedDisplay);
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof ActionOptions>),
|
||||
...vueFeatureMixin(
|
||||
"action",
|
||||
{
|
||||
...options,
|
||||
style: () => ({
|
||||
cursor: Decimal.gte(progress.value, unref(action.duration))
|
||||
? "pointer"
|
||||
: "progress",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
...unref(processedStyle)
|
||||
})
|
||||
},
|
||||
() => (
|
||||
<ClickableVue
|
||||
canClick={action.canClick}
|
||||
onClick={action.onClick}
|
||||
display={action.display}
|
||||
/>
|
||||
)
|
||||
),
|
||||
progress,
|
||||
isHolding: ref(false),
|
||||
duration: processGetter(duration),
|
||||
canClick: computed(
|
||||
(): boolean =>
|
||||
unref(processedCanClick) && Decimal.gte(progress.value, unref(action.duration))
|
||||
),
|
||||
autoStart: processGetter(autoStart) ?? false,
|
||||
display: computed(() => (
|
||||
<>
|
||||
<div style="flex-grow: 1" />
|
||||
{display == null ? null : <Component />}
|
||||
<div style="flex-grow: 1" />
|
||||
{render(progressBar)}
|
||||
</>
|
||||
)),
|
||||
progressBar,
|
||||
onClick: function () {
|
||||
if (unref(action.canClick) === false) {
|
||||
return;
|
||||
}
|
||||
const amount = Decimal.div(progress.value, unref(action.duration));
|
||||
onClick?.call(action, amount);
|
||||
progress.value = 0;
|
||||
},
|
||||
update: function (diff) {
|
||||
const duration = unref(action.duration);
|
||||
if (Decimal.gte(progress.value, duration)) {
|
||||
progress.value = duration;
|
||||
} else {
|
||||
progress.value = Decimal.add(progress.value, diff);
|
||||
if (action.isHolding.value || unref<boolean>(action.autoStart)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies Action satisfies Replace<Clickable, { type: typeof ActionType }>;
|
||||
|
||||
return action;
|
||||
});
|
||||
}
|
||||
|
||||
const listeners: Record<string, Unsubscribe | undefined> = {};
|
||||
globalBus.on("addLayer", layer => {
|
||||
const actions: Action[] = findFeatures(layer, ActionType) as Action[];
|
||||
listeners[layer.id] = layer.on("postUpdate", (diff: number) => {
|
||||
actions.forEach(action => action.update(diff));
|
||||
});
|
||||
});
|
||||
globalBus.on("removeLayer", layer => {
|
||||
// unsubscribe from postUpdate
|
||||
listeners[layer.id]?.();
|
||||
listeners[layer.id] = undefined;
|
||||
});
|
|
@ -1,204 +0,0 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, Visibility, getUniqueID, setDefault } from "features/feature";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Clickable} features. */
|
||||
export const ClickableType = Symbol("Clickable");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Clickable}.
|
||||
*/
|
||||
export interface ClickableOptions {
|
||||
/** Whether this clickable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** Whether or not the clickable may be clicked. */
|
||||
canClick?: Computable<boolean>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** The display to use for this clickable. */
|
||||
display?: Computable<
|
||||
| CoercableComponent
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
/** The main text that appears in the display. */
|
||||
description: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
/** Toggles a smaller design for the feature. */
|
||||
small?: boolean;
|
||||
/** A function that is called when the clickable is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the clickable is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link ClickableOptions} to create an {@link Clickable}.
|
||||
*/
|
||||
export interface BaseClickable {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ClickableType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be clicked or held down. */
|
||||
export type Clickable<T extends ClickableOptions> = Replace<
|
||||
T & BaseClickable,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], true>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Clickable} object. */
|
||||
export type GenericClickable = Replace<
|
||||
Clickable<ClickableOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a clickable with the given options.
|
||||
* @param optionsFunc Clickable options.
|
||||
*/
|
||||
export function createClickable<T extends ClickableOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Clickable<T> {
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const clickable =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
clickable.id = getUniqueID("clickable-");
|
||||
clickable.type = ClickableType;
|
||||
clickable[Component] = ClickableComponent as GenericComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(clickable);
|
||||
}
|
||||
|
||||
Object.assign(clickable, decoratedData);
|
||||
|
||||
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");
|
||||
|
||||
if (clickable.onClick) {
|
||||
const onClick = clickable.onClick.bind(clickable);
|
||||
clickable.onClick = function (e) {
|
||||
if (unref(clickable.canClick as ProcessedComputable<boolean>) !== false) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (clickable.onHold) {
|
||||
const onHold = clickable.onHold.bind(clickable);
|
||||
clickable.onHold = function () {
|
||||
if (unref(clickable.canClick as ProcessedComputable<boolean>) !== false) {
|
||||
onHold();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(clickable);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(clickable)),
|
||||
{}
|
||||
);
|
||||
clickable[GatherProps] = function (this: GenericClickable) {
|
||||
const {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
onClick,
|
||||
onHold,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
onHold,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return clickable as unknown as Clickable<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to auto click a clickable whenever it can be.
|
||||
* @param layer The layer the clickable is apart of
|
||||
* @param clickable The clicker to click automatically
|
||||
* @param autoActive Whether or not the clickable should currently be auto-clicking
|
||||
*/
|
||||
export function setupAutoClick(
|
||||
layer: BaseLayer,
|
||||
clickable: GenericClickable,
|
||||
autoActive: Computable<boolean> = true
|
||||
): Unsubscribe {
|
||||
const isActive: ProcessedComputable<boolean> =
|
||||
typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
return layer.on("update", () => {
|
||||
if (unref(isActive) && unref(clickable.canClick)) {
|
||||
clickable.onClick?.();
|
||||
}
|
||||
});
|
||||
}
|
136
src/features/clickables/clickable.tsx
Normal file
136
src/features/clickables/clickable.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import Clickable from "features/clickables/Clickable.vue";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Clickable} features. */
|
||||
export const ClickableType = Symbol("Clickable");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Clickable}.
|
||||
*/
|
||||
export interface ClickableOptions extends VueFeatureOptions {
|
||||
/** Whether or not the clickable may be clicked. */
|
||||
canClick?: MaybeRefOrGetter<boolean>;
|
||||
/** The display to use for this clickable. */
|
||||
display?:
|
||||
| MaybeRefOrGetter<Renderable>
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: MaybeRefOrGetter<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
description: MaybeRefOrGetter<Renderable>;
|
||||
};
|
||||
/** A function that is called when the clickable is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the clickable is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link ClickableOptions} to create an {@link Clickable}.
|
||||
*/
|
||||
export interface BaseClickable extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ClickableType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be clicked or held down. */
|
||||
export type Clickable = Replace<
|
||||
Replace<ClickableOptions, BaseClickable>,
|
||||
{
|
||||
canClick: MaybeRef<boolean>;
|
||||
display?: MaybeRef<Renderable>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a clickable with the given options.
|
||||
* @param optionsFunc Clickable options.
|
||||
*/
|
||||
export function createClickable<T extends ClickableOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseClickable, Clickable>
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as Clickable) ?? ({} as T);
|
||||
const { canClick, display: _display, onClick: onClick, onHold: onHold, ...props } = options;
|
||||
|
||||
let display: MaybeRef<Renderable> | undefined = undefined;
|
||||
if (typeof _display === "object" && "description" in _display) {
|
||||
const title = processGetter(_display.title);
|
||||
const description = processGetter(_display.description);
|
||||
|
||||
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
|
||||
const Description = () => render(description, el => <div>{el}</div>);
|
||||
|
||||
display = computed(() => (
|
||||
<span>
|
||||
{title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
</span>
|
||||
));
|
||||
} else if (_display != null) {
|
||||
display = processGetter(_display);
|
||||
}
|
||||
|
||||
const clickable = {
|
||||
type: ClickableType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof ClickableOptions>),
|
||||
...vueFeatureMixin("clickable", options, () => (
|
||||
<Clickable
|
||||
canClick={clickable.canClick}
|
||||
onClick={clickable.onClick}
|
||||
display={clickable.display}
|
||||
/>
|
||||
)),
|
||||
canClick: processGetter(canClick) ?? true,
|
||||
display,
|
||||
onClick:
|
||||
onClick == null
|
||||
? undefined
|
||||
: function (e) {
|
||||
if (unref(clickable.canClick) !== false) {
|
||||
onClick.call(clickable, e);
|
||||
}
|
||||
},
|
||||
onHold:
|
||||
onHold == null
|
||||
? undefined
|
||||
: function () {
|
||||
if (unref(clickable.canClick) !== false) {
|
||||
onHold.call(clickable);
|
||||
}
|
||||
}
|
||||
} satisfies Clickable & { onClick: T["onClick"] };
|
||||
|
||||
return clickable;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to auto click a clickable whenever it can be.
|
||||
* @param layer The layer the clickable is apart of
|
||||
* @param clickable The clicker to click automatically
|
||||
* @param autoActive Whether or not the clickable should currently be auto-clicking
|
||||
*/
|
||||
export function setupAutoClick(
|
||||
layer: BaseLayer,
|
||||
clickable: Clickable,
|
||||
autoActive: MaybeRefOrGetter<boolean> = true
|
||||
): Unsubscribe {
|
||||
const isActive: MaybeRef<boolean> =
|
||||
typeof autoActive === "function" ? computed(autoActive) : autoActive;
|
||||
return layer.on("update", () => {
|
||||
if (unref(isActive) && unref<boolean>(clickable.canClick)) {
|
||||
clickable.onClick?.();
|
||||
}
|
||||
});
|
||||
}
|
209
src/features/clickables/repeatable.tsx
Normal file
209
src/features/clickables/repeatable.tsx
Normal file
|
@ -0,0 +1,209 @@
|
|||
import Clickable from "features/clickables/Clickable.vue";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
||||
import {
|
||||
createVisibilityRequirement,
|
||||
displayRequirements,
|
||||
maxRequirementsMet,
|
||||
payRequirements,
|
||||
Requirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatWhole } from "util/bignum";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin } from "util/vue";
|
||||
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, isRef, unref } from "vue";
|
||||
import { ClickableOptions } from "./clickable";
|
||||
|
||||
/** A symbol used to identify {@link Repeatable} features. */
|
||||
export const RepeatableType = Symbol("Repeatable");
|
||||
|
||||
/** An object that configures a {@link Repeatable}. */
|
||||
export interface RepeatableOptions extends Omit<ClickableOptions, "display" | "canClick"> {
|
||||
/** The requirement(s) to increase this repeatable. */
|
||||
requirements: Requirements;
|
||||
/** The maximum amount obtainable for this repeatable. */
|
||||
limit?: MaybeRefOrGetter<DecimalSource>;
|
||||
/** The initial amount this repeatable has on a new save / after reset. */
|
||||
initialAmount?: DecimalSource;
|
||||
/** The display to use for this repeatable. */
|
||||
display?:
|
||||
| MaybeRefOrGetter<Renderable>
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: MaybeRefOrGetter<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
description?: MaybeRefOrGetter<Renderable>;
|
||||
/** A description of the current effect of this repeatable, based off its amount. */
|
||||
effectDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
|
||||
showAmount?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
|
||||
*/
|
||||
export interface BaseRepeatable extends VueFeature {
|
||||
/** The current amount this repeatable has. */
|
||||
amount: Persistent<DecimalSource>;
|
||||
/** Whether or not this repeatable's amount is at it's limit. */
|
||||
maxed: Ref<boolean>;
|
||||
/** How much amount can be increased by, or 1 if unclickable. **/
|
||||
amountToIncrease: Ref<DecimalSource>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof RepeatableType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature with multiple "levels" with scaling requirements. */
|
||||
export type Repeatable = Replace<
|
||||
Replace<RepeatableOptions, BaseRepeatable>,
|
||||
{
|
||||
limit: MaybeRef<DecimalSource>;
|
||||
display?: MaybeRef<Renderable>;
|
||||
canClick: Ref<boolean>;
|
||||
onClick: (event?: MouseEvent | TouchEvent) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a repeatable with the given options.
|
||||
* @param optionsFunc Repeatable options.
|
||||
*/
|
||||
export function createRepeatable<T extends RepeatableOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseRepeatable, Repeatable>
|
||||
) {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Repeatable);
|
||||
const {
|
||||
requirements: _requirements,
|
||||
display: _display,
|
||||
limit,
|
||||
onClick,
|
||||
initialAmount,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
if (options.classes == null) {
|
||||
options.classes = computed(() => ({ bought: unref(repeatable.maxed) }));
|
||||
} else {
|
||||
const classes = processGetter(options.classes);
|
||||
options.classes = computed(() => ({
|
||||
...unref(classes),
|
||||
bought: unref(repeatable.maxed)
|
||||
}));
|
||||
}
|
||||
const vueFeature = vueFeatureMixin("repeatable", options, () => (
|
||||
<Clickable
|
||||
canClick={repeatable.canClick}
|
||||
onClick={repeatable.onClick}
|
||||
display={repeatable.display}
|
||||
/>
|
||||
));
|
||||
|
||||
const limitRequirement = {
|
||||
requirementMet: computed(
|
||||
(): DecimalSource => Decimal.sub(unref(repeatable.limit), unref(amount))
|
||||
),
|
||||
requiresPay: false,
|
||||
visibility: Visibility.None,
|
||||
canMaximize: true
|
||||
} as const;
|
||||
const requirements: Requirements = [
|
||||
...(Array.isArray(_requirements) ? _requirements : [_requirements]),
|
||||
limitRequirement
|
||||
];
|
||||
if (vueFeature.visibility != null) {
|
||||
requirements.push(createVisibilityRequirement(vueFeature.visibility));
|
||||
}
|
||||
|
||||
let display;
|
||||
if (typeof _display === "object" && !isRef(_display) && !isJSXElement(_display)) {
|
||||
const title = processGetter(_display.title);
|
||||
const description = processGetter(_display.description);
|
||||
const effectDisplay = processGetter(_display.effectDisplay);
|
||||
const showAmount = processGetter(_display.showAmount);
|
||||
|
||||
const Title = title == null ? null : () => render(title, el => <h3>{el}</h3>);
|
||||
const Description =
|
||||
description == null ? null : () => render(description, el => <>{el}</>);
|
||||
const EffectDisplay =
|
||||
effectDisplay == null ? null : () => render(effectDisplay, el => <>{el}</>);
|
||||
|
||||
display = computed(() => (
|
||||
<span>
|
||||
{Title == null ? null : (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
)}
|
||||
{Description == null ? null : <Description />}
|
||||
{showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
<>Amount: {formatWhole(unref(amount))}</>
|
||||
{Decimal.isFinite(unref(repeatable.limit)) ? (
|
||||
<> / {formatWhole(unref(repeatable.limit))}</>
|
||||
) : undefined}
|
||||
</div>
|
||||
)}
|
||||
{EffectDisplay == null ? null : (
|
||||
<div>
|
||||
<br />
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
)}
|
||||
{unref(repeatable.maxed) ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{displayRequirements(requirements, unref(repeatable.amountToIncrease))}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
));
|
||||
} else if (_display != null) {
|
||||
const processedDisplay = processGetter(_display);
|
||||
display = computed(() => render(processedDisplay));
|
||||
} else {
|
||||
display = undefined;
|
||||
}
|
||||
|
||||
amount[DefaultValue] = initialAmount ?? 0;
|
||||
|
||||
const repeatable = {
|
||||
type: RepeatableType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof RepeatableOptions>),
|
||||
...vueFeature,
|
||||
amount,
|
||||
requirements,
|
||||
limit: processGetter(limit) ?? Decimal.dInf,
|
||||
classes: computed(() => {
|
||||
const currClasses = unref(vueFeature.classes) || {};
|
||||
if (unref(repeatable.maxed)) {
|
||||
currClasses.bought = true;
|
||||
}
|
||||
return currClasses;
|
||||
}),
|
||||
maxed: computed((): boolean => Decimal.gte(unref(amount), unref(repeatable.limit))),
|
||||
canClick: computed(() => requirementsMet(requirements)),
|
||||
amountToIncrease: computed(() => Decimal.clampMin(maxRequirementsMet(requirements), 1)),
|
||||
onClick(event?: MouseEvent | TouchEvent) {
|
||||
if (!unref(repeatable.canClick)) {
|
||||
return;
|
||||
}
|
||||
const purchaseAmount = unref(repeatable.amountToIncrease) ?? 1;
|
||||
payRequirements(requirements, purchaseAmount);
|
||||
amount.value = Decimal.add(unref(amount), purchaseAmount);
|
||||
onClick?.(event);
|
||||
},
|
||||
display
|
||||
} satisfies Repeatable;
|
||||
|
||||
return repeatable;
|
||||
});
|
||||
}
|
187
src/features/clickables/upgrade.tsx
Normal file
187
src/features/clickables/upgrade.tsx
Normal file
|
@ -0,0 +1,187 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { findFeatures } from "features/feature";
|
||||
import { Layer } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import {
|
||||
Requirements,
|
||||
createVisibilityRequirement,
|
||||
displayRequirements,
|
||||
payRequirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import { isFunction } from "util/common";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, VueFeatureOptions, render, vueFeatureMixin } from "util/vue";
|
||||
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import Clickable from "./Clickable.vue";
|
||||
|
||||
/** A symbol used to identify {@link Upgrade} features. */
|
||||
export const UpgradeType = Symbol("Upgrade");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Upgrade}.
|
||||
*/
|
||||
export interface UpgradeOptions extends VueFeatureOptions {
|
||||
/** The display to use for this upgrade. */
|
||||
display?:
|
||||
| MaybeRefOrGetter<Renderable>
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: MaybeRefOrGetter<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
description: MaybeRefOrGetter<Renderable>;
|
||||
/** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
|
||||
effectDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
};
|
||||
/** The requirements to purchase this upgrade. */
|
||||
requirements: Requirements;
|
||||
/** A function that is called when the upgrade is purchased. */
|
||||
onPurchase?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link UpgradeOptions} to create an {@link Upgrade}.
|
||||
*/
|
||||
export interface BaseUpgrade extends VueFeature {
|
||||
/** Whether or not this upgrade has been purchased. */
|
||||
bought: Persistent<boolean>;
|
||||
/** Whether or not the upgrade can currently be purchased. */
|
||||
canPurchase: Ref<boolean>;
|
||||
/** Purchase the upgrade */
|
||||
purchase: VoidFunction;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof UpgradeType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be purchased a single time. */
|
||||
export type Upgrade = Replace<
|
||||
Replace<UpgradeOptions, BaseUpgrade>,
|
||||
{
|
||||
display?:
|
||||
| MaybeRef<Renderable>
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: MaybeRef<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
description: MaybeRef<Renderable>;
|
||||
/** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
|
||||
effectDisplay?: MaybeRef<Renderable>;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an upgrade with the given options.
|
||||
* @param optionsFunc Upgrade options.
|
||||
*/
|
||||
export function createUpgrade<T extends UpgradeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseUpgrade, Upgrade>
|
||||
) {
|
||||
const bought = persistent<boolean>(false, false);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Upgrade);
|
||||
const { requirements: _requirements, display: _display, ...props } = options;
|
||||
|
||||
if (options.classes == null) {
|
||||
options.classes = computed(() => ({ bought: unref(upgrade.bought) }));
|
||||
} else {
|
||||
const classes = processGetter(options.classes);
|
||||
options.classes = computed(() => ({
|
||||
...unref(classes),
|
||||
bought: unref(upgrade.bought)
|
||||
}));
|
||||
}
|
||||
const vueFeature = vueFeatureMixin("upgrade", options, () => (
|
||||
<Clickable
|
||||
onClick={upgrade.purchase}
|
||||
canClick={upgrade.canPurchase}
|
||||
display={upgrade.display}
|
||||
/>
|
||||
));
|
||||
const requirements = Array.isArray(_requirements) ? _requirements : [_requirements];
|
||||
if (vueFeature.visibility != null) {
|
||||
requirements.push(createVisibilityRequirement(vueFeature.visibility));
|
||||
}
|
||||
|
||||
let display: MaybeRef<Renderable> | undefined = undefined;
|
||||
if (typeof _display === "object" && "description" in _display) {
|
||||
const title = processGetter(_display.title);
|
||||
const description = processGetter(_display.description);
|
||||
const effectDisplay = processGetter(_display.effectDisplay);
|
||||
|
||||
const Title = () => (title == null ? <></> : render(title, el => <h3>{el}</h3>));
|
||||
const Description = () => render(description, el => <div>{el}</div>);
|
||||
const EffectDisplay = () =>
|
||||
effectDisplay == null ? <></> : render(effectDisplay, el => <>{el}</>);
|
||||
|
||||
display = computed(() => (
|
||||
<span>
|
||||
{title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
{effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
{bought.value ? null : (
|
||||
<>
|
||||
<br />
|
||||
{displayRequirements(requirements)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
));
|
||||
} else if (_display != null) {
|
||||
display = processGetter(_display);
|
||||
}
|
||||
|
||||
const upgrade = {
|
||||
type: UpgradeType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof UpgradeOptions>),
|
||||
...vueFeature,
|
||||
bought,
|
||||
canPurchase: computed(() => !bought.value && requirementsMet(requirements)),
|
||||
requirements,
|
||||
display,
|
||||
purchase() {
|
||||
if (!unref(upgrade.canPurchase)) {
|
||||
return;
|
||||
}
|
||||
payRequirements(requirements);
|
||||
bought.value = true;
|
||||
options.onPurchase?.();
|
||||
}
|
||||
} satisfies Upgrade;
|
||||
|
||||
return upgrade;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to auto purchase a list of upgrades whenever they're affordable.
|
||||
* @param layer The layer the upgrades are apart of
|
||||
* @param autoActive Whether or not the upgrades should currently be auto-purchasing
|
||||
* @param upgrades The specific upgrades to upgrade. If unspecified, uses all upgrades on the layer.
|
||||
*/
|
||||
export function setupAutoPurchase(
|
||||
layer: Layer,
|
||||
autoActive: MaybeRefOrGetter<boolean>,
|
||||
upgrades: Upgrade[] = []
|
||||
): void {
|
||||
upgrades = upgrades.length === 0 ? (findFeatures(layer, UpgradeType) as Upgrade[]) : upgrades;
|
||||
const isAutoActive: MaybeRef<boolean> = isFunction(autoActive)
|
||||
? computed(autoActive)
|
||||
: autoActive;
|
||||
layer.on("update", () => {
|
||||
if (unref(isAutoActive)) {
|
||||
upgrades.forEach(upgrade => upgrade.purchase());
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
import type { CoercableComponent, OptionsFunc, Replace } from "features/feature";
|
||||
import { setDefault } from "features/feature";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import { InvertibleFormula, InvertibleIntegralFormula } from "game/formulas/types";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import { createBooleanRequirement } from "game/requirements";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable, processComputable } from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { GenericDecorator } from "./decorators/common";
|
||||
import { createBooleanRequirement } from "game/requirements";
|
||||
import { Renderable } from "util/vue";
|
||||
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Conversion} features. */
|
||||
export const ConversionType = Symbol("Conversion");
|
||||
|
||||
/** An object that configures a {@link Conversion}. */
|
||||
export interface ConversionOptions {
|
||||
|
@ -25,24 +25,24 @@ export interface ConversionOptions {
|
|||
* How much of the output resource the conversion can currently convert for.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
*/
|
||||
currentGain?: Computable<DecimalSource>;
|
||||
currentGain?: MaybeRefOrGetter<DecimalSource>;
|
||||
/**
|
||||
* The absolute amount the output resource will be changed by.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
* This will differ from {@link currentGain} in the cases where the conversion isn't just adding the converted amount to the output resource.
|
||||
*/
|
||||
actualGain?: Computable<DecimalSource>;
|
||||
actualGain?: MaybeRefOrGetter<DecimalSource>;
|
||||
/**
|
||||
* The amount of the input resource currently being required in order to produce the {@link currentGain}.
|
||||
* That is, if it went below this value then {@link currentGain} would decrease.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
*/
|
||||
currentAt?: Computable<DecimalSource>;
|
||||
currentAt?: MaybeRefOrGetter<DecimalSource>;
|
||||
/**
|
||||
* The amount of the input resource required to make {@link currentGain} increase.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
*/
|
||||
nextAt?: Computable<DecimalSource>;
|
||||
nextAt?: MaybeRefOrGetter<DecimalSource>;
|
||||
/**
|
||||
* The input {@link features/resources/resource.Resource} for this conversion.
|
||||
*/
|
||||
|
@ -55,7 +55,7 @@ export interface ConversionOptions {
|
|||
* Whether or not to cap the amount of the output resource gained by converting at 1.
|
||||
* Defaults to true.
|
||||
*/
|
||||
buyMax?: Computable<boolean>;
|
||||
buyMax?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* The function that performs the actual conversion from {@link baseResource} to {@link gainResource}.
|
||||
* Typically this will be set for you in a conversion constructor.
|
||||
|
@ -85,28 +85,15 @@ export interface BaseConversion {
|
|||
}
|
||||
|
||||
/** An object that converts one {@link features/resources/resource.Resource} into another at a given rate. */
|
||||
export type Conversion<T extends ConversionOptions> = Replace<
|
||||
T & BaseConversion,
|
||||
export type Conversion = Replace<
|
||||
Replace<ConversionOptions, BaseConversion>,
|
||||
{
|
||||
formula: InvertibleFormula;
|
||||
currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
|
||||
actualGain: GetComputableTypeWithDefault<T["actualGain"], Ref<DecimalSource>>;
|
||||
currentAt: GetComputableTypeWithDefault<T["currentAt"], Ref<DecimalSource>>;
|
||||
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
|
||||
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
|
||||
spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"];
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Conversion} object. */
|
||||
export type GenericConversion = Replace<
|
||||
Conversion<ConversionOptions>,
|
||||
{
|
||||
currentGain: ProcessedComputable<DecimalSource>;
|
||||
actualGain: ProcessedComputable<DecimalSource>;
|
||||
currentAt: ProcessedComputable<DecimalSource>;
|
||||
nextAt: ProcessedComputable<DecimalSource>;
|
||||
buyMax: ProcessedComputable<boolean>;
|
||||
currentGain: MaybeRef<DecimalSource>;
|
||||
actualGain: MaybeRef<DecimalSource>;
|
||||
currentAt: MaybeRef<DecimalSource>;
|
||||
nextAt: MaybeRef<DecimalSource>;
|
||||
buyMax: MaybeRef<boolean>;
|
||||
spend: (amountGained: DecimalSource) => void;
|
||||
}
|
||||
>;
|
||||
|
@ -119,80 +106,78 @@ export type GenericConversion = Replace<
|
|||
* @see {@link createIndependentConversion}.
|
||||
*/
|
||||
export function createConversion<T extends ConversionOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseConversion, GenericConversion>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Conversion<T> {
|
||||
optionsFunc: OptionsFunc<T, BaseConversion, Conversion>
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const conversion = optionsFunc.call(feature, feature);
|
||||
const options = optionsFunc.call(feature, feature as Conversion);
|
||||
const {
|
||||
baseResource,
|
||||
gainResource,
|
||||
formula,
|
||||
currentGain: _currentGain,
|
||||
actualGain,
|
||||
currentAt,
|
||||
nextAt,
|
||||
convert,
|
||||
spend,
|
||||
buyMax,
|
||||
onConvert,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(conversion);
|
||||
}
|
||||
const currentGain =
|
||||
_currentGain == null
|
||||
? computed((): Decimal => {
|
||||
let gain = Decimal.floor(conversion.formula.evaluate(baseResource.value)).max(
|
||||
0
|
||||
);
|
||||
if (unref(conversion.buyMax) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
})
|
||||
: processGetter(_currentGain);
|
||||
|
||||
(conversion as GenericConversion).formula = conversion.formula(
|
||||
Formula.variable(conversion.baseResource)
|
||||
);
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
let gain = Decimal.floor(
|
||||
(conversion as GenericConversion).formula.evaluate(
|
||||
conversion.baseResource.value
|
||||
)
|
||||
).max(0);
|
||||
if (unref(conversion.buyMax) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
}
|
||||
if (conversion.actualGain == null) {
|
||||
conversion.actualGain = conversion.currentGain;
|
||||
}
|
||||
if (conversion.currentAt == null) {
|
||||
conversion.currentAt = computed(() => {
|
||||
return (conversion as GenericConversion).formula.invert(
|
||||
Decimal.floor(unref((conversion as GenericConversion).currentGain))
|
||||
);
|
||||
});
|
||||
}
|
||||
if (conversion.nextAt == null) {
|
||||
conversion.nextAt = computed(() => {
|
||||
return (conversion as GenericConversion).formula.invert(
|
||||
Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1)
|
||||
);
|
||||
});
|
||||
}
|
||||
const conversion = {
|
||||
type: ConversionType,
|
||||
...(props as Omit<typeof props, keyof ConversionOptions>),
|
||||
baseResource,
|
||||
gainResource,
|
||||
formula: formula(Formula.variable(baseResource)),
|
||||
currentGain,
|
||||
actualGain: actualGain == null ? currentGain : processGetter(actualGain),
|
||||
currentAt:
|
||||
currentAt == null
|
||||
? computed(
|
||||
(): DecimalSource =>
|
||||
conversion.formula.invert(
|
||||
Decimal.floor(unref(conversion.currentGain))
|
||||
)
|
||||
)
|
||||
: processGetter(currentAt),
|
||||
nextAt:
|
||||
nextAt == null
|
||||
? computed(
|
||||
(): DecimalSource =>
|
||||
conversion.formula.invert(
|
||||
Decimal.floor(unref(conversion.currentGain)).add(1)
|
||||
)
|
||||
)
|
||||
: processGetter(nextAt),
|
||||
convert:
|
||||
convert ??
|
||||
function () {
|
||||
const amountGained = unref(conversion.currentGain);
|
||||
gainResource.value = Decimal.add(gainResource.value, amountGained);
|
||||
conversion.spend(amountGained);
|
||||
onConvert?.(amountGained);
|
||||
},
|
||||
spend: spend ?? (() => (baseResource.value = 0)),
|
||||
buyMax: processGetter(buyMax) ?? true,
|
||||
onConvert
|
||||
} satisfies Conversion;
|
||||
|
||||
if (conversion.convert == null) {
|
||||
conversion.convert = function () {
|
||||
const amountGained = unref((conversion as GenericConversion).currentGain);
|
||||
conversion.gainResource.value = Decimal.add(
|
||||
conversion.gainResource.value,
|
||||
amountGained
|
||||
);
|
||||
(conversion as GenericConversion).spend(amountGained);
|
||||
conversion.onConvert?.(amountGained);
|
||||
};
|
||||
}
|
||||
|
||||
if (conversion.spend == null) {
|
||||
conversion.spend = function () {
|
||||
conversion.baseResource.value = 0;
|
||||
};
|
||||
}
|
||||
|
||||
processComputable(conversion as T, "currentGain");
|
||||
processComputable(conversion as T, "actualGain");
|
||||
processComputable(conversion as T, "currentAt");
|
||||
processComputable(conversion as T, "nextAt");
|
||||
processComputable(conversion as T, "buyMax");
|
||||
setDefault(conversion, "buyMax", true);
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(conversion);
|
||||
}
|
||||
|
||||
return conversion as unknown as Conversion<T>;
|
||||
return conversion;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -203,8 +188,8 @@ export function createConversion<T extends ConversionOptions>(
|
|||
* @param optionsFunc Conversion options.
|
||||
*/
|
||||
export function createCumulativeConversion<S extends ConversionOptions>(
|
||||
optionsFunc: OptionsFunc<S, BaseConversion, GenericConversion>
|
||||
): Conversion<S> {
|
||||
optionsFunc: OptionsFunc<S, BaseConversion, Conversion>
|
||||
) {
|
||||
return createConversion(optionsFunc);
|
||||
}
|
||||
|
||||
|
@ -214,54 +199,46 @@ export function createCumulativeConversion<S extends ConversionOptions>(
|
|||
* @param optionsFunc Converison options.
|
||||
*/
|
||||
export function createIndependentConversion<S extends ConversionOptions>(
|
||||
optionsFunc: OptionsFunc<S, BaseConversion, GenericConversion>
|
||||
): Conversion<S> {
|
||||
optionsFunc: OptionsFunc<S, BaseConversion, Conversion>
|
||||
) {
|
||||
return createConversion(feature => {
|
||||
const conversion: S = optionsFunc.call(feature, feature);
|
||||
const conversion = optionsFunc.call(feature, feature);
|
||||
|
||||
setDefault(conversion, "buyMax", false);
|
||||
conversion.buyMax ??= false;
|
||||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
let gain = Decimal.floor(
|
||||
(conversion as unknown as GenericConversion).formula.evaluate(
|
||||
conversion.baseResource.value
|
||||
)
|
||||
).max(conversion.gainResource.value);
|
||||
if (unref(conversion.buyMax as ProcessedComputable<boolean>) === false) {
|
||||
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
}
|
||||
if (conversion.actualGain == null) {
|
||||
conversion.actualGain = computed(() => {
|
||||
let gain = Decimal.sub(
|
||||
(conversion as unknown as GenericConversion).formula.evaluate(
|
||||
conversion.baseResource.value
|
||||
),
|
||||
conversion.gainResource.value
|
||||
)
|
||||
.floor()
|
||||
.max(0);
|
||||
|
||||
if (unref(conversion.buyMax as ProcessedComputable<boolean>) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
}
|
||||
setDefault(conversion, "convert", function () {
|
||||
const amountGained = unref((conversion as unknown as GenericConversion).actualGain);
|
||||
conversion.gainResource.value = unref(
|
||||
(conversion as unknown as GenericConversion).currentGain
|
||||
conversion.currentGain ??= computed(() => {
|
||||
let gain = Decimal.floor(feature.formula.evaluate(conversion.baseResource.value)).max(
|
||||
conversion.gainResource.value
|
||||
);
|
||||
(conversion as unknown as GenericConversion).spend(amountGained);
|
||||
conversion.onConvert?.(amountGained);
|
||||
if (unref(conversion.buyMax as MaybeRef<boolean>) === false) {
|
||||
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
|
||||
conversion.actualGain ??= computed(() => {
|
||||
let gain = Decimal.sub(
|
||||
feature.formula.evaluate(conversion.baseResource.value),
|
||||
conversion.gainResource.value
|
||||
)
|
||||
.floor()
|
||||
.max(0);
|
||||
|
||||
if (unref(conversion.buyMax as MaybeRef<boolean>) === false) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
|
||||
conversion.convert ??= function () {
|
||||
const amountGained = unref(feature.actualGain);
|
||||
conversion.gainResource.value = unref(feature.currentGain);
|
||||
feature.spend(amountGained);
|
||||
feature.onConvert?.(amountGained);
|
||||
};
|
||||
|
||||
return conversion;
|
||||
}) as Conversion<S>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,12 +252,12 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
|||
*/
|
||||
export function setupPassiveGeneration(
|
||||
layer: BaseLayer,
|
||||
conversion: GenericConversion,
|
||||
rate: Computable<DecimalSource> = 1,
|
||||
cap: Computable<DecimalSource> = Decimal.dInf
|
||||
conversion: Conversion,
|
||||
rate: MaybeRefOrGetter<DecimalSource> = 1,
|
||||
cap: MaybeRefOrGetter<DecimalSource> = Decimal.dInf
|
||||
): void {
|
||||
const processedRate = convertComputable(rate);
|
||||
const processedCap = convertComputable(cap);
|
||||
const processedRate = processGetter(rate);
|
||||
const processedCap = processGetter(cap);
|
||||
layer.on("preUpdate", diff => {
|
||||
const currRate = unref(processedRate);
|
||||
if (Decimal.neq(currRate, 0)) {
|
||||
|
@ -300,11 +277,11 @@ export function setupPassiveGeneration(
|
|||
* @param minGainAmount The minimum gain amount that must be met for the requirement to be met
|
||||
*/
|
||||
export function createCanConvertRequirement(
|
||||
conversion: GenericConversion,
|
||||
minGainAmount: Computable<DecimalSource> = 1,
|
||||
display?: CoercableComponent
|
||||
conversion: Conversion,
|
||||
minGainAmount: MaybeRefOrGetter<DecimalSource> = 1,
|
||||
display?: MaybeRefOrGetter<Renderable>
|
||||
) {
|
||||
const computedMinGainAmount = convertComputable(minGainAmount);
|
||||
const computedMinGainAmount = processGetter(minGainAmount);
|
||||
return createBooleanRequirement(
|
||||
() => Decimal.gte(unref(conversion.actualGain), unref(computedMinGainAmount)),
|
||||
display
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
import { Replace } from "features/feature";
|
||||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
ProcessedComputable,
|
||||
processComputable
|
||||
} from "util/computed";
|
||||
import { Ref, computed, unref } from "vue";
|
||||
import { Decorator } from "./common";
|
||||
|
||||
export interface BonusAmountFeatureOptions {
|
||||
bonusAmount: Computable<DecimalSource>;
|
||||
totalAmount?: Computable<DecimalSource>;
|
||||
}
|
||||
export interface BonusCompletionsFeatureOptions {
|
||||
bonusCompletions: Computable<DecimalSource>;
|
||||
totalCompletions?: Computable<DecimalSource>;
|
||||
}
|
||||
|
||||
export interface BaseBonusAmountFeature {
|
||||
amount: Ref<DecimalSource>;
|
||||
bonusAmount: ProcessedComputable<DecimalSource>;
|
||||
totalAmount?: Ref<DecimalSource>;
|
||||
}
|
||||
export interface BaseBonusCompletionsFeature {
|
||||
completions: Ref<DecimalSource>;
|
||||
bonusCompletions: ProcessedComputable<DecimalSource>;
|
||||
totalCompletions?: Ref<DecimalSource>;
|
||||
}
|
||||
|
||||
export type BonusAmountFeature<T extends BonusAmountFeatureOptions> = Replace<
|
||||
T,
|
||||
{ bonusAmount: GetComputableType<T["bonusAmount"]> }
|
||||
>;
|
||||
export type BonusCompletionsFeature<T extends BonusCompletionsFeatureOptions> = Replace<
|
||||
T,
|
||||
{ bonusAmount: GetComputableType<T["bonusCompletions"]> }
|
||||
>;
|
||||
|
||||
export type GenericBonusAmountFeature = Replace<
|
||||
BonusAmountFeature<BonusAmountFeatureOptions>,
|
||||
{
|
||||
bonusAmount: ProcessedComputable<DecimalSource>;
|
||||
totalAmount: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
export type GenericBonusCompletionsFeature = Replace<
|
||||
BonusCompletionsFeature<BonusCompletionsFeatureOptions>,
|
||||
{
|
||||
bonusCompletions: ProcessedComputable<DecimalSource>;
|
||||
totalCompletions: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount".
|
||||
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusAmountFeatureOptions}.
|
||||
* Additionally, the base feature must have an `amount` property.
|
||||
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusAmountFeature}.
|
||||
* @example ```ts
|
||||
* createRepeatable<RepeatableOptions & BonusAmountFeatureOptions>(() => ({
|
||||
* bonusAmount: noPersist(otherRepeatable.amount),
|
||||
* ...
|
||||
* }), bonusAmountDecorator) as GenericRepeatable & GenericBonusAmountFeature
|
||||
*/
|
||||
export const bonusAmountDecorator: Decorator<
|
||||
BonusAmountFeatureOptions,
|
||||
BaseBonusAmountFeature,
|
||||
GenericBonusAmountFeature
|
||||
> = {
|
||||
postConstruct(feature) {
|
||||
if (feature.amount === undefined) {
|
||||
console.error(
|
||||
`Decorated feature ${feature.id} does not contain the required 'amount' property"`
|
||||
);
|
||||
}
|
||||
processComputable(feature, "bonusAmount");
|
||||
if (feature.totalAmount === undefined) {
|
||||
feature.totalAmount = computed(() =>
|
||||
Decimal.add(
|
||||
unref(feature.amount ?? 0),
|
||||
unref(feature.bonusAmount as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount".
|
||||
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusCompletionFeatureOptions}.
|
||||
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusCompletionFeature}.
|
||||
* @example ```ts
|
||||
* createChallenge<ChallengeOptions & BonusCompletionFeatureOptions>(() => ({
|
||||
* bonusCompletions: noPersist(otherChallenge.completions),
|
||||
* ...
|
||||
* }), bonusCompletionDecorator) as GenericChallenge & GenericBonusCompletionFeature
|
||||
* ```
|
||||
*/
|
||||
export const bonusCompletionsDecorator: Decorator<
|
||||
BonusCompletionsFeatureOptions,
|
||||
BaseBonusCompletionsFeature,
|
||||
GenericBonusCompletionsFeature
|
||||
> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "bonusCompletions");
|
||||
if (feature.totalCompletions === undefined) {
|
||||
feature.totalCompletions = computed(() =>
|
||||
Decimal.add(
|
||||
unref(feature.completions ?? 0),
|
||||
unref(feature.bonusCompletions as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
import { Replace, OptionsObject } from "../feature";
|
||||
import {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
processComputable,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { Persistent, State } from "game/persistence";
|
||||
|
||||
export type Decorator<
|
||||
FeatureOptions,
|
||||
BaseFeature = object,
|
||||
GenericFeature = BaseFeature,
|
||||
S extends State = State
|
||||
> = {
|
||||
getPersistentData?(): Record<string, Persistent<S>>;
|
||||
preConstruct?(
|
||||
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||
): void;
|
||||
postConstruct?(
|
||||
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||
): void;
|
||||
getGatheredProps?(
|
||||
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||
): Partial<OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>>;
|
||||
};
|
||||
|
||||
export type GenericDecorator = Decorator<unknown>;
|
||||
|
||||
export interface EffectFeatureOptions<T = unknown> {
|
||||
effect: Computable<T>;
|
||||
}
|
||||
|
||||
export type EffectFeature<T extends EffectFeatureOptions> = Replace<
|
||||
T,
|
||||
{ effect: GetComputableType<T["effect"]> }
|
||||
>;
|
||||
|
||||
export type GenericEffectFeature<T = unknown> = Replace<
|
||||
EffectFeature<EffectFeatureOptions>,
|
||||
{ effect: ProcessedComputable<T> }
|
||||
>;
|
||||
|
||||
/**
|
||||
* Allows the usage of an `effect` field in the decorated feature.
|
||||
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode EffectFeatureOptions}.
|
||||
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericEffectFeature}.
|
||||
* @example ```ts
|
||||
* createRepeatable<RepeatableOptions & EffectFeatureOptions>(() => ({
|
||||
* effect() { return Decimal.pow(2, this.amount); },
|
||||
* ...
|
||||
* }), effectDecorator) as GenericUpgrade & GenericEffectFeature;
|
||||
* ```
|
||||
*/
|
||||
export const effectDecorator: Decorator<EffectFeatureOptions, unknown, GenericEffectFeature> = {
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "effect");
|
||||
}
|
||||
};
|
|
@ -1,39 +1,6 @@
|
|||
import Decimal from "util/bignum";
|
||||
import { DoNotCache, ProcessedComputable } from "util/computed";
|
||||
import type { CSSProperties, DefineComponent } from "vue";
|
||||
import { isRef, unref } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
|
||||
/**
|
||||
* A symbol to use as a key for a vue component a feature can be rendered with
|
||||
* @see {@link util/vue.VueFeature}
|
||||
*/
|
||||
export const Component = Symbol("Component");
|
||||
/**
|
||||
* A symbol to use as a key for a prop gathering function that a feature can use to send to its component
|
||||
* @see {@link util/vue.VueFeature}
|
||||
*/
|
||||
export const GatherProps = Symbol("GatherProps");
|
||||
|
||||
/**
|
||||
* A type referring to a function that returns JSX and is marked that it shouldn't be wrapped in a ComputedRef
|
||||
* @see {@link jsx}
|
||||
*/
|
||||
export type JSXFunction = (() => JSX.Element) & { [DoNotCache]: true };
|
||||
/**
|
||||
* Any value that can be coerced into (or is) a vue component
|
||||
*/
|
||||
export type CoercableComponent = string | DefineComponent | JSXFunction;
|
||||
/**
|
||||
* Any value that can be passed into an HTML element's style attribute.
|
||||
* Note that Profectus uses its own StyleValue and CSSProperties that are extended,
|
||||
* in order to have additional properties added to them, such as variable CSS variables.
|
||||
*/
|
||||
export type StyleValue = string | CSSProperties | Array<string | CSSProperties>;
|
||||
|
||||
/** A type that refers to any vue component */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type GenericComponent = DefineComponent<any, any, any>;
|
||||
import { Renderable, renderCol, VueFeature } from "util/vue";
|
||||
import { computed, isRef, MaybeRef, Ref, unref } from "vue";
|
||||
|
||||
/** Utility type that is S, with any properties from T that aren't already present in S */
|
||||
export type Replace<T, S> = S & Omit<T, keyof S>;
|
||||
|
@ -43,7 +10,7 @@ export type Replace<T, S> = S & Omit<T, keyof S>;
|
|||
* with "this" bound to what the type will eventually be processed into.
|
||||
* Intended for making lazily evaluated objects.
|
||||
*/
|
||||
export type OptionsFunc<T, R = unknown, S = R> = (obj: R) => OptionsObject<T, R, S>;
|
||||
export type OptionsFunc<T, R = unknown, S = R> = (obj: S) => OptionsObject<T, R, S>;
|
||||
|
||||
export type OptionsObject<T, R = unknown, S = R> = T & Partial<R> & ThisType<T & S>;
|
||||
|
||||
|
@ -68,34 +35,18 @@ export enum Visibility {
|
|||
None
|
||||
}
|
||||
|
||||
export function isVisible(visibility: ProcessedComputable<Visibility | boolean>) {
|
||||
export function isVisible(visibility: MaybeRef<Visibility | boolean>) {
|
||||
const currVisibility = unref(visibility);
|
||||
return currVisibility !== Visibility.None && currVisibility !== false;
|
||||
}
|
||||
|
||||
export function isHidden(visibility: ProcessedComputable<Visibility | boolean>) {
|
||||
export function isHidden(visibility: MaybeRef<Visibility | boolean>) {
|
||||
const currVisibility = unref(visibility);
|
||||
return currVisibility === Visibility.Hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a function and marks it as JSX so it won't get auto-wrapped into a ComputedRef.
|
||||
* The function may also return empty string as empty JSX tags cause issues.
|
||||
*/
|
||||
export function jsx(func: () => JSX.Element | ""): JSXFunction {
|
||||
(func as Partial<JSXFunction>)[DoNotCache] = true;
|
||||
return func as JSXFunction;
|
||||
}
|
||||
|
||||
/** Utility function to set a property on an object if and only if it doesn't already exist */
|
||||
export function setDefault<T, K extends keyof T>(
|
||||
object: T,
|
||||
key: K,
|
||||
value: T[K]
|
||||
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
||||
if (object[key] == null && value != null) {
|
||||
object[key] = value;
|
||||
}
|
||||
export function isType<T extends symbol>(object: unknown, type: T): object is { type: T } {
|
||||
return object != null && typeof object === "object" && "type" in object && object.type === type;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,6 +73,24 @@ export function findFeatures(obj: Record<string, unknown>, ...types: symbol[]):
|
|||
return objects;
|
||||
}
|
||||
|
||||
export function getFirstFeature<T extends VueFeature>(
|
||||
features: T[],
|
||||
filter: (feature: T) => boolean
|
||||
): {
|
||||
firstFeature: Ref<T | undefined>;
|
||||
collapsedContent: MaybeRef<Renderable>;
|
||||
hasCollapsedContent: Ref<boolean>;
|
||||
} {
|
||||
const filteredFeatures = computed(() =>
|
||||
features.filter(feature => isVisible(feature.visibility ?? true) && filter(feature))
|
||||
);
|
||||
return {
|
||||
firstFeature: computed(() => filteredFeatures.value[0]),
|
||||
collapsedContent: computed(() => renderCol(...filteredFeatures.value.slice(1))),
|
||||
hasCollapsedContent: computed(() => filteredFeatures.value.length > 1)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses an object and returns all features that are _not_ any of the given types.
|
||||
* Features are any object with a "type" property that has a symbol value.
|
||||
|
|
|
@ -1,41 +1,26 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
:style="{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
}"
|
||||
class="table-grid"
|
||||
>
|
||||
<div v-for="row in unref(rows)" class="row-grid" :class="{ mergeAdjacent }" :key="row">
|
||||
<GridCellVue
|
||||
v-for="col in unref(cols)"
|
||||
:key="col"
|
||||
v-bind="gatherCellProps(unref(cells)[row * 100 + col])"
|
||||
/>
|
||||
</div>
|
||||
<div class="table-grid">
|
||||
<Cells />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import "components/common/table.css";
|
||||
import themes from "data/themes";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { GridCell } from "features/grids/grid";
|
||||
import type { Grid } from "features/grids/grid";
|
||||
import settings from "game/settings";
|
||||
import { render } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import GridCellVue from "./GridCell.vue";
|
||||
|
||||
defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
rows: number;
|
||||
cols: number;
|
||||
cells: Record<string, GridCell>;
|
||||
const props = defineProps<{
|
||||
rows: Grid["rows"];
|
||||
cols: Grid["cols"];
|
||||
cells: Grid["cells"];
|
||||
}>();
|
||||
|
||||
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 };
|
||||
}
|
||||
const Cells = () => new Array(unref(props.rows)).fill(0).map((_, row) => <div class={{ "row-grid": true, mergeAdjacent: mergeAdjacent.value }}>
|
||||
{new Array(unref(props.cols)).map((_, col) => render(props.cells[row][col]))}
|
||||
</div>);
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="isVisible(visibility)"
|
||||
:class="{ feature: true, tile: true, can: unref(canClick), locked: !unref(canClick) }"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
:class="{ tile: true, can: unref(canClick), locked: !unref(canClick) }"
|
||||
@click="onClick"
|
||||
@mousedown="start"
|
||||
@mouseleave="stop"
|
||||
|
@ -16,40 +9,32 @@
|
|||
@touchend.passive="stop"
|
||||
@touchcancel.passive="stop"
|
||||
>
|
||||
<div v-if="title"><component :is="titleComponent" /></div>
|
||||
<component :is="component" style="white-space: pre-line" />
|
||||
<Node :id="id" />
|
||||
<div v-if="title"><Title /></div>
|
||||
<Component style="white-space: pre-line" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import Node from "components/Node.vue";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import {
|
||||
computeComponent,
|
||||
computeOptionalComponent,
|
||||
render,
|
||||
setupHoldToClick
|
||||
} from "util/vue";
|
||||
import { toRef, unref } from "vue";
|
||||
import { GridCell } from "./grid";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
onHold?: VoidFunction;
|
||||
display: CoercableComponent;
|
||||
title?: CoercableComponent;
|
||||
style?: StyleValue;
|
||||
canClick: boolean;
|
||||
id: string;
|
||||
onClick: GridCell["onClick"];
|
||||
onHold: GridCell["onHold"];
|
||||
display: GridCell["display"];
|
||||
title: GridCell["title"];
|
||||
canClick: GridCell["canClick"];
|
||||
}>();
|
||||
|
||||
|
||||
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||
|
||||
const titleComponent = computeOptionalComponent(toRef(props, "title"));
|
||||
const component = computeComponent(toRef(props, "display"));
|
||||
const Title = () => props.title == null ? <></> : render(props.title);
|
||||
const Component = () => render(props.display);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,370 +0,0 @@
|
|||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import GridComponent from "features/grids/Grid.vue";
|
||||
import type { Persistent, State } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import { isFunction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Grid} features. */
|
||||
export const GridType = Symbol("Grid");
|
||||
|
||||
/** A type representing a computable value for a cell in the grid. */
|
||||
export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T);
|
||||
|
||||
/** Create proxy to more easily get the properties of cells on a grid. */
|
||||
function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> {
|
||||
return new Proxy({}, getGridHandler(grid)) as Record<string | number, GridCell>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns traps for a proxy that will give cell proxies when accessing any numerical key.
|
||||
* @param grid The grid to get the cells from.
|
||||
* @see {@link createGridProxy}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number, GridCell>> {
|
||||
const keys = computed(() => {
|
||||
const keys = [];
|
||||
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: Record<string | number, GridCell>, key: PropertyKey) {
|
||||
if (key === "isProxy") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof key === "symbol") {
|
||||
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 GridCell;
|
||||
}
|
||||
|
||||
return target[key];
|
||||
},
|
||||
// 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: 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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns traps for a proxy that will get the properties for the specified cell
|
||||
* @param id The grid cell ID to get properties from.
|
||||
* @see {@link getGridHandler}
|
||||
* @see {@link createGridProxy}
|
||||
*/
|
||||
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 {
|
||||
if (key === "isProxy") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let prop = (target as any)[key];
|
||||
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
}
|
||||
if (prop != null || typeof key === "symbol") {
|
||||
return prop;
|
||||
}
|
||||
|
||||
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)) {
|
||||
if (!(key in cache)) {
|
||||
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
||||
}
|
||||
return cache[key].value;
|
||||
} else if (prop != null) {
|
||||
return unref(prop);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
prop = (target as any)[`on${key}`];
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
} else if (prop != null) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (target as any)[key];
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a cell within a grid. These properties will typically be accessed via a cell proxy that calls functions on the grid to get the properties for a specific cell.
|
||||
* @see {@link createGridProxy}
|
||||
*/
|
||||
export interface GridCell {
|
||||
/** A unique identifier for the grid cell. */
|
||||
id: string;
|
||||
/** Whether this cell should be visible. */
|
||||
visibility: Visibility | boolean;
|
||||
/** Whether this cell can be clicked. */
|
||||
canClick: boolean;
|
||||
/** The initial persistent state of this cell. */
|
||||
startState: State;
|
||||
/** The persistent state of this cell. */
|
||||
state: State;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: StyleValue;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Record<string, boolean>;
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
/** The main text that appears in the display. */
|
||||
display: CoercableComponent;
|
||||
/** A function that is called when the cell is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the cell is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Grid}.
|
||||
*/
|
||||
export interface GridOptions {
|
||||
/** Whether this grid should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The number of rows in the grid. */
|
||||
rows: Computable<number>;
|
||||
/** The number of columns in the grid. */
|
||||
cols: Computable<number>;
|
||||
/** A computable to determine the visibility of a cell. */
|
||||
getVisibility?: CellComputable<Visibility | boolean>;
|
||||
/** A computable to determine if a cell can be clicked. */
|
||||
getCanClick?: CellComputable<boolean>;
|
||||
/** A computable to get the initial persistent state of a cell. */
|
||||
getStartState: Computable<State> | ((id: string | number) => State);
|
||||
/** A computable to get the CSS styles for a cell. */
|
||||
getStyle?: CellComputable<StyleValue>;
|
||||
/** A computable to get the CSS classes for a cell. */
|
||||
getClasses?: CellComputable<Record<string, boolean>>;
|
||||
/** A computable to get the title component for a cell. */
|
||||
getTitle?: CellComputable<CoercableComponent>;
|
||||
/** A computable to get the display component for a cell. */
|
||||
getDisplay: CellComputable<CoercableComponent>;
|
||||
/** A function that is called when a cell is clicked. */
|
||||
onClick?: (id: string | number, state: State, e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when a cell is held down. */
|
||||
onHold?: (id: string | number, state: State) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
|
||||
*/
|
||||
export interface BaseGrid {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
|
||||
getID: (id: string | number, state: State) => string;
|
||||
/** Get the persistent state of the given cell. */
|
||||
getState: (id: string | number) => State;
|
||||
/** Set the persistent state of the given cell. */
|
||||
setState: (id: string | number, state: State) => void;
|
||||
/** A dictionary of cells within this grid. */
|
||||
cells: Record<string | number, GridCell>;
|
||||
/** The persistent state of this grid, which is a dictionary of cell states. */
|
||||
cellState: Persistent<Record<string | number, State>>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof GridType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
|
||||
export type Grid<T extends GridOptions> = Replace<
|
||||
T & BaseGrid,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
rows: GetComputableType<T["rows"]>;
|
||||
cols: GetComputableType<T["cols"]>;
|
||||
getVisibility: GetComputableTypeWithDefault<T["getVisibility"], Visibility.Visible>;
|
||||
getCanClick: GetComputableTypeWithDefault<T["getCanClick"], true>;
|
||||
getStartState: GetComputableType<T["getStartState"]>;
|
||||
getStyle: GetComputableType<T["getStyle"]>;
|
||||
getClasses: GetComputableType<T["getClasses"]>;
|
||||
getTitle: GetComputableType<T["getTitle"]>;
|
||||
getDisplay: GetComputableType<T["getDisplay"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Grid} object. */
|
||||
export type GenericGrid = Replace<
|
||||
Grid<GridOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
getVisibility: ProcessedComputable<Visibility | boolean>;
|
||||
getCanClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a grid with the given options.
|
||||
* @param optionsFunc Grid options.
|
||||
*/
|
||||
export function createGrid<T extends GridOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseGrid, GenericGrid>
|
||||
): Grid<T> {
|
||||
const cellState = persistent<Record<string | number, State>>({}, false);
|
||||
return createLazyProxy(feature => {
|
||||
const grid = optionsFunc.call(feature, feature);
|
||||
grid.id = getUniqueID("grid-");
|
||||
grid[Component] = GridComponent as GenericComponent;
|
||||
|
||||
grid.cellState = cellState;
|
||||
|
||||
grid.getID = function (this: GenericGrid, cell: string | number) {
|
||||
return grid.id + "-" + cell;
|
||||
};
|
||||
grid.getState = function (this: GenericGrid, cell: string | number) {
|
||||
if (this.cellState.value[cell] != null) {
|
||||
return cellState.value[cell];
|
||||
}
|
||||
return this.cells[cell].startState;
|
||||
};
|
||||
grid.setState = function (this: GenericGrid, cell: string | number, state: State) {
|
||||
cellState.value[cell] = state;
|
||||
};
|
||||
|
||||
grid.cells = createGridProxy(grid as GenericGrid);
|
||||
|
||||
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");
|
||||
|
||||
if (grid.onClick) {
|
||||
const onClick = grid.onClick.bind(grid);
|
||||
grid.onClick = function (id, state, e) {
|
||||
if (unref((grid as GenericGrid).cells[id].canClick)) {
|
||||
onClick(id, state, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (grid.onHold) {
|
||||
const onHold = grid.onHold.bind(grid);
|
||||
grid.onHold = function (id, state) {
|
||||
if (unref((grid as GenericGrid).cells[id].canClick)) {
|
||||
onHold(id, state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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>;
|
||||
});
|
||||
}
|
440
src/features/grids/grid.tsx
Normal file
440
src/features/grids/grid.tsx
Normal file
|
@ -0,0 +1,440 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { Visibility } from "features/feature";
|
||||
import Grid from "features/grids/Grid.vue";
|
||||
import type { Persistent, State } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import { isFunction } from "util/common";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import GridCell from "./GridCell.vue";
|
||||
|
||||
/** A symbol used to identify {@link Grid} features. */
|
||||
export const GridType = Symbol("Grid");
|
||||
|
||||
/** A type representing a MaybeRefOrGetter value for a cell in the grid. */
|
||||
export type CellMaybeRefOrGetter<T> =
|
||||
| MaybeRefOrGetter<T>
|
||||
| ((row: number, col: number, state: State) => T);
|
||||
export type ProcessedCellRefOrGetter<T> =
|
||||
| MaybeRef<T>
|
||||
| ((row: number, col: number, state: State) => T);
|
||||
|
||||
/**
|
||||
* Represents a cell within a grid. These properties will typically be accessed via a cell proxy that calls functions on the grid to get the properties for a specific cell.
|
||||
* @see {@link createGridProxy}
|
||||
*/
|
||||
export interface GridCell extends VueFeature {
|
||||
/** Which roe in the grid this cell is from. */
|
||||
row: number;
|
||||
/** Which col in the grid this cell is from. */
|
||||
col: number;
|
||||
/** Whether this cell can be clicked. */
|
||||
canClick: boolean;
|
||||
/** The initial persistent state of this cell. */
|
||||
startState: State;
|
||||
/** The persistent state of this cell. */
|
||||
state: State;
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: MaybeRef<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
display: MaybeRef<Renderable>;
|
||||
/** A function that is called when the cell is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the cell is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Grid}.
|
||||
*/
|
||||
export interface GridOptions extends VueFeatureOptions {
|
||||
/** The number of rows in the grid. */
|
||||
rows: MaybeRefOrGetter<number>;
|
||||
/** The number of columns in the grid. */
|
||||
cols: MaybeRefOrGetter<number>;
|
||||
/** A MaybeRefOrGetter to determine the visibility of a cell. */
|
||||
getVisibility?: CellMaybeRefOrGetter<Visibility | boolean>;
|
||||
/** A MaybeRefOrGetter to determine if a cell can be clicked. */
|
||||
getCanClick?: CellMaybeRefOrGetter<boolean>;
|
||||
/** A MaybeRefOrGetter to get the initial persistent state of a cell. */
|
||||
getStartState: MaybeRefOrGetter<State> | ((row: number, col: number) => State);
|
||||
/** A MaybeRefOrGetter to get the CSS styles for a cell. */
|
||||
getStyle?: CellMaybeRefOrGetter<CSSProperties>;
|
||||
/** A MaybeRefOrGetter to get the CSS classes for a cell. */
|
||||
getClasses?: CellMaybeRefOrGetter<Record<string, boolean>>;
|
||||
/** A MaybeRefOrGetter to get the title component for a cell. */
|
||||
getTitle?: CellMaybeRefOrGetter<MaybeRefOrGetter<Renderable>>;
|
||||
/** A MaybeRefOrGetter to get the display component for a cell. */
|
||||
getDisplay: CellMaybeRefOrGetter<MaybeRefOrGetter<Renderable>>;
|
||||
/** A function that is called when a cell is clicked. */
|
||||
onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when a cell is held down. */
|
||||
onHold?: (row: number, col: number, state: State) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
|
||||
*/
|
||||
export interface BaseGrid extends VueFeature {
|
||||
/** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
|
||||
getID: (row: number, col: number, state: State) => string;
|
||||
/** Get the persistent state of the given cell. */
|
||||
getState: (row: number, col: number) => State;
|
||||
/** Set the persistent state of the given cell. */
|
||||
setState: (row: number, col: number, state: State) => void;
|
||||
/** A dictionary of cells within this grid. */
|
||||
cells: GridCell[][];
|
||||
/** The persistent state of this grid, which is a dictionary of cell states. */
|
||||
cellState: Persistent<Record<number, Record<number, State>>>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof GridType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
|
||||
export type Grid = Replace<
|
||||
Replace<GridOptions, BaseGrid>,
|
||||
{
|
||||
getVisibility: ProcessedCellRefOrGetter<Visibility | boolean>;
|
||||
getCanClick: ProcessedCellRefOrGetter<boolean>;
|
||||
rows: ProcessedRefOrGetter<GridOptions["rows"]>;
|
||||
cols: ProcessedRefOrGetter<GridOptions["cols"]>;
|
||||
getStartState: MaybeRef<State> | ((row: number, col: number) => State);
|
||||
getStyle: ProcessedCellRefOrGetter<GridOptions["getStyle"]>;
|
||||
getClasses: ProcessedCellRefOrGetter<GridOptions["getClasses"]>;
|
||||
getTitle: ProcessedCellRefOrGetter<GridOptions["getTitle"]>;
|
||||
getDisplay: ProcessedCellRefOrGetter<GridOptions["getDisplay"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
function getCellRowHandler(grid: Grid, row: number) {
|
||||
return new Proxy({} as GridCell[], {
|
||||
get(target, key) {
|
||||
if (key === "isProxy") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "length") {
|
||||
return unref(grid.cols);
|
||||
}
|
||||
|
||||
const keyNum = parseInt(key);
|
||||
if (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols)) {
|
||||
if (keyNum in target) {
|
||||
return target[keyNum];
|
||||
}
|
||||
return (target[keyNum] = getCellHandler(grid, row, keyNum));
|
||||
}
|
||||
},
|
||||
set(target, key, value) {
|
||||
console.warn("Cannot set grid cells", target, key, value);
|
||||
return false;
|
||||
},
|
||||
ownKeys() {
|
||||
return [...new Array(unref(grid.cols)).fill(0).map((_, i) => "" + i), "length"];
|
||||
},
|
||||
has(target, key) {
|
||||
if (key === "length") {
|
||||
return true;
|
||||
}
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
const keyNum = parseInt(key);
|
||||
if (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if (typeof key !== "string") {
|
||||
return;
|
||||
}
|
||||
const keyNum = parseInt(key);
|
||||
if (key !== "length" && (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols))) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns traps for a proxy that will get the properties for the specified cell
|
||||
* @param id The grid cell ID to get properties from.
|
||||
* @see {@link getGridHandler}
|
||||
* @see {@link createGridProxy}
|
||||
*/
|
||||
function getCellHandler(grid: Grid, row: number, col: number): GridCell {
|
||||
const keys = [
|
||||
"id",
|
||||
"visibility",
|
||||
"classes",
|
||||
"style",
|
||||
"components",
|
||||
"wrappers",
|
||||
VueFeature,
|
||||
"row",
|
||||
"col",
|
||||
"canClick",
|
||||
"startState",
|
||||
"state",
|
||||
"title",
|
||||
"display",
|
||||
"onClick",
|
||||
"onHold"
|
||||
] as const;
|
||||
const cache: Record<string, Ref<unknown>> = {};
|
||||
return new Proxy({} as GridCell, {
|
||||
// The typing in this function is absolutely atrocious in order to support custom properties
|
||||
get(target, key, receiver) {
|
||||
switch (key) {
|
||||
case "isProxy":
|
||||
return true;
|
||||
case "wrappers":
|
||||
return [];
|
||||
case VueFeature:
|
||||
return true;
|
||||
case "row":
|
||||
return row;
|
||||
case "col":
|
||||
return col;
|
||||
case "startState": {
|
||||
if (typeof grid.getStartState === "function") {
|
||||
return grid.getStartState(row, col);
|
||||
}
|
||||
return unref(grid.getStartState);
|
||||
}
|
||||
case "state": {
|
||||
return grid.getState(row, col);
|
||||
}
|
||||
case "components":
|
||||
return [
|
||||
computed(() => (
|
||||
<GridCell
|
||||
onClick={receiver.onClick}
|
||||
onHold={receiver.onHold}
|
||||
display={receiver.display}
|
||||
title={receiver.title}
|
||||
canClick={receiver.canClick}
|
||||
/>
|
||||
))
|
||||
];
|
||||
}
|
||||
|
||||
let prop = (grid as any)[key];
|
||||
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, row, col, grid.getState(row, col));
|
||||
}
|
||||
if (prop != null || typeof key === "symbol") {
|
||||
return prop;
|
||||
}
|
||||
|
||||
key = key.slice(0, 1).toUpperCase() + key.slice(1);
|
||||
|
||||
prop = (grid as any)[`get${key}`];
|
||||
if (isFunction(prop)) {
|
||||
if (!(key in cache)) {
|
||||
cache[key] = computed(() =>
|
||||
prop.call(receiver, row, col, grid.getState(row, col))
|
||||
);
|
||||
}
|
||||
return cache[key].value;
|
||||
} else if (prop != null) {
|
||||
return unref(prop);
|
||||
}
|
||||
|
||||
prop = (grid as any)[`on${key}`];
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, row, col, grid.getState(row, col));
|
||||
} else if (prop != null) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
return (grid as any)[key];
|
||||
},
|
||||
set(target, key, value) {
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
key = `set${key.slice(0, 1).toUpperCase() + key.slice(1)}`;
|
||||
if (key in grid && isFunction((grid as any)[key]) && (grid as any)[key].length < 3) {
|
||||
(grid as any)[key].call(grid, row, col, value);
|
||||
return true;
|
||||
} else {
|
||||
console.warn(`No setter for "${key}".`, target);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
ownKeys() {
|
||||
return keys;
|
||||
},
|
||||
has(target, key) {
|
||||
return (keys as readonly (string | symbol)[]).includes(key);
|
||||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if ((keys as readonly (string | symbol)[]).includes(key)) {
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function convertCellMaybeRefOrGetter<T>(
|
||||
value: CellMaybeRefOrGetter<T>
|
||||
): ProcessedCellRefOrGetter<T> {
|
||||
if (typeof value === "function" && value.length > 0) {
|
||||
return value;
|
||||
}
|
||||
return processGetter(value) as MaybeRef<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates a grid with the given options.
|
||||
* @param optionsFunc Grid options.
|
||||
*/
|
||||
export function createGrid<T extends GridOptions>(optionsFunc: OptionsFunc<T, BaseGrid, Grid>) {
|
||||
const cellState = persistent<Record<number, Record<number, State>>>({}, false);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Grid);
|
||||
const {
|
||||
rows,
|
||||
cols,
|
||||
getVisibility,
|
||||
getCanClick,
|
||||
getStartState,
|
||||
getStyle,
|
||||
getClasses,
|
||||
getTitle,
|
||||
getDisplay,
|
||||
onClick,
|
||||
onHold,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
const grid = {
|
||||
type: GridType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof GridOptions>),
|
||||
...vueFeatureMixin("grid", options, () => (
|
||||
<Grid rows={grid.rows} cols={grid.cols} cells={grid.cells} />
|
||||
)),
|
||||
cellState,
|
||||
cells: new Proxy({} as Record<number, GridCell[]>, {
|
||||
get(target, key: PropertyKey) {
|
||||
if (key === "isProxy") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key === "length") {
|
||||
return unref(grid.rows);
|
||||
}
|
||||
|
||||
if (typeof key !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyNum = parseInt(key);
|
||||
if (!Number.isFinite(keyNum) || keyNum >= unref(grid.rows)) {
|
||||
if (keyNum in target) {
|
||||
return target[keyNum];
|
||||
}
|
||||
return (target[keyNum] = getCellRowHandler(grid, keyNum));
|
||||
}
|
||||
},
|
||||
set(target, key, value) {
|
||||
console.warn("Cannot set grid cells", target, key, value);
|
||||
return false;
|
||||
},
|
||||
ownKeys(): string[] {
|
||||
return [...new Array(unref(grid.rows)).fill(0).map((_, i) => "" + i), "length"];
|
||||
},
|
||||
has(target, key) {
|
||||
if (key === "length") {
|
||||
return true;
|
||||
}
|
||||
if (typeof key !== "string") {
|
||||
return false;
|
||||
}
|
||||
const keyNum = parseInt(key);
|
||||
if (!Number.isFinite(keyNum) || keyNum >= unref(grid.rows)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if (typeof key !== "string") {
|
||||
return;
|
||||
}
|
||||
const keyNum = parseInt(key);
|
||||
if (
|
||||
key !== "length" &&
|
||||
(!Number.isFinite(keyNum) || keyNum >= unref(grid.rows))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false
|
||||
};
|
||||
}
|
||||
}),
|
||||
rows: processGetter(rows),
|
||||
cols: processGetter(cols),
|
||||
getVisibility: convertCellMaybeRefOrGetter(getVisibility ?? true),
|
||||
getCanClick: convertCellMaybeRefOrGetter(getCanClick ?? true),
|
||||
getStartState: processGetter(getStartState),
|
||||
getStyle: convertCellMaybeRefOrGetter(getStyle),
|
||||
getClasses: convertCellMaybeRefOrGetter(getClasses),
|
||||
getTitle: convertCellMaybeRefOrGetter(getTitle),
|
||||
getDisplay: convertCellMaybeRefOrGetter(getDisplay),
|
||||
getID: function (row: number, col: number): string {
|
||||
return grid.id + "-" + row + "-" + col;
|
||||
},
|
||||
getState: function (row: number, col: number): State {
|
||||
if (cellState.value[row][col] != null) {
|
||||
return cellState.value[row][col];
|
||||
}
|
||||
return grid.cells[row][col].startState;
|
||||
},
|
||||
setState: function (row: number, col: number, state: State) {
|
||||
cellState.value[row] ??= {};
|
||||
cellState.value[row][col] = state;
|
||||
},
|
||||
onClick:
|
||||
onClick == null
|
||||
? undefined
|
||||
: function (row, col, state, e) {
|
||||
if (grid.cells[row][col].canClick) {
|
||||
onClick.call(grid, row, col, state, e);
|
||||
}
|
||||
},
|
||||
onHold:
|
||||
onHold == null
|
||||
? undefined
|
||||
: function (row, col, state) {
|
||||
if (grid.cells[row][col].canClick) {
|
||||
onHold.call(grid, row, col, state);
|
||||
}
|
||||
}
|
||||
} satisfies Grid;
|
||||
|
||||
return grid;
|
||||
});
|
||||
}
|
|
@ -1,22 +1,21 @@
|
|||
import Hotkey from "components/Hotkey.vue";
|
||||
import { hasWon } from "data/projEntry";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { findFeatures, jsx, setDefault } from "features/feature";
|
||||
import { findFeatures } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import player from "game/player";
|
||||
import { registerInfoComponent } from "game/settings";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
import {
|
||||
processGetter,
|
||||
type MaybeRefOrGetter,
|
||||
type UnwrapRef,
|
||||
type MaybeRef
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { shallowReactive, unref } from "vue";
|
||||
import Hotkey from "components/Hotkey.vue";
|
||||
|
||||
/** A dictionary of all hotkeys. */
|
||||
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
|
||||
export const hotkeys: Record<string, Hotkey | undefined> = shallowReactive({});
|
||||
/** A symbol used to identify {@link Hotkey} features. */
|
||||
export const HotkeyType = Symbol("Hotkey");
|
||||
|
||||
|
@ -25,13 +24,13 @@ export const HotkeyType = Symbol("Hotkey");
|
|||
*/
|
||||
export interface HotkeyOptions {
|
||||
/** Whether or not this hotkey is currently enabled. */
|
||||
enabled?: Computable<boolean>;
|
||||
enabled?: MaybeRefOrGetter<boolean>;
|
||||
/** The key tied to this hotkey */
|
||||
key: string;
|
||||
/** The description of this hotkey, to display in the settings. */
|
||||
description: Computable<string>;
|
||||
description: MaybeRefOrGetter<string>;
|
||||
/** What to do upon pressing the key. */
|
||||
onPress: VoidFunction;
|
||||
onPress: (e?: MouseEvent | TouchEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,19 +42,11 @@ export interface BaseHotkey {
|
|||
}
|
||||
|
||||
/** An object that represents a hotkey shortcut that performs an action upon a key sequence being pressed. */
|
||||
export type Hotkey<T extends HotkeyOptions> = Replace<
|
||||
T & BaseHotkey,
|
||||
export type Hotkey = Replace<
|
||||
Replace<HotkeyOptions, BaseHotkey>,
|
||||
{
|
||||
enabled: GetComputableTypeWithDefault<T["enabled"], true>;
|
||||
description: GetComputableType<T["description"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Hotkey} object. */
|
||||
export type GenericHotkey = Replace<
|
||||
Hotkey<HotkeyOptions>,
|
||||
{
|
||||
enabled: ProcessedComputable<boolean>;
|
||||
enabled: MaybeRef<boolean>;
|
||||
description: UnwrapRef<HotkeyOptions["description"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -66,28 +57,33 @@ const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
|
|||
* @param optionsFunc Hotkey options.
|
||||
*/
|
||||
export function createHotkey<T extends HotkeyOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseHotkey, GenericHotkey>
|
||||
): Hotkey<T> {
|
||||
optionsFunc: OptionsFunc<T, BaseHotkey, Hotkey>
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const hotkey = optionsFunc.call(feature, feature);
|
||||
hotkey.type = HotkeyType;
|
||||
const options = optionsFunc.call(feature, feature as Hotkey);
|
||||
const { enabled, description, key, onPress, ...props } = options;
|
||||
|
||||
processComputable(hotkey as T, "enabled");
|
||||
setDefault(hotkey, "enabled", true);
|
||||
processComputable(hotkey as T, "description");
|
||||
const hotkey = {
|
||||
type: HotkeyType,
|
||||
...(props as Omit<typeof props, keyof HotkeyOptions>),
|
||||
enabled: processGetter(enabled) ?? true,
|
||||
description: processGetter(description),
|
||||
key,
|
||||
onPress
|
||||
} satisfies Hotkey;
|
||||
|
||||
return hotkey as unknown as Hotkey<T>;
|
||||
return hotkey;
|
||||
});
|
||||
}
|
||||
|
||||
globalBus.on("addLayer", layer => {
|
||||
(findFeatures(layer, HotkeyType) as GenericHotkey[]).forEach(hotkey => {
|
||||
(findFeatures(layer, HotkeyType) as Hotkey[]).forEach(hotkey => {
|
||||
hotkeys[hotkey.key] = hotkey;
|
||||
});
|
||||
});
|
||||
|
||||
globalBus.on("removeLayer", layer => {
|
||||
(findFeatures(layer, HotkeyType) as GenericHotkey[]).forEach(hotkey => {
|
||||
(findFeatures(layer, HotkeyType) as Hotkey[]).forEach(hotkey => {
|
||||
hotkeys[hotkey.key] = undefined;
|
||||
});
|
||||
});
|
||||
|
@ -123,33 +119,30 @@ document.onkeydown = function (e) {
|
|||
keysToCheck.push("ctrl+" + e.key);
|
||||
}
|
||||
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
|
||||
if (hotkey && unref(hotkey.enabled)) {
|
||||
if (hotkey != null && unref(hotkey.enabled)) {
|
||||
e.preventDefault();
|
||||
hotkey.onPress();
|
||||
}
|
||||
};
|
||||
|
||||
globalBus.on("setupVue", () =>
|
||||
registerInfoComponent(
|
||||
jsx(() => {
|
||||
const keys = Object.values(hotkeys).filter(hotkey => unref(hotkey?.enabled));
|
||||
if (keys.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<br />
|
||||
<h4>Hotkeys</h4>
|
||||
<div style="column-count: 2">
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} />{" "}
|
||||
{unref(hotkey?.description)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
registerInfoComponent(() => {
|
||||
const keys = Object.values(hotkeys).filter(hotkey => unref(hotkey?.enabled));
|
||||
if (keys.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<br />
|
||||
<h4>Hotkeys</h4>
|
||||
<div style="column-count: 2">
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
<Hotkey hotkey={hotkey as Hotkey} /> {unref(hotkey?.description)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<template>
|
||||
<div
|
||||
class="infobox"
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
:style="{
|
||||
borderColor: unref(color),
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
:class="{ collapsed: unref(collapsed), stacked, ...unref(classes) }"
|
||||
}"
|
||||
:class="{ collapsed: unref(collapsed), stacked }"
|
||||
>
|
||||
<button
|
||||
class="title"
|
||||
|
@ -17,43 +12,36 @@
|
|||
@click="collapsed.value = !unref(collapsed)"
|
||||
>
|
||||
<span class="toggle">▼</span>
|
||||
<component :is="titleComponent" />
|
||||
<Title />
|
||||
</button>
|
||||
<CollapseTransition>
|
||||
<div v-if="!unref(collapsed)" class="body" :style="{ backgroundColor: unref(color) }">
|
||||
<component :is="bodyComponent" :style="unref(bodyStyle)" />
|
||||
<Body :style="unref(bodyStyle)" />
|
||||
</div>
|
||||
</CollapseTransition>
|
||||
<Node :id="id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import settings from "game/settings";
|
||||
import { computeComponent } from "util/vue";
|
||||
import type { Ref, StyleValue } from "vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import { render } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { Infobox } from "./infobox";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
display: CoercableComponent;
|
||||
title: CoercableComponent;
|
||||
color?: string;
|
||||
collapsed: Ref<boolean>;
|
||||
style?: StyleValue;
|
||||
titleStyle?: StyleValue;
|
||||
bodyStyle?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
id: string;
|
||||
color: Infobox["color"];
|
||||
titleStyle: Infobox["titleStyle"];
|
||||
bodyStyle: Infobox["bodyStyle"];
|
||||
collapsed: Infobox["collapsed"];
|
||||
display: Infobox["display"];
|
||||
title: Infobox["title"];
|
||||
}>();
|
||||
|
||||
const titleComponent = computeComponent(toRef(props, "title"));
|
||||
const bodyComponent = computeComponent(toRef(props, "display"));
|
||||
const Title = () => render(props.title);
|
||||
const Body = () => render(props.display);
|
||||
|
||||
const stacked = computed(() => themes[settings.theme].mergeAdjacent);
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import InfoboxComponent from "features/infoboxes/Infobox.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Infobox} features. */
|
||||
export const InfoboxType = Symbol("Infobox");
|
||||
|
||||
/**
|
||||
* An object that configures an {@link Infobox}.
|
||||
*/
|
||||
export interface InfoboxOptions {
|
||||
/** Whether this clickable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The background color of the Infobox. */
|
||||
color?: Computable<string>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** CSS to apply to the title of the infobox. */
|
||||
titleStyle?: Computable<StyleValue>;
|
||||
/** CSS to apply to the body of the infobox. */
|
||||
bodyStyle?: Computable<StyleValue>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** A header to appear at the top of the display. */
|
||||
title: Computable<CoercableComponent>;
|
||||
/** The main text that appears in the display. */
|
||||
display: Computable<CoercableComponent>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link InfoboxOptions} to create an {@link Infobox}.
|
||||
*/
|
||||
export interface BaseInfobox {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** Whether or not this infobox is collapsed. */
|
||||
collapsed: Persistent<boolean>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof InfoboxType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that displays information in a collapsible way. */
|
||||
export type Infobox<T extends InfoboxOptions> = Replace<
|
||||
T & BaseInfobox,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
color: GetComputableType<T["color"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
titleStyle: GetComputableType<T["titleStyle"]>;
|
||||
bodyStyle: GetComputableType<T["bodyStyle"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
title: GetComputableType<T["title"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Infobox} object. */
|
||||
export type GenericInfobox = Replace<
|
||||
Infobox<InfoboxOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an infobox with the given options.
|
||||
* @param optionsFunc Infobox options.
|
||||
*/
|
||||
export function createInfobox<T extends InfoboxOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseInfobox, GenericInfobox>
|
||||
): Infobox<T> {
|
||||
const collapsed = persistent<boolean>(false, false);
|
||||
return createLazyProxy(feature => {
|
||||
const infobox = optionsFunc.call(feature, feature);
|
||||
infobox.id = getUniqueID("infobox-");
|
||||
infobox.type = InfoboxType;
|
||||
infobox[Component] = InfoboxComponent as GenericComponent;
|
||||
|
||||
infobox.collapsed = collapsed;
|
||||
|
||||
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");
|
||||
|
||||
infobox[GatherProps] = function (this: GenericInfobox) {
|
||||
const {
|
||||
visibility,
|
||||
display,
|
||||
title,
|
||||
color,
|
||||
collapsed,
|
||||
style,
|
||||
titleStyle,
|
||||
bodyStyle,
|
||||
classes,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
visibility,
|
||||
display,
|
||||
title,
|
||||
color,
|
||||
collapsed,
|
||||
style: unref(style),
|
||||
titleStyle,
|
||||
bodyStyle,
|
||||
classes,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
return infobox as unknown as Infobox<T>;
|
||||
});
|
||||
}
|
86
src/features/infoboxes/infobox.tsx
Normal file
86
src/features/infoboxes/infobox.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import Infobox from "features/infoboxes/Infobox.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import { CSSProperties, MaybeRefOrGetter } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Infobox} features. */
|
||||
export const InfoboxType = Symbol("Infobox");
|
||||
|
||||
/**
|
||||
* An object that configures an {@link Infobox}.
|
||||
*/
|
||||
export interface InfoboxOptions extends VueFeatureOptions {
|
||||
/** The background color of the Infobox. */
|
||||
color?: MaybeRefOrGetter<string>;
|
||||
/** CSS to apply to the title of the infobox. */
|
||||
titleStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** CSS to apply to the body of the infobox. */
|
||||
bodyStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
/** A header to appear at the top of the display. */
|
||||
title: MaybeRefOrGetter<Renderable>;
|
||||
/** The main text that appears in the display. */
|
||||
display: MaybeRefOrGetter<Renderable>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link InfoboxOptions} to create an {@link Infobox}.
|
||||
*/
|
||||
export interface BaseInfobox extends VueFeature {
|
||||
/** Whether or not this infobox is collapsed. */
|
||||
collapsed: Persistent<boolean>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof InfoboxType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that displays information in a collapsible way. */
|
||||
export type Infobox = Replace<
|
||||
Replace<InfoboxOptions, BaseInfobox>,
|
||||
{
|
||||
color: ProcessedRefOrGetter<InfoboxOptions["color"]>;
|
||||
titleStyle: ProcessedRefOrGetter<InfoboxOptions["titleStyle"]>;
|
||||
bodyStyle: ProcessedRefOrGetter<InfoboxOptions["bodyStyle"]>;
|
||||
title: ProcessedRefOrGetter<InfoboxOptions["title"]>;
|
||||
display: ProcessedRefOrGetter<InfoboxOptions["display"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an infobox with the given options.
|
||||
* @param optionsFunc Infobox options.
|
||||
*/
|
||||
export function createInfobox<T extends InfoboxOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseInfobox, Infobox>
|
||||
) {
|
||||
const collapsed = persistent<boolean>(false, false);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Infobox);
|
||||
const { color, titleStyle, bodyStyle, title, display, ...props } = options;
|
||||
|
||||
const infobox = {
|
||||
type: InfoboxType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof InfoboxOptions>),
|
||||
...vueFeatureMixin("infobox", options, () => (
|
||||
<Infobox
|
||||
color={infobox.color}
|
||||
titleStyle={infobox.titleStyle}
|
||||
bodyStyle={infobox.bodyStyle}
|
||||
collapsed={infobox.collapsed}
|
||||
title={infobox.title}
|
||||
display={infobox.display}
|
||||
/>
|
||||
)),
|
||||
collapsed,
|
||||
color: processGetter(color),
|
||||
titleStyle: processGetter(titleStyle),
|
||||
bodyStyle: processGetter(bodyStyle),
|
||||
title: processGetter(title),
|
||||
display: processGetter(display)
|
||||
} satisfies Infobox;
|
||||
|
||||
return infobox;
|
||||
});
|
||||
}
|
|
@ -13,13 +13,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Link } from "features/links/links";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { BoundsInjectionKey, NodesInjectionKey } from "game/layers";
|
||||
import { computed, inject, onMounted, ref, watch } from "vue";
|
||||
import { computed, inject, onMounted, ref, unref, watch } from "vue";
|
||||
import LinkVue from "./Link.vue";
|
||||
import { Links } from "./links";
|
||||
|
||||
const props = defineProps<{ links?: Link[] }>();
|
||||
const props = defineProps<{ links: Links["links"] }>();
|
||||
|
||||
const resizeListener = ref<Element | null>(null);
|
||||
|
||||
|
@ -35,7 +35,8 @@ onMounted(() => (boundingRect.value = resizeListener.value?.getBoundingClientRec
|
|||
const validLinks = computed(() => {
|
||||
const n = nodes.value;
|
||||
return (
|
||||
props.links?.filter(link => n[link.startNode.id]?.rect && n[link.endNode.id]?.rect) ?? []
|
||||
unref(props.links)?.filter(link =>
|
||||
n[link.startNode.id]?.rect && n[link.endNode.id]?.rect) ?? []
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import type { GenericComponent, OptionsFunc, Replace } from "features/feature";
|
||||
import { GatherProps, Component } from "features/feature";
|
||||
import type { Position } from "game/layers";
|
||||
import type { Computable, GetComputableType, ProcessedComputable } from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { SVGAttributes } from "vue";
|
||||
import LinksComponent from "./Links.vue";
|
||||
|
||||
/** A symbol used to identify {@link Links} features. */
|
||||
export const LinksType = Symbol("Links");
|
||||
|
||||
/** Represents a link between two nodes. It will be displayed as an SVG line, and can take any appropriate properties for an SVG line element. */
|
||||
export interface Link extends SVGAttributes {
|
||||
startNode: { id: string };
|
||||
endNode: { id: string };
|
||||
offsetStart?: Position;
|
||||
offsetEnd?: Position;
|
||||
}
|
||||
|
||||
/** An object that configures a {@link Links}. */
|
||||
export interface LinksOptions {
|
||||
/** The list of links to display. */
|
||||
links: Computable<Link[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link LinksOptions} to create an {@link Links}.
|
||||
*/
|
||||
export interface BaseLinks {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof LinksType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
|
||||
export type Links<T extends LinksOptions> = Replace<
|
||||
T & BaseLinks,
|
||||
{
|
||||
links: GetComputableType<T["links"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Links} object. */
|
||||
export type GenericLinks = Replace<
|
||||
Links<LinksOptions>,
|
||||
{
|
||||
links: ProcessedComputable<Link[]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates links with the given options.
|
||||
* @param optionsFunc Links options.
|
||||
*/
|
||||
export function createLinks<T extends LinksOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseLinks, GenericLinks>
|
||||
): Links<T> {
|
||||
return createLazyProxy(feature => {
|
||||
const links = optionsFunc.call(feature, feature);
|
||||
links.type = LinksType;
|
||||
links[Component] = LinksComponent as GenericComponent;
|
||||
|
||||
processComputable(links as T, "links");
|
||||
|
||||
links[GatherProps] = function (this: GenericLinks) {
|
||||
const { links } = this;
|
||||
return {
|
||||
links
|
||||
};
|
||||
};
|
||||
|
||||
return links as unknown as Links<T>;
|
||||
});
|
||||
}
|
60
src/features/links/links.tsx
Normal file
60
src/features/links/links.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import type { Position } from "game/layers";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { VueFeature, vueFeatureMixin } from "util/vue";
|
||||
import type { MaybeRef, MaybeRefOrGetter, SVGAttributes } from "vue";
|
||||
import Links from "./Links.vue";
|
||||
|
||||
/** A symbol used to identify {@link Links} features. */
|
||||
export const LinksType = Symbol("Links");
|
||||
|
||||
/** Represents a link between two nodes. It will be displayed as an SVG line, and can take any appropriate properties for an SVG line element. */
|
||||
export interface Link extends SVGAttributes {
|
||||
startNode: { id: string };
|
||||
endNode: { id: string };
|
||||
offsetStart?: Position;
|
||||
offsetEnd?: Position;
|
||||
}
|
||||
|
||||
/** An object that configures a {@link Links}. */
|
||||
export interface LinksOptions {
|
||||
/** The list of links to display. */
|
||||
links: MaybeRefOrGetter<Link[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link LinksOptions} to create an {@link Links}.
|
||||
*/
|
||||
export interface BaseLinks extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof LinksType;
|
||||
}
|
||||
|
||||
/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
|
||||
export type Links = Replace<
|
||||
Replace<LinksOptions, BaseLinks>,
|
||||
{
|
||||
links: MaybeRef<Link[]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates links with the given options.
|
||||
* @param optionsFunc Links options.
|
||||
*/
|
||||
export function createLinks<T extends LinksOptions>(optionsFunc: OptionsFunc<T, BaseLinks, Links>) {
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as Links);
|
||||
const { links, ...props } = options;
|
||||
|
||||
const retLinks = {
|
||||
type: LinksType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof LinksOptions>),
|
||||
...vueFeatureMixin("links", {}, () => <Links links={retLinks.links} />),
|
||||
links: processGetter(links)
|
||||
} satisfies Links;
|
||||
|
||||
return retLinks;
|
||||
});
|
||||
}
|
|
@ -2,25 +2,20 @@
|
|||
<div
|
||||
ref="resizeListener"
|
||||
class="resize-listener"
|
||||
:style="unref(style)"
|
||||
:class="unref(classes)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { Application } from "@pixi/app";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import "lib/pixi";
|
||||
import { nextTick, onBeforeUnmount, onMounted, shallowRef, unref } from "vue";
|
||||
import type { Particles } from "./particles";
|
||||
|
||||
const props = defineProps<{
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
onContainerResized: Particles["onContainerResized"];
|
||||
onHotReload: Particles["onHotReload"];
|
||||
onInit: (app: Application) => void;
|
||||
id: string;
|
||||
onContainerResized?: (rect: DOMRect) => void;
|
||||
onHotReload?: VoidFunction;
|
||||
}>();
|
||||
|
||||
const app = shallowRef<null | Application>(null);
|
||||
|
@ -38,7 +33,7 @@ onMounted(() => {
|
|||
backgroundAlpha: 0
|
||||
});
|
||||
resizeListener.value?.appendChild(app.value.view);
|
||||
props.onInit?.(app.value as Application);
|
||||
props.onInit(app.value);
|
||||
}
|
||||
updateBounds();
|
||||
if (props.onHotReload) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Application } from "@pixi/app";
|
||||
import type { EmitterConfigV3 } from "@pixi/particle-emitter";
|
||||
import { Emitter, upgradeConfig } from "@pixi/particle-emitter";
|
||||
import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID } from "features/feature";
|
||||
import ParticlesComponent from "features/particles/Particles.vue";
|
||||
import type { Computable, GetComputableType } from "util/computed";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { ProcessedRefOrGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Ref, shallowRef, unref } from "vue";
|
||||
import { VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import { Ref, shallowRef } from "vue";
|
||||
import Particles from "./Particles.vue";
|
||||
|
||||
/** A symbol used to identify {@link Particles} features. */
|
||||
export const ParticlesType = Symbol("Particles");
|
||||
|
@ -14,11 +14,7 @@ export const ParticlesType = Symbol("Particles");
|
|||
/**
|
||||
* An object that configures {@link Particles}.
|
||||
*/
|
||||
export interface ParticlesOptions {
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
export interface ParticlesOptions extends VueFeatureOptions {
|
||||
/** A function that is called when the particles canvas is resized. */
|
||||
onContainerResized?: (boundingRect: DOMRect) => void;
|
||||
/** A function that is called whenever the particles element is reloaded during development. For restarting particle effects. */
|
||||
|
@ -28,9 +24,7 @@ export interface ParticlesOptions {
|
|||
/**
|
||||
* The properties that are added onto a processed {@link ParticlesOptions} to create an {@link Particles}.
|
||||
*/
|
||||
export interface BaseParticles {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
export interface BaseParticles extends VueFeature {
|
||||
/** The Pixi.JS Application powering this particles canvas. */
|
||||
app: Ref<null | Application>;
|
||||
/**
|
||||
|
@ -41,52 +35,30 @@ export interface BaseParticles {
|
|||
addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof ParticlesType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that represents a feature that display particle effects on the screen.
|
||||
* The config should typically be gotten by designing the effect using the [online particle effect editor](https://pixijs.io/pixi-particles-editor/) and passing it into the {@link upgradeConfig} from @pixi/particle-emitter.
|
||||
*/
|
||||
export type Particles<T extends ParticlesOptions> = Replace<
|
||||
T & BaseParticles,
|
||||
export type Particles = Replace<
|
||||
Replace<ParticlesOptions, BaseParticles>,
|
||||
{
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
classes: ProcessedRefOrGetter<ParticlesOptions["classes"]>;
|
||||
style: ProcessedRefOrGetter<ParticlesOptions["style"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Particles} object. */
|
||||
export type GenericParticles = Particles<ParticlesOptions>;
|
||||
|
||||
/**
|
||||
* Lazily creates particles with the given options.
|
||||
* @param optionsFunc Particles options.
|
||||
*/
|
||||
export function createParticles<T extends ParticlesOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseParticles, GenericParticles>
|
||||
): Particles<T> {
|
||||
optionsFunc?: OptionsFunc<T, BaseParticles, Particles>
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const particles =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
particles.id = getUniqueID("particles-");
|
||||
particles.type = ParticlesType;
|
||||
particles[Component] = ParticlesComponent as GenericComponent;
|
||||
|
||||
particles.app = shallowRef(null);
|
||||
particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
|
||||
const genericParticles = particles as GenericParticles;
|
||||
if (genericParticles.app.value) {
|
||||
return Promise.resolve(new Emitter(genericParticles.app.value.stage, config));
|
||||
}
|
||||
return new Promise<Emitter>(resolve => {
|
||||
emittersToAdd.push({ resolve, config });
|
||||
});
|
||||
};
|
||||
const options = optionsFunc?.call(feature, feature as Particles) ?? ({} as T);
|
||||
const { onContainerResized, onHotReload, ...props } = options;
|
||||
|
||||
let emittersToAdd: {
|
||||
resolve: (value: Emitter | PromiseLike<Emitter>) => void;
|
||||
|
@ -94,27 +66,34 @@ export function createParticles<T extends ParticlesOptions>(
|
|||
}[] = [];
|
||||
|
||||
function onInit(app: Application) {
|
||||
const genericParticles = particles as GenericParticles;
|
||||
genericParticles.app.value = app;
|
||||
emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
|
||||
emittersToAdd = [];
|
||||
}
|
||||
|
||||
particles.onContainerResized = particles.onContainerResized?.bind(particles);
|
||||
const particles = {
|
||||
type: ParticlesType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof ParticlesOptions>),
|
||||
...vueFeatureMixin("particles", options, () => (
|
||||
<Particles
|
||||
onInit={onInit}
|
||||
onContainerResized={particles.onContainerResized}
|
||||
onHotReload={particles.onHotReload}
|
||||
/>
|
||||
)),
|
||||
app: shallowRef<null | Application>(null),
|
||||
onContainerResized,
|
||||
onHotReload,
|
||||
addEmitter: (config: EmitterConfigV3): Promise<Emitter> => {
|
||||
if (particles.app.value != null) {
|
||||
return Promise.resolve(new Emitter(particles.app.value.stage, config));
|
||||
}
|
||||
return new Promise<Emitter>(resolve => {
|
||||
emittersToAdd.push({ resolve, config });
|
||||
});
|
||||
}
|
||||
} satisfies Particles;
|
||||
|
||||
particles[GatherProps] = function (this: GenericParticles) {
|
||||
const { id, style, classes, onContainerResized, onHotReload } = this;
|
||||
return {
|
||||
id,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onContainerResized,
|
||||
onHotReload,
|
||||
onInit
|
||||
};
|
||||
};
|
||||
|
||||
return particles as unknown as Particles<T>;
|
||||
return particles;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,298 +0,0 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, Visibility, getUniqueID, jsx, setDefault } from "features/feature";
|
||||
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
||||
import {
|
||||
Requirements,
|
||||
createVisibilityRequirement,
|
||||
displayRequirements,
|
||||
maxRequirementsMet,
|
||||
payRequirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatWhole } from "util/bignum";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { GenericDecorator } from "./decorators/common";
|
||||
|
||||
/** A symbol used to identify {@link Repeatable} features. */
|
||||
export const RepeatableType = Symbol("Repeatable");
|
||||
|
||||
/** A type that can be used to customize the {@link Repeatable} display. */
|
||||
export type RepeatableDisplay =
|
||||
| CoercableComponent
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
/** The main text that appears in the display. */
|
||||
description?: CoercableComponent;
|
||||
/** A description of the current effect of this repeatable, based off its amount. */
|
||||
effectDisplay?: CoercableComponent;
|
||||
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
|
||||
showAmount?: boolean;
|
||||
};
|
||||
|
||||
/** An object that configures a {@link Repeatable}. */
|
||||
export interface RepeatableOptions {
|
||||
/** Whether this repeatable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The requirement(s) to increase this repeatable. */
|
||||
requirements: Requirements;
|
||||
/** The maximum amount obtainable for this repeatable. */
|
||||
limit?: Computable<DecimalSource>;
|
||||
/** The initial amount this repeatable has on a new save / after reset. */
|
||||
initialAmount?: DecimalSource;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** Toggles a smaller design for the feature. */
|
||||
small?: Computable<boolean>;
|
||||
/** The display to use for this repeatable. */
|
||||
display?: Computable<RepeatableDisplay>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
|
||||
*/
|
||||
export interface BaseRepeatable {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** The current amount this repeatable has. */
|
||||
amount: Persistent<DecimalSource>;
|
||||
/** Whether or not this repeatable's amount is at it's limit. */
|
||||
maxed: Ref<boolean>;
|
||||
/** Whether or not this repeatable can be clicked. */
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
/**
|
||||
* How much amount can be increased by, or 1 if unclickable.
|
||||
**/
|
||||
amountToIncrease: Ref<DecimalSource>;
|
||||
/** A function that gets called when this repeatable is clicked. */
|
||||
onClick: (event?: MouseEvent | TouchEvent) => void;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof RepeatableType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature with multiple "levels" with scaling requirements. */
|
||||
export type Repeatable<T extends RepeatableOptions> = Replace<
|
||||
T & BaseRepeatable,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
limit: GetComputableTypeWithDefault<T["limit"], Decimal>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
small: GetComputableType<T["small"]>;
|
||||
display: Ref<CoercableComponent>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Repeatable} object. */
|
||||
export type GenericRepeatable = Replace<
|
||||
Repeatable<RepeatableOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
limit: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a repeatable with the given options.
|
||||
* @param optionsFunc Repeatable options.
|
||||
*/
|
||||
export function createRepeatable<T extends RepeatableOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Repeatable<T> {
|
||||
const amount = persistent<DecimalSource>(0);
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy<Repeatable<T>, Repeatable<T>>(feature => {
|
||||
const repeatable = optionsFunc.call(feature, feature);
|
||||
|
||||
repeatable.id = getUniqueID("repeatable-");
|
||||
repeatable.type = RepeatableType;
|
||||
repeatable[Component] = ClickableComponent as GenericComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(repeatable);
|
||||
}
|
||||
|
||||
repeatable.amount = amount;
|
||||
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
||||
|
||||
Object.assign(repeatable, decoratedData);
|
||||
|
||||
const limitRequirement = {
|
||||
requirementMet: computed(() =>
|
||||
Decimal.sub(
|
||||
unref((repeatable as GenericRepeatable).limit),
|
||||
(repeatable as GenericRepeatable).amount.value
|
||||
)
|
||||
),
|
||||
requiresPay: false,
|
||||
visibility: Visibility.None,
|
||||
canMaximize: true
|
||||
} as const;
|
||||
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
||||
if (Array.isArray(repeatable.requirements)) {
|
||||
repeatable.requirements.unshift(visibilityRequirement);
|
||||
repeatable.requirements.push(limitRequirement);
|
||||
} else {
|
||||
repeatable.requirements = [
|
||||
visibilityRequirement,
|
||||
repeatable.requirements,
|
||||
limitRequirement
|
||||
];
|
||||
}
|
||||
|
||||
repeatable.maxed = computed(() =>
|
||||
Decimal.gte(
|
||||
(repeatable as GenericRepeatable).amount.value,
|
||||
unref((repeatable as GenericRepeatable).limit)
|
||||
)
|
||||
);
|
||||
processComputable(repeatable as T, "classes");
|
||||
const classes = repeatable.classes as
|
||||
| ProcessedComputable<Record<string, boolean>>
|
||||
| undefined;
|
||||
repeatable.classes = computed(() => {
|
||||
const currClasses = unref(classes) || {};
|
||||
if ((repeatable as GenericRepeatable).maxed.value) {
|
||||
currClasses.bought = true;
|
||||
}
|
||||
return currClasses;
|
||||
});
|
||||
repeatable.amountToIncrease = computed(() =>
|
||||
Decimal.clampMin(maxRequirementsMet(repeatable.requirements), 1)
|
||||
);
|
||||
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
||||
const onClick = repeatable.onClick;
|
||||
repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) {
|
||||
const genericRepeatable = repeatable as GenericRepeatable;
|
||||
if (!unref(genericRepeatable.canClick)) {
|
||||
return;
|
||||
}
|
||||
const amountToIncrease = unref(repeatable.amountToIncrease) ?? 1;
|
||||
payRequirements(repeatable.requirements, amountToIncrease);
|
||||
genericRepeatable.amount.value = Decimal.add(
|
||||
genericRepeatable.amount.value,
|
||||
amountToIncrease
|
||||
);
|
||||
onClick?.(event);
|
||||
};
|
||||
processComputable(repeatable as T, "display");
|
||||
const display = repeatable.display;
|
||||
repeatable.display = jsx(() => {
|
||||
// TODO once processComputable types correctly, remove this "as X"
|
||||
const currDisplay = unref(display) as RepeatableDisplay;
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
const CurrDisplay = coerceComponent(currDisplay);
|
||||
return <CurrDisplay />;
|
||||
}
|
||||
if (currDisplay != null) {
|
||||
const genericRepeatable = repeatable as GenericRepeatable;
|
||||
const Title = coerceComponent(currDisplay.title ?? "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description ?? "");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay ?? "");
|
||||
|
||||
return (
|
||||
<span>
|
||||
{currDisplay.title == null ? null : (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.description == null ? null : <Description />}
|
||||
{currDisplay.showAmount === false ? null : (
|
||||
<div>
|
||||
<br />
|
||||
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
||||
{Decimal.isFinite(unref(genericRepeatable.limit)) ? (
|
||||
<> / {formatWhole(unref(genericRepeatable.limit))}</>
|
||||
) : undefined}
|
||||
</div>
|
||||
)}
|
||||
{currDisplay.effectDisplay == null ? null : (
|
||||
<div>
|
||||
<br />
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
)}
|
||||
{genericRepeatable.maxed.value ? null : (
|
||||
<div>
|
||||
<br />
|
||||
{displayRequirements(
|
||||
genericRepeatable.requirements,
|
||||
unref(repeatable.amountToIncrease)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
processComputable(repeatable as T, "visibility");
|
||||
setDefault(repeatable, "visibility", Visibility.Visible);
|
||||
processComputable(repeatable as T, "limit");
|
||||
setDefault(repeatable, "limit", Decimal.dInf);
|
||||
processComputable(repeatable as T, "style");
|
||||
processComputable(repeatable as T, "mark");
|
||||
processComputable(repeatable as T, "small");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(repeatable);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(repeatable)),
|
||||
{}
|
||||
);
|
||||
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
||||
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
||||
this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onClick,
|
||||
canClick,
|
||||
small,
|
||||
mark,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return repeatable as unknown as Repeatable<T>;
|
||||
});
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { getUniqueID } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import { NonPersistent, Persistent, SkipPersistence } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
import {
|
||||
DefaultValue,
|
||||
NonPersistent,
|
||||
Persistent,
|
||||
persistent,
|
||||
SkipPersistence
|
||||
} from "game/persistence";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import Decimal from "util/bignum";
|
||||
import type { Computable, GetComputableType } from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { processGetter, type MaybeRefOrGetter, type UnwrapRef } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { isRef, unref } from "vue";
|
||||
|
||||
|
@ -20,7 +23,7 @@ export const ResetType = Symbol("Reset");
|
|||
*/
|
||||
export interface ResetOptions {
|
||||
/** List of things to reset. Can include objects which will be recursed over for persistent values. */
|
||||
thingsToReset: Computable<unknown[]>;
|
||||
thingsToReset: MaybeRefOrGetter<unknown[]>;
|
||||
/** A function that is called when the reset is performed. */
|
||||
onReset?: VoidFunction;
|
||||
}
|
||||
|
@ -29,8 +32,6 @@ export interface ResetOptions {
|
|||
* The properties that are added onto a processed {@link ResetOptions} to create an {@link Reset}.
|
||||
*/
|
||||
export interface BaseReset {
|
||||
/** An auto-generated ID for identifying which reset is being performed. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** Trigger the reset. */
|
||||
reset: VoidFunction;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
|
@ -38,57 +39,55 @@ export interface BaseReset {
|
|||
}
|
||||
|
||||
/** An object that represents a reset mechanic, which resets progress back to its initial state. */
|
||||
export type Reset<T extends ResetOptions> = Replace<
|
||||
T & BaseReset,
|
||||
export type Reset = Replace<
|
||||
Replace<ResetOptions, BaseReset>,
|
||||
{
|
||||
thingsToReset: GetComputableType<T["thingsToReset"]>;
|
||||
thingsToReset: UnwrapRef<ResetOptions["thingsToReset"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Reset} object. */
|
||||
export type GenericReset = Reset<ResetOptions>;
|
||||
|
||||
/**
|
||||
* Lazily creates a reset with the given options.
|
||||
* @param optionsFunc Reset options.
|
||||
*/
|
||||
export function createReset<T extends ResetOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseReset, GenericReset>
|
||||
): Reset<T> {
|
||||
export function createReset<T extends ResetOptions>(optionsFunc: OptionsFunc<T, BaseReset, Reset>) {
|
||||
return createLazyProxy(feature => {
|
||||
const reset = optionsFunc.call(feature, feature);
|
||||
reset.id = getUniqueID("reset-");
|
||||
reset.type = ResetType;
|
||||
const options = optionsFunc.call(feature, feature as Reset);
|
||||
const { thingsToReset, onReset, ...props } = options;
|
||||
|
||||
reset.reset = function () {
|
||||
const handleObject = (obj: unknown) => {
|
||||
if (
|
||||
obj != null &&
|
||||
typeof obj === "object" &&
|
||||
!(obj instanceof Decimal) &&
|
||||
!(obj instanceof Formula)
|
||||
) {
|
||||
if (SkipPersistence in obj && obj[SkipPersistence] === true) {
|
||||
return;
|
||||
const reset = {
|
||||
type: ResetType,
|
||||
...(props as Omit<typeof props, keyof ResetOptions>),
|
||||
onReset,
|
||||
thingsToReset: processGetter(thingsToReset),
|
||||
reset: function () {
|
||||
const handleObject = (obj: unknown) => {
|
||||
if (
|
||||
obj != null &&
|
||||
typeof obj === "object" &&
|
||||
!(obj instanceof Decimal) &&
|
||||
!(obj instanceof Formula)
|
||||
) {
|
||||
if (SkipPersistence in obj && obj[SkipPersistence] === true) {
|
||||
return;
|
||||
}
|
||||
if (DefaultValue in obj) {
|
||||
const persistent = obj as NonPersistent;
|
||||
persistent.value = persistent[DefaultValue];
|
||||
} else if (!(obj instanceof Decimal) && !isRef(obj)) {
|
||||
Object.values(obj).forEach(obj =>
|
||||
handleObject(obj as Record<string, unknown>)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (DefaultValue in obj) {
|
||||
const persistent = obj as NonPersistent;
|
||||
persistent.value = 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(reset.thingsToReset).forEach(handleObject);
|
||||
globalBus.emit("reset", reset);
|
||||
onReset?.();
|
||||
}
|
||||
} satisfies Reset;
|
||||
|
||||
processComputable(reset as T, "thingsToReset");
|
||||
|
||||
return reset as unknown as Reset<T>;
|
||||
return reset;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -98,7 +97,7 @@ const listeners: Record<string, Unsubscribe | undefined> = {};
|
|||
* @param layer The layer the reset is attached to
|
||||
* @param reset The reset mechanic to track the time since
|
||||
*/
|
||||
export function trackResetTime(layer: BaseLayer, reset: GenericReset): Persistent<Decimal> {
|
||||
export function trackResetTime(layer: BaseLayer, reset: Reset): Persistent<Decimal> {
|
||||
const resetTime = persistent<Decimal>(new Decimal(0));
|
||||
globalBus.on("addLayer", layerBeingAdded => {
|
||||
if (layer.id === layerBeingAdded.id) {
|
||||
|
@ -123,6 +122,6 @@ globalBus.on("removeLayer", layer => {
|
|||
|
||||
declare module "game/events" {
|
||||
interface GlobalEvents {
|
||||
reset: (reset: GenericReset) => void;
|
||||
reset: (reset: Reset) => void;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<ResourceVue :resource="resource" :color="color || 'white'" />
|
||||
{{ resource.displayName
|
||||
}}<!-- remove whitespace -->
|
||||
<span v-if="effectComponent"
|
||||
>, <component :is="effectComponent" ref="effectRef"
|
||||
<span v-if="effectDisplay"
|
||||
>, <Effect ref="effectRef"
|
||||
/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,24 +20,23 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import Sticky from "components/layout/Sticky.vue";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import ResourceVue from "features/resources/Resource.vue";
|
||||
import Decimal from "util/bignum";
|
||||
import { computeOptionalComponent } from "util/vue";
|
||||
import { ComponentPublicInstance, computed, ref, StyleValue, toRef } from "vue";
|
||||
import { Renderable } from "util/vue";
|
||||
import { ComponentPublicInstance, computed, MaybeRefOrGetter, ref, StyleValue, toValue } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
resource: Resource;
|
||||
color?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: StyleValue;
|
||||
effectDisplay?: CoercableComponent;
|
||||
effectDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
}>();
|
||||
|
||||
const effectRef = ref<ComponentPublicInstance | null>(null);
|
||||
|
||||
const effectComponent = computeOptionalComponent(toRef(props, "effectDisplay"));
|
||||
const Effect = () => toValue(props.effectDisplay);
|
||||
|
||||
const showPrefix = computed(() => {
|
||||
return Decimal.lt(props.resource.value, "1e1000");
|
||||
|
|
|
@ -3,9 +3,8 @@ import type { Persistent, State } from "game/persistence";
|
|||
import { NonPersistent, persistent } from "game/persistence";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
import type { ProcessedComputable } from "util/computed";
|
||||
import { loadingSave } from "util/save";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import type { ComputedRef, MaybeRef, Ref } from "vue";
|
||||
import { computed, isRef, ref, unref, watch } from "vue";
|
||||
|
||||
/** An object that represents a named and quantifiable resource in the game. */
|
||||
|
@ -159,7 +158,7 @@ export function displayResource(resource: Resource, overrideAmount?: DecimalSour
|
|||
}
|
||||
|
||||
/** Utility for unwrapping a resource that may or may not be inside a ref. */
|
||||
export function unwrapResource(resource: ProcessedComputable<Resource>): Resource {
|
||||
export function unwrapResource(resource: MaybeRef<Resource>): Resource {
|
||||
if ("displayName" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<template>
|
||||
<component :is="component" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { computeComponent } from "util/vue";
|
||||
import { toRef } from "vue";
|
||||
|
||||
const props = defineProps<{ display: CoercableComponent }>();
|
||||
const component = computeComponent(toRef(props, "display"));
|
||||
</script>
|
|
@ -1,37 +1,18 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="isVisible(visibility)"
|
||||
@click="selectTab"
|
||||
class="tabButton"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
glowColorStyle,
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
:class="{
|
||||
active,
|
||||
...unref(classes)
|
||||
}"
|
||||
>
|
||||
<component :is="component" />
|
||||
<button @click="selectTab" class="tabButton" :style="glowColorStyle" :class="{ active }">
|
||||
<Component />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import { getNotifyStyle } from "game/notifications";
|
||||
import { computeComponent } from "util/vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import { render } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { TabButton } from "./tabFamily";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
display: CoercableComponent;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
glowColor?: string;
|
||||
display: TabButton["display"];
|
||||
glowColor: TabButton["glowColor"];
|
||||
active?: boolean;
|
||||
floating?: boolean;
|
||||
}>();
|
||||
|
@ -40,10 +21,10 @@ const emit = defineEmits<{
|
|||
selectTab: [];
|
||||
}>();
|
||||
|
||||
const component = computeComponent(toRef(props, "display"));
|
||||
const Component = () => render(props.display);
|
||||
|
||||
const glowColorStyle = computed(() => {
|
||||
const color = props.glowColor;
|
||||
const color = unref(props.glowColor);
|
||||
if (color == null || color === "") {
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
class="tab-family-container"
|
||||
:class="{ ...unref(classes), ...tabClasses }"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? [],
|
||||
tabStyle ?? []
|
||||
]"
|
||||
>
|
||||
<div class="tab-family-container" :class="tabClasses" :style="tabStyle">
|
||||
<Sticky
|
||||
class="tab-buttons-container"
|
||||
:class="unref(buttonContainerClasses)"
|
||||
|
@ -23,79 +12,60 @@
|
|||
:floating="floating"
|
||||
:key="id"
|
||||
:active="unref(button.tab) === unref(activeTab)"
|
||||
v-bind="gatherButtonProps(button)"
|
||||
:display="button.display"
|
||||
:glowColor="button.glowColor"
|
||||
/>
|
||||
</div>
|
||||
</Sticky>
|
||||
<template v-if="unref(activeTab)">
|
||||
<component :is="unref(component)" />
|
||||
</template>
|
||||
<Component v-if="unref(activeTab) != null" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Sticky from "components/layout/Sticky.vue";
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
||||
import type { GenericTab } from "features/tabs/tab";
|
||||
import TabButton from "features/tabs/TabButton.vue";
|
||||
import type { GenericTabButton } from "features/tabs/tabFamily";
|
||||
import settings from "game/settings";
|
||||
import { coerceComponent, deepUnref, isCoercableComponent } from "util/vue";
|
||||
import type { Component, Ref } from "vue";
|
||||
import { computed, shallowRef, unref, watchEffect } from "vue";
|
||||
import { render } from "util/vue";
|
||||
import type { Component } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { TabFamily } from "./tabFamily";
|
||||
import { TabType } from "./tab";
|
||||
import { isType } from "features/feature";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
activeTab: GenericTab | CoercableComponent | null;
|
||||
selected: Ref<string>;
|
||||
tabs: Record<string, GenericTabButton>;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
buttonContainerStyle?: StyleValue;
|
||||
buttonContainerClasses?: Record<string, boolean>;
|
||||
activeTab: TabFamily["activeTab"];
|
||||
selected: TabFamily["selected"];
|
||||
tabs: TabFamily["tabs"];
|
||||
buttonContainerClasses: TabFamily["buttonContainerClasses"];
|
||||
buttonContainerStyle: TabFamily["buttonContainerStyle"];
|
||||
}>();
|
||||
|
||||
const floating = computed(() => {
|
||||
return themes[settings.theme].floatingTabs;
|
||||
});
|
||||
|
||||
const component = shallowRef<Component | string>("");
|
||||
|
||||
watchEffect(() => {
|
||||
const currActiveTab = props.activeTab;
|
||||
if (currActiveTab == null) {
|
||||
component.value = "";
|
||||
const Component = () => {
|
||||
const activeTab = unref(props.activeTab);
|
||||
if (activeTab == null) {
|
||||
return;
|
||||
}
|
||||
if (isCoercableComponent(currActiveTab)) {
|
||||
component.value = coerceComponent(currActiveTab);
|
||||
return;
|
||||
}
|
||||
component.value = coerceComponent(unref(currActiveTab.display));
|
||||
});
|
||||
return render(activeTab);
|
||||
};
|
||||
|
||||
const tabClasses = computed(() => {
|
||||
const currActiveTab = props.activeTab;
|
||||
const tabClasses =
|
||||
isCoercableComponent(currActiveTab) || !currActiveTab
|
||||
? undefined
|
||||
: unref(currActiveTab.classes);
|
||||
return tabClasses;
|
||||
const activeTab = unref(props.activeTab);
|
||||
if (isType(activeTab, TabType)) {
|
||||
return unref(activeTab.classes);
|
||||
}
|
||||
});
|
||||
|
||||
const tabStyle = computed(() => {
|
||||
const currActiveTab = props.activeTab;
|
||||
return isCoercableComponent(currActiveTab) || !currActiveTab
|
||||
? undefined
|
||||
: unref(currActiveTab.style);
|
||||
const activeTab = unref(props.activeTab);
|
||||
if (isType(activeTab, TabType)) {
|
||||
return unref(activeTab.style);
|
||||
}
|
||||
});
|
||||
|
||||
function gatherButtonProps(button: GenericTabButton) {
|
||||
const { display, style, classes, glowColor, visibility } = deepUnref(button);
|
||||
return { display, style, classes, glowColor, visibility };
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID } from "features/feature";
|
||||
import TabComponent from "features/tabs/Tab.vue";
|
||||
import type { Computable, GetComputableType } from "util/computed";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import { MaybeRefOrGetter } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
|
||||
/** A symbol used to identify {@link Tab} features. */
|
||||
export const TabType = Symbol("Tab");
|
||||
|
@ -16,63 +11,46 @@ export const TabType = Symbol("Tab");
|
|||
/**
|
||||
* An object that configures a {@link Tab}.
|
||||
*/
|
||||
export interface TabOptions {
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
export interface TabOptions extends VueFeatureOptions {
|
||||
/** The display to use for this tab. */
|
||||
display: Computable<CoercableComponent>;
|
||||
display: MaybeRefOrGetter<Renderable>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TabOptions} to create an {@link Tab}.
|
||||
*/
|
||||
export interface BaseTab {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
export interface BaseTab extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TabType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object representing a tab of content in a tabbed interface.
|
||||
* @see {@link TabFamily}
|
||||
*/
|
||||
export type Tab<T extends TabOptions> = Replace<
|
||||
T & BaseTab,
|
||||
export type Tab = Replace<
|
||||
Replace<TabOptions, BaseTab>,
|
||||
{
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
display: ProcessedRefOrGetter<TabOptions["display"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Tab} object. */
|
||||
export type GenericTab = Tab<TabOptions>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tab with the given options.
|
||||
* @param optionsFunc Tab options.
|
||||
*/
|
||||
export function createTab<T extends TabOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseTab, GenericTab>
|
||||
): Tab<T> {
|
||||
export function createTab<T extends TabOptions>(optionsFunc: OptionsFunc<T, BaseTab, Tab>) {
|
||||
return createLazyProxy(feature => {
|
||||
const tab = optionsFunc.call(feature, feature);
|
||||
tab.id = getUniqueID("tab-");
|
||||
tab.type = TabType;
|
||||
tab[Component] = TabComponent as GenericComponent;
|
||||
const options = optionsFunc?.call(feature, feature as Tab) ?? ({} as T);
|
||||
const { display, ...props } = options;
|
||||
|
||||
tab[GatherProps] = function (this: GenericTab) {
|
||||
const { display } = this;
|
||||
return { display };
|
||||
};
|
||||
const tab = {
|
||||
type: TabType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TabOptions>),
|
||||
...vueFeatureMixin("tab", options, (): JSX.Element => render(tab.display)),
|
||||
display: processGetter(display)
|
||||
} satisfies Tab;
|
||||
|
||||
return tab as unknown as Tab<T>;
|
||||
return tab;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,232 +0,0 @@
|
|||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
isVisible,
|
||||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import TabButtonComponent from "features/tabs/TabButton.vue";
|
||||
import TabFamilyComponent from "features/tabs/TabFamily.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import type { GenericTab } from "./tab";
|
||||
|
||||
/** A symbol used to identify {@link TabButton} features. */
|
||||
export const TabButtonType = Symbol("TabButton");
|
||||
/** A symbol used to identify {@link TabFamily} features. */
|
||||
export const TabFamilyType = Symbol("TabFamily");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TabButton}.
|
||||
*/
|
||||
export interface TabButtonOptions {
|
||||
/** Whether this tab button should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The tab to display when this button is clicked. */
|
||||
tab: Computable<GenericTab | CoercableComponent>;
|
||||
/** The label on this button. */
|
||||
display: Computable<CoercableComponent>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** The color of the glow effect to display when this button is active. */
|
||||
glowColor?: Computable<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TabButtonOptions} to create an {@link TabButton}.
|
||||
*/
|
||||
export interface BaseTabButton {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TabButtonType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that represents a button that can be clicked to change tabs in a tabbed interface.
|
||||
* @see {@link TabFamily}
|
||||
*/
|
||||
export type TabButton<T extends TabButtonOptions> = Replace<
|
||||
T & BaseTabButton,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
tab: GetComputableType<T["tab"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
glowColor: GetComputableType<T["glowColor"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link TabButton} object. */
|
||||
export type GenericTabButton = Replace<
|
||||
TabButton<TabButtonOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TabFamily}.
|
||||
*/
|
||||
export interface TabFamilyOptions {
|
||||
/** Whether this tab button should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** A dictionary of CSS classes to apply to the list of buttons for changing tabs. */
|
||||
buttonContainerClasses?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to the list of buttons for changing tabs. */
|
||||
buttonContainerStyle?: Computable<StyleValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TabFamilyOptions} to create an {@link TabFamily}.
|
||||
*/
|
||||
export interface BaseTabFamily {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** All the tabs within this family. */
|
||||
tabs: Record<string, TabButtonOptions>;
|
||||
/** The currently active tab, if any. */
|
||||
activeTab: Ref<GenericTab | CoercableComponent | null>;
|
||||
/** The name of the tab that is currently active. */
|
||||
selected: Persistent<string>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TabFamilyType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that represents a tabbed interface.
|
||||
* @see {@link TabFamily}
|
||||
*/
|
||||
export type TabFamily<T extends TabFamilyOptions> = Replace<
|
||||
T & BaseTabFamily,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
tabs: Record<string, GenericTabButton>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link TabFamily} object. */
|
||||
export type GenericTabFamily = Replace<
|
||||
TabFamily<TabFamilyOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tab family with the given options.
|
||||
* @param optionsFunc Tab family options.
|
||||
*/
|
||||
export function createTabFamily<T extends TabFamilyOptions>(
|
||||
tabs: Record<string, () => TabButtonOptions>,
|
||||
optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
|
||||
): TabFamily<T> {
|
||||
if (Object.keys(tabs).length === 0) {
|
||||
console.error("Cannot create tab family with 0 tabs");
|
||||
}
|
||||
|
||||
const selected = persistent(Object.keys(tabs)[0], false);
|
||||
return createLazyProxy(feature => {
|
||||
const tabFamily =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
|
||||
tabFamily.id = getUniqueID("tabFamily-");
|
||||
tabFamily.type = TabFamilyType;
|
||||
tabFamily[Component] = TabFamilyComponent as GenericComponent;
|
||||
|
||||
tabFamily.tabs = Object.keys(tabs).reduce<Record<string, GenericTabButton>>(
|
||||
(parsedTabs, tab) => {
|
||||
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabs[tab]();
|
||||
tabButton.type = TabButtonType;
|
||||
tabButton[Component] = TabButtonComponent as GenericComponent;
|
||||
|
||||
processComputable(tabButton as TabButtonOptions, "visibility");
|
||||
setDefault(tabButton, "visibility", Visibility.Visible);
|
||||
processComputable(tabButton as TabButtonOptions, "tab");
|
||||
processComputable(tabButton as TabButtonOptions, "display");
|
||||
processComputable(tabButton as TabButtonOptions, "classes");
|
||||
processComputable(tabButton as TabButtonOptions, "style");
|
||||
processComputable(tabButton as TabButtonOptions, "glowColor");
|
||||
parsedTabs[tab] = tabButton as GenericTabButton;
|
||||
return parsedTabs;
|
||||
},
|
||||
{}
|
||||
);
|
||||
tabFamily.selected = selected;
|
||||
tabFamily.activeTab = computed(() => {
|
||||
const tabs = unref(processedTabFamily.tabs);
|
||||
if (selected.value in tabs && isVisible(tabs[selected.value].visibility)) {
|
||||
return unref(tabs[selected.value].tab);
|
||||
}
|
||||
const firstTab = Object.values(tabs).find(tab => isVisible(tab.visibility));
|
||||
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");
|
||||
processComputable(tabFamily as T, "buttonContainerClasses");
|
||||
processComputable(tabFamily as T, "buttonContainerStyle");
|
||||
|
||||
tabFamily[GatherProps] = function (this: GenericTabFamily) {
|
||||
const {
|
||||
visibility,
|
||||
activeTab,
|
||||
selected,
|
||||
tabs,
|
||||
style,
|
||||
classes,
|
||||
buttonContainerClasses,
|
||||
buttonContainerStyle
|
||||
} = this;
|
||||
return {
|
||||
visibility,
|
||||
activeTab,
|
||||
selected,
|
||||
tabs,
|
||||
style: unref(style),
|
||||
classes,
|
||||
buttonContainerClasses,
|
||||
buttonContainerStyle
|
||||
};
|
||||
};
|
||||
|
||||
// This is necessary because board.types is different from T and TabFamily
|
||||
const processedTabFamily = tabFamily as unknown as TabFamily<T>;
|
||||
return processedTabFamily;
|
||||
});
|
||||
}
|
154
src/features/tabs/tabFamily.tsx
Normal file
154
src/features/tabs/tabFamily.tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { isVisible } from "features/feature";
|
||||
import { Tab } from "features/tabs/tab";
|
||||
import TabButton from "features/tabs/TabButton.vue";
|
||||
import TabFamily from "features/tabs/TabFamily.vue";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link TabButton} features. */
|
||||
export const TabButtonType = Symbol("TabButton");
|
||||
/** A symbol used to identify {@link TabFamily} features. */
|
||||
export const TabFamilyType = Symbol("TabFamily");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TabButton}.
|
||||
*/
|
||||
export interface TabButtonOptions extends VueFeatureOptions {
|
||||
/** The tab to display when this button is clicked. */
|
||||
tab: Tab | MaybeRefOrGetter<Renderable>;
|
||||
/** The label on this button. */
|
||||
display: MaybeRefOrGetter<Renderable>;
|
||||
/** The color of the glow effect to display when this button is active. */
|
||||
glowColor?: MaybeRefOrGetter<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TabButtonOptions} to create an {@link TabButton}.
|
||||
*/
|
||||
export interface BaseTabButton extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TabButtonType;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that represents a button that can be clicked to change tabs in a tabbed interface.
|
||||
* @see {@link TabFamily}
|
||||
*/
|
||||
export type TabButton = Replace<
|
||||
Replace<TabButtonOptions, BaseTabButton>,
|
||||
{
|
||||
tab: Tab | MaybeRef<Renderable>;
|
||||
display: ProcessedRefOrGetter<TabButtonOptions["display"]>;
|
||||
glowColor: ProcessedRefOrGetter<TabButtonOptions["glowColor"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TabFamily}.
|
||||
*/
|
||||
export interface TabFamilyOptions extends VueFeatureOptions {
|
||||
/** A dictionary of CSS classes to apply to the list of buttons for changing tabs. */
|
||||
buttonContainerClasses?: MaybeRefOrGetter<Record<string, boolean>>;
|
||||
/** CSS to apply to the list of buttons for changing tabs. */
|
||||
buttonContainerStyle?: MaybeRefOrGetter<CSSProperties>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TabFamilyOptions} to create an {@link TabFamily}.
|
||||
*/
|
||||
export interface BaseTabFamily extends VueFeature {
|
||||
/** All the tabs within this family. */
|
||||
tabs: Record<string, TabButtonOptions>;
|
||||
/** The currently active tab, if any. */
|
||||
activeTab: Ref<Tab | MaybeRef<Renderable> | null>;
|
||||
/** The name of the tab that is currently active. */
|
||||
selected: Persistent<string>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TabFamilyType;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that represents a tabbed interface.
|
||||
* @see {@link TabFamily}
|
||||
*/
|
||||
export type TabFamily = Replace<
|
||||
Replace<TabFamilyOptions, BaseTabFamily>,
|
||||
{
|
||||
tabs: Record<string, TabButton>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tab family with the given options.
|
||||
* @param optionsFunc Tab family options.
|
||||
*/
|
||||
export function createTabFamily<T extends TabFamilyOptions>(
|
||||
tabs: Record<string, () => TabButtonOptions>,
|
||||
optionsFunc?: OptionsFunc<T, BaseTabFamily, TabFamily>
|
||||
) {
|
||||
if (Object.keys(tabs).length === 0) {
|
||||
console.error("Cannot create tab family with 0 tabs");
|
||||
}
|
||||
|
||||
const selected = persistent(Object.keys(tabs)[0], false);
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as TabFamily) ?? ({} as T);
|
||||
const { buttonContainerClasses, buttonContainerStyle, ...props } = options;
|
||||
|
||||
const tabFamily = {
|
||||
type: TabFamilyType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TabFamilyOptions>),
|
||||
...vueFeatureMixin("tabFamily", options, () => (
|
||||
<TabFamily
|
||||
activeTab={tabFamily.activeTab}
|
||||
selected={tabFamily.selected}
|
||||
tabs={tabFamily.tabs}
|
||||
buttonContainerClasses={tabFamily.buttonContainerClasses}
|
||||
buttonContainerStyle={tabFamily.buttonContainerStyle}
|
||||
/>
|
||||
)),
|
||||
tabs: Object.keys(tabs).reduce<Record<string, TabButton>>((parsedTabs, tab) => {
|
||||
const options = tabs[tab]();
|
||||
const { tab: buttonTab, glowColor, display, ...props } = options;
|
||||
|
||||
const tabButton = {
|
||||
type: TabButtonType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TabButtonOptions>),
|
||||
...vueFeatureMixin("tabButton", options),
|
||||
tab: processGetter(buttonTab),
|
||||
glowColor: processGetter(glowColor),
|
||||
display: processGetter(display)
|
||||
} satisfies TabButton;
|
||||
|
||||
parsedTabs[tab] = tabButton;
|
||||
return parsedTabs;
|
||||
}, {}),
|
||||
buttonContainerClasses: processGetter(buttonContainerClasses),
|
||||
buttonContainerStyle: processGetter(buttonContainerStyle),
|
||||
selected,
|
||||
activeTab: computed((): Tab | MaybeRef<Renderable> | null => {
|
||||
if (
|
||||
selected.value in tabFamily.tabs &&
|
||||
isVisible(tabFamily.tabs[selected.value].visibility ?? true)
|
||||
) {
|
||||
return unref(tabFamily.tabs[selected.value].tab);
|
||||
}
|
||||
const firstTab = Object.values(tabFamily.tabs).find(tab =>
|
||||
isVisible(tab.visibility ?? true)
|
||||
);
|
||||
if (firstTab != null) {
|
||||
return unref(firstTab.tab);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
} satisfies TabFamily;
|
||||
|
||||
return tabFamily;
|
||||
});
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
|
||||
import { Component, GatherProps, setDefault } from "features/feature";
|
||||
import { persistent } from "game/persistence";
|
||||
import { Direction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import type { VueFeature } from "util/vue";
|
||||
import type { Ref } from "vue";
|
||||
import { nextTick, unref } from "vue";
|
||||
import TooltipComponent from "./Tooltip.vue";
|
||||
|
||||
declare module "@vue/runtime-dom" {
|
||||
interface CSSProperties {
|
||||
"--xoffset"?: string;
|
||||
"--yoffset"?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Tooltip}.
|
||||
*/
|
||||
export interface TooltipOptions {
|
||||
/** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
|
||||
pinnable?: boolean;
|
||||
/** The text to display inside the tooltip. */
|
||||
display: Computable<CoercableComponent>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** The direction in which to display the tooltip */
|
||||
direction?: Computable<Direction>;
|
||||
/** The x offset of the tooltip, in px. */
|
||||
xoffset?: Computable<string>;
|
||||
/** The y offset of the tooltip, in px. */
|
||||
yoffset?: Computable<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TooltipOptions} to create an {@link Tooltip}.
|
||||
*/
|
||||
export interface BaseTooltip {
|
||||
pinned?: Ref<boolean>;
|
||||
}
|
||||
|
||||
/** An object that represents a tooltip that appears when hovering over an element. */
|
||||
export type Tooltip<T extends TooltipOptions> = Replace<
|
||||
T & BaseTooltip,
|
||||
{
|
||||
pinnable: undefined extends T["pinnable"] ? false : T["pinnable"];
|
||||
pinned: T["pinnable"] extends true ? Ref<boolean> : undefined;
|
||||
display: GetComputableType<T["display"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
direction: GetComputableTypeWithDefault<T["direction"], Direction.Up>;
|
||||
xoffset: GetComputableType<T["xoffset"]>;
|
||||
yoffset: GetComputableType<T["yoffset"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Tooltip} object. */
|
||||
export type GenericTooltip = Replace<
|
||||
Tooltip<TooltipOptions>,
|
||||
{
|
||||
pinnable: boolean;
|
||||
pinned: Ref<boolean> | undefined;
|
||||
direction: ProcessedComputable<Direction>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a tooltip on the given element with the given options.
|
||||
* @param element The renderable feature to display the tooltip on.
|
||||
* @param options Tooltip options.
|
||||
*/
|
||||
export function addTooltip<T extends TooltipOptions>(
|
||||
element: VueFeature,
|
||||
options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
|
||||
): Tooltip<T> {
|
||||
processComputable(options as T, "display");
|
||||
processComputable(options as T, "classes");
|
||||
processComputable(options as T, "style");
|
||||
processComputable(options as T, "direction");
|
||||
setDefault(options, "direction", Direction.Up);
|
||||
processComputable(options as T, "xoffset");
|
||||
processComputable(options as T, "yoffset");
|
||||
|
||||
if (options.pinnable) {
|
||||
options.pinned = persistent<boolean>(false, false);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const elementComponent = element[Component];
|
||||
element[Component] = TooltipComponent as GenericComponent;
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
|
||||
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
|
||||
return {
|
||||
element: {
|
||||
[Component]: elementComponent,
|
||||
[GatherProps]: elementGatherProps
|
||||
},
|
||||
display,
|
||||
classes,
|
||||
style: unref(style),
|
||||
direction,
|
||||
xoffset,
|
||||
yoffset,
|
||||
pinned
|
||||
};
|
||||
}.bind(options as GenericTooltip);
|
||||
});
|
||||
|
||||
return options as unknown as Tooltip<T>;
|
||||
}
|
|
@ -1,63 +1,38 @@
|
|||
<template>
|
||||
<component :is="nodesComp" />
|
||||
<component v-if="leftNodesComp" :is="leftNodesComp" />
|
||||
<component v-if="rightNodesComp" :is="rightNodesComp" />
|
||||
<Nodes />
|
||||
<LeftNodes v-if="leftSideNodes" />
|
||||
<RightNodes v-if="rightSideNodes" />
|
||||
<Links v-if="branches" :links="unref(branches)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/table.css";
|
||||
import { jsx } from "features/feature";
|
||||
import Links from "features/links/Links.vue";
|
||||
import type { GenericTreeNode, TreeBranch } from "features/trees/tree";
|
||||
import { coerceComponent, renderJSX } from "util/vue";
|
||||
import type { Component } from "vue";
|
||||
import { shallowRef, unref, watchEffect } from "vue";
|
||||
import type { Tree } from "features/trees/tree";
|
||||
import { joinJSX, render } from "util/vue";
|
||||
import { unref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: GenericTreeNode[][];
|
||||
leftSideNodes?: GenericTreeNode[];
|
||||
rightSideNodes?: GenericTreeNode[];
|
||||
branches?: TreeBranch[];
|
||||
nodes: Tree["nodes"];
|
||||
leftSideNodes: Tree["leftSideNodes"];
|
||||
rightSideNodes: Tree["rightSideNodes"];
|
||||
branches: Tree["branches"];
|
||||
}>();
|
||||
|
||||
const nodesComp = shallowRef<Component | "">();
|
||||
watchEffect(() => {
|
||||
const currNodes = props.nodes;
|
||||
nodesComp.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<>
|
||||
{currNodes.map(row => (
|
||||
<span class="row tree-row" style="margin: 50px auto;">
|
||||
{row.map(renderJSX)}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
))
|
||||
);
|
||||
});
|
||||
const Nodes = () => unref(props.nodes).map(nodes =>
|
||||
<span class="row tree-row" style="margin: 50px auto;">
|
||||
{nodes.map(node => render(node))}
|
||||
</span>);
|
||||
|
||||
const LeftNodes = () => props.leftSideNodes == null ? <></> :
|
||||
<span class="left-side-nodes small">
|
||||
{unref(props.leftSideNodes).map(node => render(node))}
|
||||
</span>;
|
||||
|
||||
const leftNodesComp = shallowRef<Component | "">();
|
||||
watchEffect(() => {
|
||||
const currNodes = props.leftSideNodes;
|
||||
leftNodesComp.value = currNodes
|
||||
? coerceComponent(
|
||||
jsx(() => (
|
||||
<span class="left-side-nodes small">{currNodes.map(renderJSX)}</span>
|
||||
))
|
||||
)
|
||||
: "";
|
||||
});
|
||||
|
||||
const rightNodesComp = shallowRef<Component | "">();
|
||||
watchEffect(() => {
|
||||
const currNodes = props.rightSideNodes;
|
||||
rightNodesComp.value = currNodes
|
||||
? coerceComponent(
|
||||
jsx(() => <span class="side-nodes small">{currNodes.map(renderJSX)}</span>)
|
||||
)
|
||||
: "";
|
||||
});
|
||||
const RightNodes = () => props.rightSideNodes == null ? <></> :
|
||||
<span class="side-nodes small">
|
||||
{unref(props.rightSideNodes).map(node => render(node))}
|
||||
</span>;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isVisible(visibility)"
|
||||
:style="{ visibility: isHidden(visibility) ? 'hidden' : undefined }"
|
||||
:style="{
|
||||
backgroundColor: unref(color),
|
||||
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${unref(
|
||||
glowColor
|
||||
)}`
|
||||
}"
|
||||
:class="{
|
||||
treeNode: true,
|
||||
can: unref(canClick),
|
||||
...unref(classes)
|
||||
can: unref(canClick)
|
||||
}"
|
||||
@click="onClick"
|
||||
@mousedown="start"
|
||||
|
@ -15,48 +18,26 @@
|
|||
@touchend.passive="stop"
|
||||
@touchcancel.passive="stop"
|
||||
>
|
||||
<div
|
||||
:style="[
|
||||
{
|
||||
backgroundColor: unref(color),
|
||||
boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${unref(
|
||||
glowColor
|
||||
)}`
|
||||
},
|
||||
unref(style) ?? []
|
||||
]"
|
||||
>
|
||||
<component :is="unref(comp)" />
|
||||
</div>
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
<Component />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CoercableComponent, StyleValue, Visibility } from "features/feature";
|
||||
import { isHidden, isVisible } from "features/feature";
|
||||
import {
|
||||
computeOptionalComponent,
|
||||
setupHoldToClick
|
||||
} from "util/vue";
|
||||
<script setup lang="tsx">
|
||||
import { render, setupHoldToClick } from "util/vue";
|
||||
import { toRef, unref } from "vue";
|
||||
import { TreeNode } from "./tree";
|
||||
|
||||
const props = defineProps<{
|
||||
visibility: Visibility | boolean;
|
||||
canClick: boolean;
|
||||
id: string;
|
||||
display?: CoercableComponent;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
onHold?: VoidFunction;
|
||||
color?: string;
|
||||
glowColor?: string;
|
||||
mark?: boolean | string;
|
||||
canClick: TreeNode["canClick"];
|
||||
display: TreeNode["display"];
|
||||
onClick: TreeNode["onClick"];
|
||||
onHold: TreeNode["onHold"];
|
||||
color: TreeNode["color"];
|
||||
glowColor: TreeNode["glowColor"];
|
||||
}>();
|
||||
|
||||
const comp = computeOptionalComponent(toRef(props, "display"));
|
||||
const Component = () => props.display == null ? <></> :
|
||||
render(props.display, el => <div>{el}</div>);
|
||||
|
||||
const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "onHold"));
|
||||
</script>
|
||||
|
@ -65,16 +46,10 @@ const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "
|
|||
.treeNode {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
margin: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.treeNode > *:first-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: inherit;
|
||||
font-size: 40px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
|
||||
|
@ -82,7 +57,7 @@ const { start, stop } = setupHoldToClick(toRef(props, "onClick"), toRef(props, "
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.treeNode > *:first-child > * {
|
||||
.treeNode > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,387 +0,0 @@
|
|||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
||||
import type { Link } from "features/links/links";
|
||||
import type { GenericReset } from "features/reset";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import { displayResource } from "features/resources/resource";
|
||||
import TreeComponent from "features/trees/Tree.vue";
|
||||
import TreeNodeComponent from "features/trees/TreeNode.vue";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { convertComputable, processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, ref, shallowRef, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link TreeNode} features. */
|
||||
export const TreeNodeType = Symbol("TreeNode");
|
||||
/** A symbol used to identify {@link Tree} features. */
|
||||
export const TreeType = Symbol("Tree");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TreeNode}.
|
||||
*/
|
||||
export interface TreeNodeOptions {
|
||||
/** Whether this tree node should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** Whether or not this tree node can be clicked. */
|
||||
canClick?: Computable<boolean>;
|
||||
/** The background color for this node. */
|
||||
color?: Computable<string>;
|
||||
/** The label to display on this tree node. */
|
||||
display?: Computable<CoercableComponent>;
|
||||
/** The color of the glow effect shown to notify the user there's something to do with this node. */
|
||||
glowColor?: Computable<string>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** A reset object attached to this node, used for propagating resets through the tree. */
|
||||
reset?: GenericReset;
|
||||
/** A function that is called when the tree node is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the tree node is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}.
|
||||
*/
|
||||
export interface BaseTreeNode {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TreeNodeType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a node on a tree. */
|
||||
export type TreeNode<T extends TreeNodeOptions> = Replace<
|
||||
T & BaseTreeNode,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], true>;
|
||||
color: GetComputableType<T["color"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
glowColor: GetComputableType<T["glowColor"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link TreeNode} object. */
|
||||
export type GenericTreeNode = Replace<
|
||||
TreeNode<TreeNodeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tree node with the given options.
|
||||
* @param optionsFunc Tree Node options.
|
||||
*/
|
||||
export function createTreeNode<T extends TreeNodeOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>,
|
||||
...decorators: GenericDecorator[]
|
||||
): TreeNode<T> {
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const treeNode =
|
||||
optionsFunc?.call(feature, feature) ??
|
||||
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||
treeNode.id = getUniqueID("treeNode-");
|
||||
treeNode.type = TreeNodeType;
|
||||
treeNode[Component] = TreeNodeComponent as GenericComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(treeNode);
|
||||
}
|
||||
|
||||
Object.assign(decoratedData);
|
||||
|
||||
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, "glowColor");
|
||||
processComputable(treeNode as T, "classes");
|
||||
processComputable(treeNode as T, "style");
|
||||
processComputable(treeNode as T, "mark");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(treeNode);
|
||||
}
|
||||
|
||||
if (treeNode.onClick) {
|
||||
const onClick = treeNode.onClick.bind(treeNode);
|
||||
treeNode.onClick = function (e) {
|
||||
if (
|
||||
unref(treeNode.canClick as ProcessedComputable<boolean | undefined>) !== false
|
||||
) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (treeNode.onHold) {
|
||||
const onHold = treeNode.onHold.bind(treeNode);
|
||||
treeNode.onHold = function () {
|
||||
if (
|
||||
unref(treeNode.canClick as ProcessedComputable<boolean | undefined>) !== false
|
||||
) {
|
||||
onHold();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(treeNode)),
|
||||
{}
|
||||
);
|
||||
treeNode[GatherProps] = function (this: GenericTreeNode) {
|
||||
const {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
onClick,
|
||||
onHold,
|
||||
color,
|
||||
glowColor,
|
||||
canClick,
|
||||
mark,
|
||||
id
|
||||
} = this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
onClick,
|
||||
onHold,
|
||||
color,
|
||||
glowColor,
|
||||
canClick,
|
||||
mark,
|
||||
id,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return treeNode as unknown as TreeNode<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/** Represents a branch between two nodes in a tree. */
|
||||
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
|
||||
startNode: GenericTreeNode;
|
||||
endNode: GenericTreeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Tree}.
|
||||
*/
|
||||
export interface TreeOptions {
|
||||
/** Whether this clickable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** The nodes within the tree, in a 2D array. */
|
||||
nodes: Computable<GenericTreeNode[][]>;
|
||||
/** Nodes to show on the left side of the tree. */
|
||||
leftSideNodes?: Computable<GenericTreeNode[]>;
|
||||
/** Nodes to show on the right side of the tree. */
|
||||
rightSideNodes?: Computable<GenericTreeNode[]>;
|
||||
/** The branches between nodes within this tree. */
|
||||
branches?: Computable<TreeBranch[]>;
|
||||
/** How to propagate resets through the tree. */
|
||||
resetPropagation?: ResetPropagation;
|
||||
/** A function that is called when a node within the tree is reset. */
|
||||
onReset?: (node: GenericTreeNode) => void;
|
||||
}
|
||||
|
||||
export interface BaseTree {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** The link objects for each of the branches of the tree. */
|
||||
links: Ref<Link[]>;
|
||||
/** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */
|
||||
reset: (node: GenericTreeNode) => void;
|
||||
/** A flag that is true while the reset is still propagating through the tree. */
|
||||
isResetting: Ref<boolean>;
|
||||
/** A reference to the node that caused the currently propagating reset. */
|
||||
resettingNode: Ref<GenericTreeNode | null>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TreeType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */
|
||||
export type Tree<T extends TreeOptions> = Replace<
|
||||
T & BaseTree,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
nodes: GetComputableType<T["nodes"]>;
|
||||
leftSideNodes: GetComputableType<T["leftSideNodes"]>;
|
||||
rightSideNodes: GetComputableType<T["rightSideNodes"]>;
|
||||
branches: GetComputableType<T["branches"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Tree} object. */
|
||||
export type GenericTree = Replace<
|
||||
Tree<TreeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tree with the given options.
|
||||
* @param optionsFunc Tree options.
|
||||
*/
|
||||
export function createTree<T extends TreeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseTree, GenericTree>
|
||||
): Tree<T> {
|
||||
return createLazyProxy(feature => {
|
||||
const tree = optionsFunc.call(feature, feature);
|
||||
tree.id = getUniqueID("tree-");
|
||||
tree.type = TreeType;
|
||||
tree[Component] = TreeComponent as GenericComponent;
|
||||
|
||||
tree.isResetting = ref(false);
|
||||
tree.resettingNode = shallowRef(null);
|
||||
|
||||
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");
|
||||
|
||||
tree[GatherProps] = function (this: GenericTree) {
|
||||
const { nodes, leftSideNodes, rightSideNodes, branches } = this;
|
||||
return { nodes, leftSideNodes, rightSideNodes, branches };
|
||||
};
|
||||
|
||||
return tree as unknown as Tree<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/** A function that is used to propagate resets through a tree. */
|
||||
export type ResetPropagation = {
|
||||
(tree: GenericTree, resettingNode: GenericTreeNode): void;
|
||||
};
|
||||
|
||||
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||
export const defaultResetPropagation = function (
|
||||
tree: GenericTree,
|
||||
resettingNode: GenericTreeNode
|
||||
): void {
|
||||
const nodes = unref(tree.nodes);
|
||||
const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) - 1;
|
||||
for (let x = row; x >= 0; x--) {
|
||||
nodes[x].forEach(node => node.reset?.reset());
|
||||
}
|
||||
};
|
||||
|
||||
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||
export const invertedResetPropagation = function (
|
||||
tree: GenericTree,
|
||||
resettingNode: GenericTreeNode
|
||||
): void {
|
||||
const nodes = unref(tree.nodes);
|
||||
const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) + 1;
|
||||
for (let x = row; x < nodes.length; x++) {
|
||||
nodes[x].forEach(node => node.reset?.reset());
|
||||
}
|
||||
};
|
||||
|
||||
/** Propagate resets down the branches of the tree. */
|
||||
export const branchedResetPropagation = function (
|
||||
tree: GenericTree,
|
||||
resettingNode: GenericTreeNode
|
||||
): void {
|
||||
const links = unref(tree.branches);
|
||||
if (links == null) return;
|
||||
const reset: GenericTreeNode[] = [];
|
||||
let current = [resettingNode];
|
||||
while (current.length !== 0) {
|
||||
const next: GenericTreeNode[] = [];
|
||||
for (const node of current) {
|
||||
for (const link of links.filter(link => link.startNode === node)) {
|
||||
if ([...reset, ...current].includes(link.endNode)) continue;
|
||||
next.push(link.endNode);
|
||||
link.endNode.reset?.reset();
|
||||
}
|
||||
}
|
||||
reset.push(...current);
|
||||
current = next;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource.
|
||||
* It sounds oddly specific, but comes up a lot.
|
||||
*/
|
||||
export function createResourceTooltip(
|
||||
resource: Resource,
|
||||
requiredResource: Resource | null = null,
|
||||
requirement: Computable<DecimalSource> = 0
|
||||
): Ref<string> {
|
||||
const req = convertComputable(requirement);
|
||||
return computed(() => {
|
||||
if (requiredResource == null || Decimal.gte(resource.value, unref(req))) {
|
||||
return displayResource(resource) + " " + resource.displayName;
|
||||
}
|
||||
return `Reach ${
|
||||
Decimal.eq(requiredResource.precision, 0)
|
||||
? formatWhole(unref(req))
|
||||
: format(unref(req), requiredResource.precision)
|
||||
} ${requiredResource.displayName} to unlock (You have ${
|
||||
Decimal.eq(requiredResource.precision, 0)
|
||||
? formatWhole(requiredResource.value)
|
||||
: format(requiredResource.value, requiredResource.precision)
|
||||
})`;
|
||||
});
|
||||
}
|
279
src/features/trees/tree.tsx
Normal file
279
src/features/trees/tree.tsx
Normal file
|
@ -0,0 +1,279 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { Link } from "features/links/links";
|
||||
import type { Reset } from "features/reset";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import { displayResource } from "features/resources/resource";
|
||||
import Tree from "features/trees/Tree.vue";
|
||||
import TreeNode from "features/trees/TreeNode.vue";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue";
|
||||
import type { MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, ref, shallowRef, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link TreeNode} features. */
|
||||
export const TreeNodeType = Symbol("TreeNode");
|
||||
/** A symbol used to identify {@link Tree} features. */
|
||||
export const TreeType = Symbol("Tree");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link TreeNode}.
|
||||
*/
|
||||
export interface TreeNodeOptions extends VueFeatureOptions {
|
||||
/** Whether or not this tree node can be clicked. */
|
||||
canClick?: MaybeRefOrGetter<boolean>;
|
||||
/** The background color for this node. */
|
||||
color?: MaybeRefOrGetter<string>;
|
||||
/** The label to display on this tree node. */
|
||||
display?: MaybeRefOrGetter<Renderable>;
|
||||
/** The color of the glow effect shown to notify the user there's something to do with this node. */
|
||||
glowColor?: MaybeRefOrGetter<string>;
|
||||
/** A reset object attached to this node, used for propagating resets through the tree. */
|
||||
reset?: Reset;
|
||||
/** A function that is called when the tree node is clicked. */
|
||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||
/** A function that is called when the tree node is held down. */
|
||||
onHold?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}.
|
||||
*/
|
||||
export interface BaseTreeNode extends VueFeature {
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TreeNodeType;
|
||||
}
|
||||
|
||||
/** An object that represents a node on a tree. */
|
||||
export type TreeNode = Replace<
|
||||
TreeNodeOptions & BaseTreeNode,
|
||||
{
|
||||
canClick: MaybeRef<boolean>;
|
||||
color: ProcessedRefOrGetter<TreeNodeOptions["color"]>;
|
||||
display: ProcessedRefOrGetter<TreeNodeOptions["display"]>;
|
||||
glowColor: ProcessedRefOrGetter<TreeNodeOptions["glowColor"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tree node with the given options.
|
||||
* @param optionsFunc Tree Node options.
|
||||
*/
|
||||
export function createTreeNode<T extends TreeNodeOptions>(
|
||||
optionsFunc?: OptionsFunc<T, BaseTreeNode, TreeNode>
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc?.call(feature, feature as TreeNode) ?? ({} as T);
|
||||
const { canClick, color, display, glowColor, onClick, onHold, ...props } = options;
|
||||
|
||||
const treeNode = {
|
||||
type: TreeNodeType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TreeNodeOptions>),
|
||||
...vueFeatureMixin("treeNode", options, () => (
|
||||
<TreeNode
|
||||
canClick={treeNode.canClick}
|
||||
display={treeNode.display}
|
||||
onClick={treeNode.onClick}
|
||||
onHold={treeNode.onHold}
|
||||
color={treeNode.color}
|
||||
glowColor={treeNode.glowColor}
|
||||
/>
|
||||
)),
|
||||
canClick: processGetter(canClick) ?? true,
|
||||
color: processGetter(color),
|
||||
display: processGetter(display),
|
||||
glowColor: processGetter(glowColor),
|
||||
onClick:
|
||||
onClick == null
|
||||
? undefined
|
||||
: function (e) {
|
||||
if (unref(treeNode.canClick) !== false) {
|
||||
onClick.call(treeNode, e);
|
||||
}
|
||||
},
|
||||
onHold:
|
||||
onHold == null
|
||||
? undefined
|
||||
: function () {
|
||||
if (unref(treeNode.canClick) !== false) {
|
||||
onHold.call(treeNode);
|
||||
}
|
||||
}
|
||||
} satisfies TreeNode;
|
||||
|
||||
return treeNode;
|
||||
});
|
||||
}
|
||||
|
||||
/** Represents a branch between two nodes in a tree. */
|
||||
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
|
||||
startNode: TreeNode;
|
||||
endNode: TreeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Tree}.
|
||||
*/
|
||||
export interface TreeOptions extends VueFeatureOptions {
|
||||
/** The nodes within the tree, in a 2D array. */
|
||||
nodes: MaybeRefOrGetter<TreeNode[][]>;
|
||||
/** Nodes to show on the left side of the tree. */
|
||||
leftSideNodes?: MaybeRefOrGetter<TreeNode[]>;
|
||||
/** Nodes to show on the right side of the tree. */
|
||||
rightSideNodes?: MaybeRefOrGetter<TreeNode[]>;
|
||||
/** The branches between nodes within this tree. */
|
||||
branches?: MaybeRefOrGetter<TreeBranch[]>;
|
||||
/** How to propagate resets through the tree. */
|
||||
resetPropagation?: ResetPropagation;
|
||||
/** A function that is called when a node within the tree is reset. */
|
||||
onReset?: (node: TreeNode) => void;
|
||||
}
|
||||
|
||||
export interface BaseTree extends VueFeature {
|
||||
/** The link objects for each of the branches of the tree. */
|
||||
links: Ref<Link[]>;
|
||||
/** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */
|
||||
reset: (node: TreeNode) => void;
|
||||
/** A flag that is true while the reset is still propagating through the tree. */
|
||||
isResetting: Ref<boolean>;
|
||||
/** A reference to the node that caused the currently propagating reset. */
|
||||
resettingNode: Ref<TreeNode | null>;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof TreeType;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */
|
||||
export type Tree = Replace<
|
||||
TreeOptions & BaseTree,
|
||||
{
|
||||
nodes: ProcessedRefOrGetter<TreeOptions["nodes"]>;
|
||||
leftSideNodes: ProcessedRefOrGetter<TreeOptions["leftSideNodes"]>;
|
||||
rightSideNodes: ProcessedRefOrGetter<TreeOptions["rightSideNodes"]>;
|
||||
branches: ProcessedRefOrGetter<TreeOptions["branches"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates a tree with the given options.
|
||||
* @param optionsFunc Tree options.
|
||||
*/
|
||||
export function createTree<T extends TreeOptions>(optionsFunc: OptionsFunc<T, BaseTree, Tree>) {
|
||||
return createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Tree);
|
||||
const {
|
||||
branches,
|
||||
nodes,
|
||||
leftSideNodes,
|
||||
rightSideNodes,
|
||||
reset,
|
||||
resetPropagation,
|
||||
onReset,
|
||||
...props
|
||||
} = options;
|
||||
|
||||
const tree = {
|
||||
type: TreeType,
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TreeOptions>),
|
||||
...vueFeatureMixin("tree", options, () => (
|
||||
<Tree
|
||||
nodes={tree.nodes}
|
||||
leftSideNodes={tree.leftSideNodes}
|
||||
rightSideNodes={tree.rightSideNodes}
|
||||
branches={tree.branches}
|
||||
/>
|
||||
)),
|
||||
branches: processGetter(branches),
|
||||
isResetting: ref(false),
|
||||
resettingNode: shallowRef<TreeNode | null>(null),
|
||||
nodes: processGetter(nodes),
|
||||
leftSideNodes: processGetter(leftSideNodes),
|
||||
rightSideNodes: processGetter(rightSideNodes),
|
||||
links: processGetter(branches) ?? [],
|
||||
resetPropagation,
|
||||
onReset,
|
||||
reset:
|
||||
reset ??
|
||||
function (node: TreeNode) {
|
||||
tree.isResetting.value = true;
|
||||
tree.resettingNode.value = node;
|
||||
tree.resetPropagation?.(tree, node);
|
||||
tree.onReset?.(node);
|
||||
tree.isResetting.value = false;
|
||||
tree.resettingNode.value = null;
|
||||
}
|
||||
} satisfies Tree;
|
||||
|
||||
return tree;
|
||||
});
|
||||
}
|
||||
|
||||
/** A function that is used to propagate resets through a tree. */
|
||||
export type ResetPropagation = {
|
||||
(tree: Tree, resettingNode: TreeNode): void;
|
||||
};
|
||||
|
||||
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||
export const defaultResetPropagation = function (tree: Tree, resettingNode: TreeNode): void {
|
||||
const nodes = unref(tree.nodes);
|
||||
const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) - 1;
|
||||
for (let x = row; x >= 0; x--) {
|
||||
nodes[x].forEach(node => node.reset?.reset());
|
||||
}
|
||||
};
|
||||
|
||||
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||
export const invertedResetPropagation = function (tree: Tree, resettingNode: TreeNode): void {
|
||||
const nodes = unref(tree.nodes);
|
||||
const row = nodes.findIndex(nodes => nodes.includes(resettingNode)) + 1;
|
||||
for (let x = row; x < nodes.length; x++) {
|
||||
nodes[x].forEach(node => node.reset?.reset());
|
||||
}
|
||||
};
|
||||
|
||||
/** Propagate resets down the branches of the tree. */
|
||||
export const branchedResetPropagation = function (tree: Tree, resettingNode: TreeNode): void {
|
||||
const links = unref(tree.branches);
|
||||
if (links == null) return;
|
||||
const reset: TreeNode[] = [];
|
||||
let current = [resettingNode];
|
||||
while (current.length !== 0) {
|
||||
const next: TreeNode[] = [];
|
||||
for (const node of current) {
|
||||
for (const link of links.filter(link => link.startNode === node)) {
|
||||
if ([...reset, ...current].includes(link.endNode)) continue;
|
||||
next.push(link.endNode);
|
||||
link.endNode.reset?.reset();
|
||||
}
|
||||
}
|
||||
reset.push(...current);
|
||||
current = next;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource.
|
||||
* It sounds oddly specific, but comes up a lot.
|
||||
*/
|
||||
export function createResourceTooltip(
|
||||
resource: Resource,
|
||||
requiredResource: Resource | null = null,
|
||||
requirement: MaybeRefOrGetter<DecimalSource> = 0
|
||||
): Ref<string> {
|
||||
const req = processGetter(requirement);
|
||||
return computed(() => {
|
||||
if (requiredResource == null || Decimal.gte(resource.value, unref(req))) {
|
||||
return displayResource(resource) + " " + resource.displayName;
|
||||
}
|
||||
return `Reach ${
|
||||
Decimal.eq(requiredResource.precision, 0)
|
||||
? formatWhole(unref(req))
|
||||
: format(unref(req), requiredResource.precision)
|
||||
} ${requiredResource.displayName} to unlock (You have ${
|
||||
Decimal.eq(requiredResource.precision, 0)
|
||||
? formatWhole(requiredResource.value)
|
||||
: format(requiredResource.value, requiredResource.precision)
|
||||
})`;
|
||||
});
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="isVisible(visibility)"
|
||||
:style="[
|
||||
{
|
||||
visibility: isHidden(visibility) ? 'hidden' : undefined
|
||||
},
|
||||
unref(style) ?? {}
|
||||
]"
|
||||
@click="purchase"
|
||||
:class="{
|
||||
feature: true,
|
||||
upgrade: true,
|
||||
can: unref(canPurchase),
|
||||
locked: !unref(canPurchase),
|
||||
bought: unref(bought),
|
||||
...unref(classes)
|
||||
}"
|
||||
:disabled="!unref(canPurchase)"
|
||||
>
|
||||
<component v-if="unref(component)" :is="unref(component)" />
|
||||
<MarkNode :mark="unref(mark)" />
|
||||
<Node :id="id" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "components/common/features.css";
|
||||
import MarkNode from "components/MarkNode.vue";
|
||||
import Node from "components/Node.vue";
|
||||
import type { StyleValue } from "features/feature";
|
||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||
import type { GenericUpgrade } from "features/upgrades/upgrade";
|
||||
import { displayRequirements, Requirements } from "game/requirements";
|
||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||
import type { Component, UnwrapRef } from "vue";
|
||||
import { shallowRef, unref, watchEffect } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
display: UnwrapRef<GenericUpgrade["display"]>;
|
||||
visibility: Visibility | boolean;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
requirements: Requirements;
|
||||
canPurchase: boolean;
|
||||
bought: boolean;
|
||||
mark?: boolean | string;
|
||||
id: string;
|
||||
purchase?: VoidFunction;
|
||||
}>();
|
||||
|
||||
const component = shallowRef<Component | string>("");
|
||||
|
||||
watchEffect(() => {
|
||||
const currDisplay = props.display;
|
||||
if (currDisplay == null) {
|
||||
component.value = "";
|
||||
return;
|
||||
}
|
||||
if (isCoercableComponent(currDisplay)) {
|
||||
component.value = coerceComponent(currDisplay);
|
||||
return;
|
||||
}
|
||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||
const Description = coerceComponent(currDisplay.description, "div");
|
||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||
component.value = coerceComponent(
|
||||
jsx(() => (
|
||||
<span>
|
||||
{currDisplay.title != null ? (
|
||||
<div>
|
||||
<Title />
|
||||
</div>
|
||||
) : null}
|
||||
<Description />
|
||||
{currDisplay.effectDisplay != null ? (
|
||||
<div>
|
||||
Currently: <EffectDisplay />
|
||||
</div>
|
||||
) : null}
|
||||
{props.bought ? null : <><br />{displayRequirements(props.requirements)}</>}
|
||||
</span>
|
||||
))
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upgrade {
|
||||
min-height: 120px;
|
||||
width: 120px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.upgrade > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -1,227 +0,0 @@
|
|||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
GenericComponent,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import {
|
||||
Component,
|
||||
GatherProps,
|
||||
Visibility,
|
||||
findFeatures,
|
||||
getUniqueID,
|
||||
setDefault
|
||||
} from "features/feature";
|
||||
import UpgradeComponent from "features/upgrades/Upgrade.vue";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import {
|
||||
Requirements,
|
||||
createVisibilityRequirement,
|
||||
payRequirements,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import { isFunction } from "util/common";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
|
||||
/** A symbol used to identify {@link Upgrade} features. */
|
||||
export const UpgradeType = Symbol("Upgrade");
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Upgrade}.
|
||||
*/
|
||||
export interface UpgradeOptions {
|
||||
/** Whether this clickable should be visible. */
|
||||
visibility?: Computable<Visibility | boolean>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: Computable<StyleValue>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
mark?: Computable<boolean | string>;
|
||||
/** The display to use for this clickable. */
|
||||
display?: Computable<
|
||||
| CoercableComponent
|
||||
| {
|
||||
/** A header to appear at the top of the display. */
|
||||
title?: CoercableComponent;
|
||||
/** The main text that appears in the display. */
|
||||
description: CoercableComponent;
|
||||
/** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
|
||||
effectDisplay?: CoercableComponent;
|
||||
}
|
||||
>;
|
||||
/** The requirements to purchase this upgrade. */
|
||||
requirements: Requirements;
|
||||
/** A function that is called when the upgrade is purchased. */
|
||||
onPurchase?: VoidFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link UpgradeOptions} to create an {@link Upgrade}.
|
||||
*/
|
||||
export interface BaseUpgrade {
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** Whether or not this upgrade has been purchased. */
|
||||
bought: Persistent<boolean>;
|
||||
/** Whether or not the upgrade can currently be purchased. */
|
||||
canPurchase: Ref<boolean>;
|
||||
/** Purchase the upgrade */
|
||||
purchase: VoidFunction;
|
||||
/** A symbol that helps identify features of the same type. */
|
||||
type: typeof UpgradeType;
|
||||
/** The Vue component used to render this feature. */
|
||||
[Component]: GenericComponent;
|
||||
/** A function to gather the props the vue component requires for this feature. */
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** An object that represents a feature that can be purchased a single time. */
|
||||
export type Upgrade<T extends UpgradeOptions> = Replace<
|
||||
T & BaseUpgrade,
|
||||
{
|
||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Upgrade} object. */
|
||||
export type GenericUpgrade = Replace<
|
||||
Upgrade<UpgradeOptions>,
|
||||
{
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lazily creates an upgrade with the given options.
|
||||
* @param optionsFunc Upgrade options.
|
||||
*/
|
||||
export function createUpgrade<T extends UpgradeOptions>(
|
||||
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>,
|
||||
...decorators: GenericDecorator[]
|
||||
): Upgrade<T> {
|
||||
const bought = persistent<boolean>(false, false);
|
||||
const decoratedData = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||
{}
|
||||
);
|
||||
return createLazyProxy(feature => {
|
||||
const upgrade = optionsFunc.call(feature, feature);
|
||||
upgrade.id = getUniqueID("upgrade-");
|
||||
upgrade.type = UpgradeType;
|
||||
upgrade[Component] = UpgradeComponent as GenericComponent;
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.preConstruct?.(upgrade);
|
||||
}
|
||||
|
||||
upgrade.bought = bought;
|
||||
Object.assign(upgrade, decoratedData);
|
||||
|
||||
upgrade.canPurchase = computed(
|
||||
() => !bought.value && requirementsMet(upgrade.requirements)
|
||||
);
|
||||
upgrade.purchase = function () {
|
||||
const genericUpgrade = upgrade as GenericUpgrade;
|
||||
if (!unref(genericUpgrade.canPurchase)) {
|
||||
return;
|
||||
}
|
||||
payRequirements(upgrade.requirements);
|
||||
bought.value = true;
|
||||
genericUpgrade.onPurchase?.();
|
||||
};
|
||||
|
||||
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
|
||||
if (Array.isArray(upgrade.requirements)) {
|
||||
upgrade.requirements.unshift(visibilityRequirement);
|
||||
} else {
|
||||
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(upgrade);
|
||||
}
|
||||
|
||||
const decoratedProps = decorators.reduce(
|
||||
(current, next) => Object.assign(current, next.getGatheredProps?.(upgrade)),
|
||||
{}
|
||||
);
|
||||
upgrade[GatherProps] = function (this: GenericUpgrade) {
|
||||
const {
|
||||
display,
|
||||
visibility,
|
||||
style,
|
||||
classes,
|
||||
requirements,
|
||||
canPurchase,
|
||||
bought,
|
||||
mark,
|
||||
id,
|
||||
purchase
|
||||
} = this;
|
||||
return {
|
||||
display,
|
||||
visibility,
|
||||
style: unref(style),
|
||||
classes,
|
||||
requirements,
|
||||
canPurchase,
|
||||
bought,
|
||||
mark,
|
||||
id,
|
||||
purchase,
|
||||
...decoratedProps
|
||||
};
|
||||
};
|
||||
|
||||
return upgrade as unknown as Upgrade<T>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to auto purchase a list of upgrades whenever they're affordable.
|
||||
* @param layer The layer the upgrades are apart of
|
||||
* @param autoActive Whether or not the upgrades should currently be auto-purchasing
|
||||
* @param upgrades The specific upgrades to upgrade. If unspecified, uses all upgrades on the layer.
|
||||
*/
|
||||
export function setupAutoPurchase(
|
||||
layer: GenericLayer,
|
||||
autoActive: Computable<boolean>,
|
||||
upgrades: GenericUpgrade[] = []
|
||||
): void {
|
||||
upgrades =
|
||||
upgrades.length === 0 ? (findFeatures(layer, UpgradeType) as GenericUpgrade[]) : upgrades;
|
||||
const isAutoActive: ProcessedComputable<boolean> = isFunction(autoActive)
|
||||
? computed(autoActive)
|
||||
: autoActive;
|
||||
layer.on("update", () => {
|
||||
if (unref(isAutoActive)) {
|
||||
upgrades.forEach(upgrade => upgrade.purchase());
|
||||
}
|
||||
});
|
||||
}
|
|
@ -7,23 +7,17 @@
|
|||
@mouseup="e => mouseUp(e)"
|
||||
@touchend.passive="e => mouseUp(e)"
|
||||
>
|
||||
<component v-if="comp" :is="comp" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { jsx } from "features/feature";
|
||||
import { VueFeature, coerceComponent, renderJSX } from "util/vue";
|
||||
import { Ref, shallowRef, unref } from "vue";
|
||||
import { Ref, unref } from "vue";
|
||||
import { NodePosition } from "./board";
|
||||
unref;
|
||||
|
||||
const props = defineProps<{
|
||||
element: VueFeature;
|
||||
defineProps<{
|
||||
mouseDown: (e: MouseEvent | TouchEvent) => void;
|
||||
mouseUp: (e: MouseEvent | TouchEvent) => void;
|
||||
position: Ref<NodePosition>;
|
||||
}>();
|
||||
|
||||
const comp = shallowRef(coerceComponent(jsx(() => renderJSX(props.element))));
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { Component, GatherProps, GenericComponent, jsx } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import { Persistent, persistent } from "game/persistence";
|
||||
import type { PanZoom } from "panzoom";
|
||||
import { Direction, isFunction } from "util/common";
|
||||
import type { Computable, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable } from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { VueFeature } from "util/vue";
|
||||
import type { ComponentPublicInstance, Ref } from "vue";
|
||||
import { computed, nextTick, ref, unref, watchEffect } from "vue";
|
||||
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { computed, ref, unref, watchEffect } from "vue";
|
||||
import panZoom from "vue-panzoom";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
import Board from "./Board.vue";
|
||||
|
@ -20,10 +18,10 @@ globalBus.on("setupVue", app => panZoom.install(app));
|
|||
export type NodePosition = { x: number; y: number };
|
||||
|
||||
/**
|
||||
* A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
|
||||
* A type representing a MaybeRefOrGetter value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
|
||||
*/
|
||||
export type NodeComputable<T, R, S extends unknown[] = []> =
|
||||
| Computable<R>
|
||||
export type NodeMaybeRefOrGetter<T, R, S extends unknown[] = []> =
|
||||
| MaybeRefOrGetter<R>
|
||||
| ((node: T, ...args: S) => R);
|
||||
|
||||
/**
|
||||
|
@ -32,11 +30,11 @@ export type NodeComputable<T, R, S extends unknown[] = []> =
|
|||
* @param node The node to get the property of
|
||||
*/
|
||||
export function unwrapNodeRef<T, R, S extends unknown[]>(
|
||||
property: NodeComputable<T, R, S>,
|
||||
property: NodeMaybeRefOrGetter<T, R, S>,
|
||||
node: T,
|
||||
...args: S
|
||||
): R {
|
||||
return isFunction<R, [T, ...S], ProcessedComputable<R>>(property)
|
||||
return isFunction<R, [T, ...S], MaybeRef<R>>(property)
|
||||
? property(node, ...args)
|
||||
: unref(property);
|
||||
}
|
||||
|
@ -46,8 +44,8 @@ export function unwrapNodeRef<T, R, S extends unknown[]>(
|
|||
* @param nodes The list of current nodes with IDs as properties
|
||||
* @returns A computed ref that will give the value of the next unique ID
|
||||
*/
|
||||
export function setupUniqueIds(nodes: Computable<{ id: number }[]>) {
|
||||
const processedNodes = convertComputable(nodes);
|
||||
export function setupUniqueIds(nodes: MaybeRefOrGetter<{ id: number }[]>) {
|
||||
const processedNodes = processGetter(nodes);
|
||||
return computed(() => Math.max(-1, ...unref(processedNodes).map(node => node.id)) + 1);
|
||||
}
|
||||
|
||||
|
@ -60,9 +58,9 @@ export interface DraggableNodeOptions<T> {
|
|||
/** Setter function to update the position of a node. */
|
||||
setPosition: (node: T, position: NodePosition) => void;
|
||||
/** A list of nodes that the currently dragged node can be dropped upon. */
|
||||
receivingNodes?: NodeComputable<T, T[]>;
|
||||
receivingNodes?: NodeMaybeRefOrGetter<T, T[]>;
|
||||
/** The maximum distance (in pixels, before zoom) away a node can be and still drop onto a receiving node. */
|
||||
dropAreaRadius?: NodeComputable<T, number>;
|
||||
dropAreaRadius?: NodeMaybeRefOrGetter<T, number>;
|
||||
/** A callback for when a node gets dropped upon a receiving node. */
|
||||
onDrop?: (acceptingNode: T, draggingNode: T) => void;
|
||||
}
|
||||
|
@ -262,12 +260,12 @@ export interface MakeDraggableOptions<T> {
|
|||
* @param element The vue feature to make draggable.
|
||||
* @param options The options to configure the dragging behavior.
|
||||
*/
|
||||
export function makeDraggable<T extends VueFeature, S>(
|
||||
element: T,
|
||||
options: MakeDraggableOptions<S>
|
||||
): asserts element is T & { position: Persistent<NodePosition> } {
|
||||
export function makeDraggable<T>(
|
||||
element: VueFeature,
|
||||
options: MakeDraggableOptions<T>
|
||||
): asserts element is VueFeature & { position: Persistent<NodePosition> } {
|
||||
const position = persistent(options.initialPosition ?? { x: 0, y: 0 });
|
||||
(element as T & { position: Persistent<NodePosition> }).position = position;
|
||||
(element as VueFeature & { position: Persistent<NodePosition> }).position = position;
|
||||
const computedPosition = computed(() => {
|
||||
if (options.nodeBeingDragged.value === options.id) {
|
||||
return {
|
||||
|
@ -292,36 +290,25 @@ export function makeDraggable<T extends VueFeature, S>(
|
|||
options.onMouseUp?.(e);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const elementComponent = element[Component];
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
element[Component] = Draggable as GenericComponent;
|
||||
element[GatherProps] = function gatherTooltipProps(this: typeof options) {
|
||||
return {
|
||||
element: {
|
||||
[Component]: elementComponent,
|
||||
[GatherProps]: elementGatherProps
|
||||
},
|
||||
mouseDown: handleMouseDown,
|
||||
mouseUp: handleMouseUp,
|
||||
position: computedPosition
|
||||
};
|
||||
}.bind(options);
|
||||
});
|
||||
element.wrappers.push(el => (
|
||||
<Draggable mouseDown={handleMouseDown} mouseUp={handleMouseUp} position={computedPosition}>
|
||||
{el}
|
||||
</Draggable>
|
||||
));
|
||||
}
|
||||
|
||||
/** An object that configures how to setup a list of actions using {@link setupActions}. */
|
||||
export interface SetupActionsOptions<T extends NodePosition> {
|
||||
/** The node to display actions upon, or undefined when the actions should be hidden. */
|
||||
node: Computable<T | undefined>;
|
||||
node: MaybeRefOrGetter<T | undefined>;
|
||||
/** Whether or not to currently display the actions. */
|
||||
shouldShowActions?: NodeComputable<T, boolean>;
|
||||
shouldShowActions?: NodeMaybeRefOrGetter<T, boolean>;
|
||||
/** The list of actions to display. Actions are arbitrary JSX elements. */
|
||||
actions: NodeComputable<T, ((position: NodePosition) => JSX.Element)[]>;
|
||||
actions: NodeMaybeRefOrGetter<T, ((position: NodePosition) => JSX.Element)[]>;
|
||||
/** The distance from the node to place the actions. */
|
||||
distance: NodeComputable<T, number>;
|
||||
distance: NodeMaybeRefOrGetter<T, number>;
|
||||
/** The arc length to place between actions, in radians. */
|
||||
arcLength?: NodeComputable<T, number>;
|
||||
arcLength?: NodeMaybeRefOrGetter<T, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -330,8 +317,8 @@ export interface SetupActionsOptions<T extends NodePosition> {
|
|||
* @returns A JSX function to render the actions.
|
||||
*/
|
||||
export function setupActions<T extends NodePosition>(options: SetupActionsOptions<T>) {
|
||||
const node = convertComputable(options.node);
|
||||
return jsx(() => {
|
||||
const node = processGetter(options.node) as MaybeRef<T | undefined>;
|
||||
return computed(() => {
|
||||
const currNode = unref(node);
|
||||
if (currNode == null) {
|
||||
return "";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Settings } from "game/settings";
|
||||
import { createNanoEvents } from "nanoevents";
|
||||
import type { App } from "vue";
|
||||
import type { GenericLayer } from "./layers";
|
||||
import type { Layer } from "./layers";
|
||||
import state from "./state";
|
||||
|
||||
/** All types of events able to be sent or emitted from the global event bus. */
|
||||
|
@ -11,12 +11,12 @@ export interface GlobalEvents {
|
|||
* @param layer The layer being added.
|
||||
* @param saveData The layer's save data object within player.
|
||||
*/
|
||||
addLayer: (layer: GenericLayer, saveData: Record<string, unknown>) => void;
|
||||
addLayer: (layer: Layer, saveData: Record<string, unknown>) => void;
|
||||
/**
|
||||
* Sent whenever a layer is removed.
|
||||
* @param layer The layer being removed.
|
||||
*/
|
||||
removeLayer: (layer: GenericLayer) => void;
|
||||
removeLayer: (layer: Layer) => void;
|
||||
/**
|
||||
* Sent every game tick. Runs the life cycle of the project.
|
||||
* @param diff The delta time since last tick, in ms.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Resource } from "features/resources/resource";
|
||||
import { NonPersistent } from "game/persistence";
|
||||
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||
import { Computable, ProcessedComputable, convertComputable } from "util/computed";
|
||||
import { MaybeRefOrGetter, MaybeRef, processGetter } from "util/computed";
|
||||
import { Ref, computed, ref, unref } from "vue";
|
||||
import * as ops from "./operations";
|
||||
import type {
|
||||
|
@ -60,7 +60,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
protected readonly description: string | undefined;
|
||||
protected readonly internalVariables: number;
|
||||
|
||||
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
|
||||
public readonly innermostVariable: MaybeRef<DecimalSource> | undefined;
|
||||
|
||||
constructor(options: FormulaOptions<T>) {
|
||||
let readonlyProperties;
|
||||
|
@ -93,7 +93,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
private setupVariable({
|
||||
variable
|
||||
}: {
|
||||
variable: ProcessedComputable<DecimalSource>;
|
||||
variable: MaybeRef<DecimalSource>;
|
||||
}): InternalFormulaProperties<T> {
|
||||
return {
|
||||
inputs: [variable] as T,
|
||||
|
@ -207,7 +207,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
* Creates a formula that evaluates to a constant value.
|
||||
* @param value The constant value for this formula.
|
||||
*/
|
||||
public static constant(value: ProcessedComputable<DecimalSource>): InvertibleIntegralFormula {
|
||||
public static constant(value: MaybeRef<DecimalSource>): InvertibleIntegralFormula {
|
||||
return new Formula({ inputs: [value] });
|
||||
}
|
||||
|
||||
|
@ -215,7 +215,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
* Creates a formula that is marked as the variable for an outer formula. Typically used for inverting and integrating.
|
||||
* @param value The variable for this formula.
|
||||
*/
|
||||
public static variable(value: ProcessedComputable<DecimalSource>): InvertibleIntegralFormula {
|
||||
public static variable(value: MaybeRef<DecimalSource>): InvertibleIntegralFormula {
|
||||
return new Formula({ variable: value });
|
||||
}
|
||||
|
||||
|
@ -248,11 +248,11 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
*/
|
||||
public static step(
|
||||
value: FormulaSource,
|
||||
start: Computable<DecimalSource>,
|
||||
start: MaybeRefOrGetter<DecimalSource>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
const formula = formulaModifier(Formula.variable(0));
|
||||
const processedStart = convertComputable(start);
|
||||
const processedStart = processGetter(start);
|
||||
function evalStep(lhs: DecimalSource) {
|
||||
if (Decimal.lt(lhs, unref(processedStart))) {
|
||||
return lhs;
|
||||
|
@ -293,7 +293,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
*/
|
||||
public static if(
|
||||
value: FormulaSource,
|
||||
condition: Computable<boolean>,
|
||||
condition: MaybeRefOrGetter<boolean>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula,
|
||||
elseFormulaModifier?: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
|
@ -301,7 +301,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
const variable = Formula.variable(lhsRef);
|
||||
const formula = formulaModifier(variable);
|
||||
const elseFormula = elseFormulaModifier?.(variable);
|
||||
const processedCondition = convertComputable(condition);
|
||||
const processedCondition = processGetter(condition);
|
||||
function evalStep(lhs: DecimalSource) {
|
||||
if (unref(processedCondition)) {
|
||||
lhsRef.value = lhs;
|
||||
|
@ -340,7 +340,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
}
|
||||
public static conditional(
|
||||
value: FormulaSource,
|
||||
condition: Computable<boolean>,
|
||||
condition: MaybeRefOrGetter<boolean>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula,
|
||||
elseFormulaModifier?: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
|
@ -909,20 +909,20 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
}
|
||||
|
||||
public step(
|
||||
start: Computable<DecimalSource>,
|
||||
start: MaybeRefOrGetter<DecimalSource>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
return Formula.step(this, start, formulaModifier);
|
||||
}
|
||||
|
||||
public if(
|
||||
condition: Computable<boolean>,
|
||||
condition: MaybeRefOrGetter<boolean>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
return Formula.if(this, condition, formulaModifier);
|
||||
}
|
||||
public conditional(
|
||||
condition: Computable<boolean>,
|
||||
condition: MaybeRefOrGetter<boolean>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
return Formula.if(this, condition, formulaModifier);
|
||||
|
@ -1443,13 +1443,13 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
|
|||
export function calculateMaxAffordable(
|
||||
formula: GenericFormula,
|
||||
resource: Resource,
|
||||
cumulativeCost: Computable<boolean> = true,
|
||||
directSum?: Computable<number>,
|
||||
maxBulkAmount: Computable<DecimalSource> = Decimal.dInf
|
||||
cumulativeCost: MaybeRefOrGetter<boolean> = true,
|
||||
directSum?: MaybeRefOrGetter<number>,
|
||||
maxBulkAmount: MaybeRefOrGetter<DecimalSource> = Decimal.dInf
|
||||
) {
|
||||
const computedCumulativeCost = convertComputable(cumulativeCost);
|
||||
const computedDirectSum = convertComputable(directSum);
|
||||
const computedmaxBulkAmount = convertComputable(maxBulkAmount);
|
||||
const computedCumulativeCost = processGetter(cumulativeCost);
|
||||
const computedDirectSum = processGetter(directSum);
|
||||
const computedmaxBulkAmount = processGetter(maxBulkAmount);
|
||||
return computed(() => {
|
||||
const maxBulkAmount = unref(computedmaxBulkAmount);
|
||||
if (Decimal.eq(maxBulkAmount, 1)) {
|
||||
|
|
8
src/game/formulas/types.d.ts
vendored
8
src/game/formulas/types.d.ts
vendored
|
@ -1,10 +1,10 @@
|
|||
import { InternalFormula } from "game/formulas/formulas";
|
||||
import { DecimalSource } from "util/bignum";
|
||||
import { ProcessedComputable } from "util/computed";
|
||||
import { MaybeRef } from "util/computed";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type GenericFormula = InternalFormula<any>;
|
||||
type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
|
||||
type FormulaSource = MaybeRef<DecimalSource> | GenericFormula;
|
||||
type InvertibleFormula = GenericFormula & {
|
||||
invert: NonNullable<GenericFormula["invert"]>;
|
||||
};
|
||||
|
@ -38,7 +38,7 @@ type SubstitutionFunction<T> = (
|
|||
) => GenericFormula;
|
||||
|
||||
type VariableFormulaOptions = {
|
||||
variable: ProcessedComputable<DecimalSource>;
|
||||
variable: MaybeRef<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
type ConstantFormulaOptions = {
|
||||
|
@ -67,7 +67,7 @@ type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
|
|||
internalIntegrate?: IntegrateFunction<T>;
|
||||
internalIntegrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
innermostVariable?: ProcessedComputable<DecimalSource>;
|
||||
innermostVariable?: MaybeRef<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,28 +1,26 @@
|
|||
import Modal from "components/modals/Modal.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
JSXFunction,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
import { jsx, setDefault } from "features/feature";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import type { Emitter } from "nanoevents";
|
||||
import { createNanoEvents } from "nanoevents";
|
||||
import type {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
GetComputableTypeWithDefault,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processComputable } from "util/computed";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { computed, InjectionKey, Ref } from "vue";
|
||||
import { ref, shallowReactive, unref } from "vue";
|
||||
import { Renderable } from "util/vue";
|
||||
import {
|
||||
computed,
|
||||
type CSSProperties,
|
||||
InjectionKey,
|
||||
MaybeRef,
|
||||
MaybeRefOrGetter,
|
||||
Ref,
|
||||
ref,
|
||||
shallowReactive,
|
||||
unref
|
||||
} from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
|
||||
/** A feature's node in the DOM that has its size tracked. */
|
||||
export interface FeatureNode {
|
||||
|
@ -74,12 +72,12 @@ export interface LayerEvents {
|
|||
* A reference to all the current layers.
|
||||
* It is shallow reactive so it will update when layers are added or removed, but not interfere with the existing refs within each layer.
|
||||
*/
|
||||
export const layers: Record<string, Readonly<GenericLayer> | undefined> = shallowReactive({});
|
||||
export const layers: Record<string, Readonly<Layer>> = shallowReactive({});
|
||||
|
||||
declare global {
|
||||
/** Augment the window object so the layers can be accessed from the console. */
|
||||
interface Window {
|
||||
layers: Record<string, Readonly<GenericLayer> | undefined>;
|
||||
layers: Record<string, Readonly<Layer> | undefined>;
|
||||
}
|
||||
}
|
||||
window.layers = layers;
|
||||
|
@ -106,42 +104,42 @@ export interface Position {
|
|||
*/
|
||||
export interface LayerOptions {
|
||||
/** The color of the layer, used to theme the entire layer's display. */
|
||||
color?: Computable<string>;
|
||||
color?: MaybeRefOrGetter<string>;
|
||||
/**
|
||||
* The layout of this layer's features.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is displayed.
|
||||
*/
|
||||
display: Computable<CoercableComponent>;
|
||||
display: MaybeRefOrGetter<Renderable>;
|
||||
/** An object of classes that should be applied to the display. */
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
classes?: MaybeRefOrGetter<Record<string, boolean>>;
|
||||
/** Styles that should be applied to the display. */
|
||||
style?: Computable<StyleValue>;
|
||||
style?: MaybeRefOrGetter<CSSProperties>;
|
||||
/**
|
||||
* The name of the layer, used on minimized tabs.
|
||||
* Defaults to {@link BaseLayer.id}.
|
||||
*/
|
||||
name?: Computable<string>;
|
||||
name?: MaybeRefOrGetter<string>;
|
||||
/**
|
||||
* Whether or not the layer can be minimized.
|
||||
* Defaults to true.
|
||||
*/
|
||||
minimizable?: Computable<boolean>;
|
||||
minimizable?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* The layout of this layer's features.
|
||||
* When the layer is open in {@link game/player.PlayerData.tabs}, but the tab is {@link Layer.minimized} this is the content that is displayed.
|
||||
*/
|
||||
minimizedDisplay?: Computable<CoercableComponent>;
|
||||
minimizedDisplay?: MaybeRefOrGetter<Renderable>;
|
||||
/**
|
||||
* Whether or not to force the go back button to be hidden.
|
||||
* If true, go back will be hidden regardless of {@link data/projInfo.allowGoBack}.
|
||||
*/
|
||||
forceHideGoBack?: Computable<boolean>;
|
||||
forceHideGoBack?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* A CSS min-width value that is applied to the layer.
|
||||
* Can be a number, in which case the unit is assumed to be px.
|
||||
* Defaults to 600px.
|
||||
*/
|
||||
minWidth?: Computable<number | string>;
|
||||
minWidth?: MaybeRefOrGetter<number | string>;
|
||||
}
|
||||
|
||||
/** The properties that are added onto a processed {@link LayerOptions} to create a {@link Layer} */
|
||||
|
@ -165,28 +163,18 @@ export interface BaseLayer {
|
|||
}
|
||||
|
||||
/** An unit of game content. Displayed to the user as a tab or modal. */
|
||||
export type Layer<T extends LayerOptions> = Replace<
|
||||
T & BaseLayer,
|
||||
export type Layer = Replace<
|
||||
Replace<LayerOptions, BaseLayer>,
|
||||
{
|
||||
color: GetComputableType<T["color"]>;
|
||||
display: GetComputableType<T["display"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
name: GetComputableTypeWithDefault<T["name"], string>;
|
||||
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
|
||||
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
|
||||
minimizedDisplay: GetComputableType<T["minimizedDisplay"]>;
|
||||
forceHideGoBack: GetComputableType<T["forceHideGoBack"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/** A type that matches any valid {@link Layer} object. */
|
||||
export type GenericLayer = Replace<
|
||||
Layer<LayerOptions>,
|
||||
{
|
||||
name: ProcessedComputable<string>;
|
||||
minWidth: ProcessedComputable<number>;
|
||||
minimizable: ProcessedComputable<boolean>;
|
||||
color?: ProcessedRefOrGetter<LayerOptions["color"]>;
|
||||
display: ProcessedRefOrGetter<LayerOptions["display"]>;
|
||||
classes?: ProcessedRefOrGetter<LayerOptions["classes"]>;
|
||||
style?: ProcessedRefOrGetter<LayerOptions["style"]>;
|
||||
name: MaybeRef<string>;
|
||||
minWidth: MaybeRef<string | number>;
|
||||
minimizable: MaybeRef<boolean>;
|
||||
minimizedDisplay?: ProcessedRefOrGetter<LayerOptions["minimizedDisplay"]>;
|
||||
forceHideGoBack?: ProcessedRefOrGetter<LayerOptions["forceHideGoBack"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -206,72 +194,85 @@ export const addingLayers: string[] = [];
|
|||
export function createLayer<T extends LayerOptions>(
|
||||
id: string,
|
||||
optionsFunc: OptionsFunc<T, BaseLayer>
|
||||
): Layer<T> {
|
||||
) {
|
||||
return createLazyProxy(() => {
|
||||
const layer = {} as T & Partial<BaseLayer>;
|
||||
const emitter = (layer.emitter = createNanoEvents<LayerEvents>());
|
||||
layer.on = emitter.on.bind(emitter);
|
||||
layer.emit = emitter.emit.bind(emitter) as <K extends keyof LayerEvents>(
|
||||
...args: [K, ...Parameters<LayerEvents[K]>]
|
||||
) => void;
|
||||
layer.nodes = ref({});
|
||||
layer.id = id;
|
||||
|
||||
const emitter = createNanoEvents<LayerEvents>();
|
||||
addingLayers.push(id);
|
||||
persistentRefs[id] = new Set();
|
||||
layer.minimized = persistent(false, false);
|
||||
Object.assign(layer, optionsFunc.call(layer, layer as BaseLayer));
|
||||
|
||||
const baseLayer = {
|
||||
id,
|
||||
emitter,
|
||||
...emitter,
|
||||
nodes: ref({}),
|
||||
minimized: persistent(false, false)
|
||||
} satisfies BaseLayer;
|
||||
|
||||
const options = optionsFunc.call(baseLayer, baseLayer);
|
||||
const {
|
||||
color,
|
||||
display,
|
||||
classes,
|
||||
style: _style,
|
||||
name,
|
||||
forceHideGoBack,
|
||||
minWidth,
|
||||
minimizable,
|
||||
minimizedDisplay,
|
||||
...props
|
||||
} = options;
|
||||
if (
|
||||
addingLayers[addingLayers.length - 1] == null ||
|
||||
addingLayers[addingLayers.length - 1] !== id
|
||||
) {
|
||||
throw new Error(
|
||||
`Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`
|
||||
`Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${id}`
|
||||
);
|
||||
}
|
||||
addingLayers.pop();
|
||||
|
||||
processComputable(layer as T, "color");
|
||||
processComputable(layer as T, "display");
|
||||
processComputable(layer as T, "classes");
|
||||
processComputable(layer as T, "style");
|
||||
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, "minimizedDisplay");
|
||||
const style = processGetter(_style);
|
||||
|
||||
const style = layer.style as ProcessedComputable<StyleValue> | undefined;
|
||||
layer.style = computed(() => {
|
||||
let width = unref(layer.minWidth as ProcessedComputable<number | string>);
|
||||
if (typeof width === "number" || !Number.isNaN(parseInt(width))) {
|
||||
width = width + "px";
|
||||
}
|
||||
return [
|
||||
unref(style) ?? "",
|
||||
layer.minimized?.value
|
||||
? {
|
||||
flexGrow: "0",
|
||||
flexShrink: "0",
|
||||
width: "60px",
|
||||
minWidth: "",
|
||||
flexBasis: "",
|
||||
margin: "0"
|
||||
}
|
||||
: {
|
||||
flexGrow: "",
|
||||
flexShrink: "",
|
||||
width: "",
|
||||
minWidth: width,
|
||||
flexBasis: width,
|
||||
margin: ""
|
||||
}
|
||||
];
|
||||
}) as Ref<StyleValue>;
|
||||
const layer = {
|
||||
...baseLayer,
|
||||
...(props as Omit<typeof props, keyof LayerOptions>),
|
||||
color: processGetter(color),
|
||||
display: processGetter(display),
|
||||
classes: processGetter(classes),
|
||||
style: computed((): CSSProperties => {
|
||||
let width = unref(layer.minWidth);
|
||||
if (typeof width === "number" || !Number.isNaN(parseInt(width))) {
|
||||
width = width + "px";
|
||||
}
|
||||
return {
|
||||
...unref(style),
|
||||
...(baseLayer.minimized.value
|
||||
? {
|
||||
flexGrow: "0",
|
||||
flexShrink: "0",
|
||||
width: "60px",
|
||||
minWidth: "",
|
||||
flexBasis: "",
|
||||
margin: "0"
|
||||
}
|
||||
: {
|
||||
flexGrow: "",
|
||||
flexShrink: "",
|
||||
width: "",
|
||||
minWidth: width,
|
||||
flexBasis: width,
|
||||
margin: ""
|
||||
})
|
||||
};
|
||||
}),
|
||||
name: processGetter(name) ?? id,
|
||||
forceHideGoBack: processGetter(forceHideGoBack),
|
||||
minWidth: processGetter(minWidth) ?? 600,
|
||||
minimizable: processGetter(minimizable) ?? true,
|
||||
minimizedDisplay: processGetter(minimizedDisplay)
|
||||
} satisfies Layer;
|
||||
|
||||
return layer as unknown as Layer<T>;
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -284,11 +285,11 @@ export function createLayer<T extends LayerOptions>(
|
|||
* @param player The player data object, which will have a data object for this layer.
|
||||
*/
|
||||
export function addLayer(
|
||||
layer: GenericLayer,
|
||||
layer: Layer,
|
||||
player: { layers?: Record<string, Record<string, unknown>> }
|
||||
): void {
|
||||
console.info("Adding layer", layer.id);
|
||||
if (layers[layer.id]) {
|
||||
if (layers[layer.id] != null) {
|
||||
console.error(
|
||||
"Attempted to add layer with same ID as existing layer",
|
||||
layer.id,
|
||||
|
@ -297,7 +298,7 @@ export function addLayer(
|
|||
return;
|
||||
}
|
||||
|
||||
setDefault(player, "layers", {});
|
||||
player.layers ??= {};
|
||||
if (player.layers[layer.id] == null) {
|
||||
player.layers[layer.id] = {};
|
||||
}
|
||||
|
@ -310,7 +311,7 @@ export function addLayer(
|
|||
* Convenience method for getting a layer by its ID with correct typing.
|
||||
* @param layerID The ID of the layer to get.
|
||||
*/
|
||||
export function getLayer<T extends GenericLayer>(layerID: string): T {
|
||||
export function getLayer<T extends Layer>(layerID: string): T {
|
||||
return layers[layerID] as T;
|
||||
}
|
||||
|
||||
|
@ -319,11 +320,11 @@ export function getLayer<T extends GenericLayer>(layerID: string): T {
|
|||
* Note that accessing a layer/its properties does NOT require it to be enabled.
|
||||
* @param layer The layer to remove.
|
||||
*/
|
||||
export function removeLayer(layer: GenericLayer): void {
|
||||
export function removeLayer(layer: Layer): void {
|
||||
console.info("Removing layer", layer.id);
|
||||
globalBus.emit("removeLayer", layer);
|
||||
|
||||
layers[layer.id] = undefined;
|
||||
delete layers[layer.id];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -331,7 +332,7 @@ export function removeLayer(layer: GenericLayer): void {
|
|||
* This is useful for layers with dynamic content, to ensure persistent refs are correctly configured.
|
||||
* @param layer Layer to remove and then re-add
|
||||
*/
|
||||
export function reloadLayer(layer: GenericLayer): void {
|
||||
export function reloadLayer(layer: Layer): void {
|
||||
removeLayer(layer);
|
||||
|
||||
// Re-create layer
|
||||
|
@ -343,14 +344,14 @@ export function reloadLayer(layer: GenericLayer): void {
|
|||
* Returns the modal itself, which can be rendered anywhere you need, as well as a function to open the modal.
|
||||
* @param layer The layer to display in the modal.
|
||||
*/
|
||||
export function setupLayerModal(layer: GenericLayer): {
|
||||
export function setupLayerModal(layer: Layer): {
|
||||
openModal: VoidFunction;
|
||||
modal: JSXFunction;
|
||||
modal: Ref<JSX.Element>;
|
||||
} {
|
||||
const showModal = ref(false);
|
||||
return {
|
||||
openModal: () => (showModal.value = true),
|
||||
modal: jsx(() => (
|
||||
modal: computed(() => (
|
||||
<Modal
|
||||
modelValue={showModal.value}
|
||||
onUpdate:modelValue={value => (showModal.value = value)}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import "components/common/modifiers.css";
|
||||
import type { CoercableComponent, OptionsFunc } from "features/feature";
|
||||
import { jsx } from "features/feature";
|
||||
import type { OptionsFunc } from "features/feature";
|
||||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatSmall } from "util/bignum";
|
||||
import type { RequiredKeys, WithRequired } from "util/common";
|
||||
import type { Computable, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable } from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { renderJSX } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { render, Renderable } from "util/vue";
|
||||
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
|
||||
import Formula from "./formulas/formulas";
|
||||
import { FormulaSource, GenericFormula } from "./formulas/types";
|
||||
|
||||
|
@ -30,12 +28,12 @@ export interface Modifier {
|
|||
* Whether or not this modifier should be considered enabled.
|
||||
* Typically for use with modifiers passed into {@link createSequentialModifier}.
|
||||
*/
|
||||
enabled?: ProcessedComputable<boolean>;
|
||||
enabled?: MaybeRef<boolean>;
|
||||
/**
|
||||
* A description of this modifier.
|
||||
* @see {@link createModifierSection}.
|
||||
*/
|
||||
description?: ProcessedComputable<CoercableComponent>;
|
||||
description?: MaybeRef<Renderable>;
|
||||
}
|
||||
|
||||
/** Utility type that represents the output of all modifiers that represent a single operation. */
|
||||
|
@ -47,11 +45,11 @@ export type OperationModifier<T> = WithRequired<
|
|||
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
|
||||
export interface AdditiveModifierOptions {
|
||||
/** The amount to add to the input value. */
|
||||
addend: Computable<DecimalSource>;
|
||||
addend: MaybeRefOrGetter<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: Computable<CoercableComponent>;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: Computable<boolean>;
|
||||
description?: MaybeRefOrGetter<Renderable>;
|
||||
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: MaybeRefOrGetter<boolean>;
|
||||
/** Determines if numbers larger or smaller than 0 should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
@ -69,25 +67,22 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
|
|||
feature
|
||||
);
|
||||
|
||||
const processedAddend = convertComputable(addend);
|
||||
const processedDescription = convertComputable(description);
|
||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||
const processedAddend = processGetter(addend);
|
||||
const processedDescription = processGetter(description);
|
||||
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
|
||||
return {
|
||||
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
|
||||
invert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)),
|
||||
getFormula: (gain: FormulaSource) => Formula.add(gain, processedAddend),
|
||||
enabled: processedEnabled,
|
||||
description:
|
||||
description == null
|
||||
processedDescription == null
|
||||
? undefined
|
||||
: jsx(() => (
|
||||
: computed(() => (
|
||||
<div class="modifier-container">
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
</span>
|
||||
) : null}
|
||||
<span class="modifier-description">
|
||||
{render(processedDescription)}
|
||||
</span>
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
|
@ -112,11 +107,11 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
|
|||
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
|
||||
export interface MultiplicativeModifierOptions {
|
||||
/** The amount to multiply the input value by. */
|
||||
multiplier: Computable<DecimalSource>;
|
||||
multiplier: MaybeRefOrGetter<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: Computable<CoercableComponent> | undefined;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: Computable<boolean> | undefined;
|
||||
description?: MaybeRefOrGetter<Renderable> | undefined;
|
||||
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: MaybeRefOrGetter<boolean> | undefined;
|
||||
/** Determines if numbers larger or smaller than 1 should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
@ -135,25 +130,22 @@ export function createMultiplicativeModifier<
|
|||
feature
|
||||
);
|
||||
|
||||
const processedMultiplier = convertComputable(multiplier);
|
||||
const processedDescription = convertComputable(description);
|
||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||
const processedMultiplier = processGetter(multiplier);
|
||||
const processedDescription = processGetter(description);
|
||||
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
|
||||
return {
|
||||
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
|
||||
invert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)),
|
||||
getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier),
|
||||
enabled: processedEnabled,
|
||||
description:
|
||||
description == null
|
||||
processedDescription == null
|
||||
? undefined
|
||||
: jsx(() => (
|
||||
: computed(() => (
|
||||
<div class="modifier-container">
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
</span>
|
||||
) : null}
|
||||
<span class="modifier-description">
|
||||
{render(processedDescription)}
|
||||
</span>
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
|
@ -177,11 +169,11 @@ export function createMultiplicativeModifier<
|
|||
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */
|
||||
export interface ExponentialModifierOptions {
|
||||
/** The amount to raise the input value to the power of. */
|
||||
exponent: Computable<DecimalSource>;
|
||||
exponent: MaybeRefOrGetter<DecimalSource>;
|
||||
/** Description of what this modifier is doing. */
|
||||
description?: Computable<CoercableComponent> | undefined;
|
||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: Computable<boolean> | undefined;
|
||||
description?: MaybeRefOrGetter<Renderable> | undefined;
|
||||
/** A MaybeRefOrGetter that will be processed and passed directly into the returned modifier. */
|
||||
enabled?: MaybeRefOrGetter<boolean> | undefined;
|
||||
/** Add 1 before calculating, then remove it afterwards. This prevents low numbers from becoming lower. */
|
||||
supportLowNumbers?: boolean;
|
||||
/** Determines if numbers larger or smaller than 1 should be displayed as red. */
|
||||
|
@ -200,9 +192,9 @@ export function createExponentialModifier<
|
|||
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
||||
optionsFunc.call(feature, feature);
|
||||
|
||||
const processedExponent = convertComputable(exponent);
|
||||
const processedDescription = convertComputable(description);
|
||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||
const processedExponent = processGetter(exponent);
|
||||
const processedDescription = processGetter(description);
|
||||
const processedEnabled = enabled == null ? undefined : processGetter(enabled);
|
||||
return {
|
||||
apply: (gain: DecimalSource) => {
|
||||
let result = gain;
|
||||
|
@ -232,17 +224,14 @@ export function createExponentialModifier<
|
|||
: Formula.pow(gain, processedExponent),
|
||||
enabled: processedEnabled,
|
||||
description:
|
||||
description == null
|
||||
processedDescription == null
|
||||
? undefined
|
||||
: jsx(() => (
|
||||
: computed(() => (
|
||||
<div class="modifier-container">
|
||||
{unref(processedDescription) != null ? (
|
||||
<span class="modifier-description">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{renderJSX(unref(processedDescription)!)}
|
||||
{supportLowNumbers ? " (+1 effective)" : null}
|
||||
</span>
|
||||
) : null}
|
||||
<span class="modifier-description">
|
||||
{render(processedDescription)}
|
||||
{supportLowNumbers ? " (+1 effective)" : null}
|
||||
</span>
|
||||
<span
|
||||
class="modifier-amount"
|
||||
style={
|
||||
|
@ -304,16 +293,14 @@ export function createSequentialModifier<
|
|||
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
|
||||
: undefined,
|
||||
description: modifiers.some(m => m.description != null)
|
||||
? jsx(() => (
|
||||
<>
|
||||
{(
|
||||
modifiers
|
||||
.filter(m => unref(m.enabled) !== false)
|
||||
.map(m => unref(m.description))
|
||||
.filter(d => d) as CoercableComponent[]
|
||||
).map(renderJSX)}
|
||||
</>
|
||||
))
|
||||
? computed(() =>
|
||||
(
|
||||
modifiers
|
||||
.filter(m => unref(m.enabled) !== false)
|
||||
.map(m => unref(m.description))
|
||||
.filter(d => d) as MaybeRef<Renderable>[]
|
||||
).map(m => render(m))
|
||||
)
|
||||
: undefined
|
||||
};
|
||||
}) as S;
|
||||
|
@ -332,7 +319,7 @@ export interface ModifierSectionOptions {
|
|||
/** The unit of the value being modified, if any. */
|
||||
unit?: string;
|
||||
/** The label to use for the base value. Defaults to "Base". */
|
||||
baseText?: CoercableComponent;
|
||||
baseText?: MaybeRefOrGetter<Renderable>;
|
||||
/** Determines if numbers larger or smaller than the base should be displayed as red. */
|
||||
smallerIsBetter?: boolean;
|
||||
}
|
||||
|
@ -352,6 +339,7 @@ export function createModifierSection({
|
|||
smallerIsBetter
|
||||
}: ModifierSectionOptions) {
|
||||
const total = modifier.apply(base ?? 1);
|
||||
const processedBaseText = processGetter(baseText);
|
||||
return (
|
||||
<div style={{ "--unit": settings.alignUnits && unit != null ? "'" + unit + "'" : "" }}>
|
||||
<h3>
|
||||
|
@ -360,13 +348,13 @@ export function createModifierSection({
|
|||
</h3>
|
||||
<br />
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">{renderJSX(baseText ?? "Base")}</span>
|
||||
<span class="modifier-description">{render(processedBaseText ?? "Base")}</span>
|
||||
<span class="modifier-amount">
|
||||
{formatSmall(base ?? 1)}
|
||||
{unit}
|
||||
</span>
|
||||
</div>
|
||||
{renderJSX(unref(modifier.description))}
|
||||
{render(modifier.description)}
|
||||
<hr />
|
||||
<div class="modifier-container">
|
||||
<span class="modifier-description">Total</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { globalBus } from "game/events";
|
||||
import { convertComputable } from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { trackHover, VueFeature } from "util/vue";
|
||||
import { nextTick, Ref } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
|
@ -37,7 +37,7 @@ export function createDismissableNotify(
|
|||
element: VueFeature,
|
||||
shouldNotify: Ref<boolean> | (() => boolean)
|
||||
): Ref<boolean> {
|
||||
const processedShouldNotify = convertComputable(shouldNotify) as Ref<boolean>;
|
||||
const processedShouldNotify = processGetter(shouldNotify) as Ref<boolean>;
|
||||
const notifying = ref(false);
|
||||
nextTick(() => {
|
||||
notifying.value = processedShouldNotify.value;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { globalBus } from "game/events";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import type { Layer } from "game/layers";
|
||||
import { addingLayers, persistentRefs } from "game/layers";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import type { Ref, WritableComputedRef } from "vue";
|
||||
import { computed, isReactive, isRef, ref } from "vue";
|
||||
import Formula from "./formulas/formulas";
|
||||
import player from "./player";
|
||||
import state from "./state";
|
||||
import Formula from "./formulas/formulas";
|
||||
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
|
@ -251,7 +251,7 @@ export function deletePersistent(persistent: Persistent) {
|
|||
persistent[Deleted] = true;
|
||||
}
|
||||
|
||||
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
|
||||
globalBus.on("addLayer", (layer: Layer, saveData: Record<string, unknown>) => {
|
||||
const features: { type: typeof Symbol }[] = [];
|
||||
const handleObject = (obj: Record<string, unknown>, path: string[] = []): boolean => {
|
||||
let foundPersistent = false;
|
||||
|
|
|
@ -1,26 +1,12 @@
|
|||
import {
|
||||
CoercableComponent,
|
||||
isVisible,
|
||||
jsx,
|
||||
OptionsFunc,
|
||||
Replace,
|
||||
setDefault,
|
||||
Visibility
|
||||
} from "features/feature";
|
||||
import { isVisible, OptionsFunc, Replace, Visibility } from "features/feature";
|
||||
import { displayResource, Resource } from "features/resources/resource";
|
||||
import Decimal, { DecimalSource } from "lib/break_eternity";
|
||||
import {
|
||||
Computable,
|
||||
convertComputable,
|
||||
processComputable,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { joinJSX, renderJSX } from "util/vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
import { joinJSX, render, Renderable } from "util/vue";
|
||||
import { computed, MaybeRef, MaybeRefOrGetter, unref } from "vue";
|
||||
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
||||
import type { GenericFormula } from "./formulas/types";
|
||||
import type { GenericFormula, InvertibleIntegralFormula } from "./formulas/types";
|
||||
import { DefaultValue, Persistent } from "./persistence";
|
||||
|
||||
/**
|
||||
|
@ -31,27 +17,27 @@ export interface Requirement {
|
|||
/**
|
||||
* The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}.
|
||||
*/
|
||||
partialDisplay?: (amount?: DecimalSource) => JSX.Element;
|
||||
partialDisplay?: (amount?: DecimalSource) => Renderable;
|
||||
/**
|
||||
* The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}.
|
||||
*/
|
||||
display?: (amount?: DecimalSource) => JSX.Element;
|
||||
display?: (amount?: DecimalSource) => Renderable;
|
||||
/**
|
||||
* Whether or not this requirement should be displayed in Vue Features. {@link displayRequirements} will respect this property.
|
||||
*/
|
||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
||||
visibility: MaybeRef<Visibility | boolean>;
|
||||
/**
|
||||
* Whether or not this requirement has been met.
|
||||
*/
|
||||
requirementMet: ProcessedComputable<DecimalSource | boolean>;
|
||||
requirementMet: MaybeRef<DecimalSource | boolean>;
|
||||
/**
|
||||
* Whether or not this requirement will need to affect the game state when whatever is using this requirement gets triggered.
|
||||
*/
|
||||
requiresPay: ProcessedComputable<boolean>;
|
||||
requiresPay: MaybeRef<boolean>;
|
||||
/**
|
||||
* Whether or not this requirement can have multiple levels of requirements that can be met at once. Requirement is assumed to not have multiple levels if this property not present.
|
||||
*/
|
||||
canMaximize?: ProcessedComputable<boolean>;
|
||||
canMaximize?: MaybeRef<boolean>;
|
||||
/**
|
||||
* Perform any effects to the game state that should happen when the requirement gets triggered.
|
||||
* @param amount The amount of levels of requirements to pay for.
|
||||
|
@ -73,28 +59,28 @@ export interface CostRequirementOptions {
|
|||
/**
|
||||
* The amount of {@link resource} that must be met for this requirement. You can pass a formula, in which case maximizing will work out of the box (assuming its invertible and, for more accurate calculations, its integral is invertible). If you don't pass a formula then you can still support maximizing by passing a custom {@link pay} function.
|
||||
*/
|
||||
cost: Computable<DecimalSource> | GenericFormula;
|
||||
cost: MaybeRefOrGetter<DecimalSource> | GenericFormula;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.visibility}.
|
||||
*/
|
||||
visibility?: Computable<Visibility.Visible | Visibility.None | boolean>;
|
||||
visibility?: MaybeRefOrGetter<Visibility.Visible | Visibility.None | boolean>;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.requiresPay}. If not set to false, the default {@link pay} function will remove {@link cost} from {@link resource}.
|
||||
*/
|
||||
requiresPay?: Computable<boolean>;
|
||||
requiresPay?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
|
||||
* @see {Formula}
|
||||
*/
|
||||
cumulativeCost?: Computable<boolean>;
|
||||
cumulativeCost?: MaybeRefOrGetter<boolean>;
|
||||
/**
|
||||
* Upper limit on levels that can be performed at once. Defaults to 1.
|
||||
*/
|
||||
maxBulkAmount?: Computable<DecimalSource>;
|
||||
maxBulkAmount?: MaybeRefOrGetter<DecimalSource>;
|
||||
/**
|
||||
* When calculating requirement for multiple levels, how many should be directly summed for increase accuracy. High numbers can cause lag. Defaults to 10 if cumulative cost, 0 otherwise.
|
||||
*/
|
||||
directSum?: Computable<number>;
|
||||
directSum?: MaybeRefOrGetter<number>;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.pay}. May be required for maximizing support.
|
||||
* @see {@link cost} for restrictions on maximizing support.
|
||||
|
@ -105,11 +91,11 @@ export interface CostRequirementOptions {
|
|||
export type CostRequirement = Replace<
|
||||
Requirement & CostRequirementOptions,
|
||||
{
|
||||
cost: ProcessedComputable<DecimalSource> | GenericFormula;
|
||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
||||
requiresPay: ProcessedComputable<boolean>;
|
||||
cumulativeCost: ProcessedComputable<boolean>;
|
||||
canMaximize: ProcessedComputable<boolean>;
|
||||
cost: MaybeRef<DecimalSource> | GenericFormula;
|
||||
visibility: MaybeRef<Visibility.Visible | Visibility.None | boolean>;
|
||||
requiresPay: MaybeRef<boolean>;
|
||||
cumulativeCost: MaybeRef<boolean>;
|
||||
canMaximize: MaybeRef<boolean>;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -119,116 +105,123 @@ export type CostRequirement = Replace<
|
|||
*/
|
||||
export function createCostRequirement<T extends CostRequirementOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): CostRequirement {
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const req = optionsFunc.call(feature, feature) as T & Partial<Requirement>;
|
||||
const options = optionsFunc.call(feature, feature);
|
||||
const {
|
||||
visibility,
|
||||
cost,
|
||||
resource,
|
||||
requiresPay,
|
||||
cumulativeCost,
|
||||
maxBulkAmount,
|
||||
directSum,
|
||||
pay
|
||||
} = options;
|
||||
|
||||
req.partialDisplay = amount => (
|
||||
<span
|
||||
style={
|
||||
unref(req.requirementMet as ProcessedComputable<boolean>)
|
||||
? ""
|
||||
: "color: var(--danger)"
|
||||
const requirement = {
|
||||
resource,
|
||||
visibility: processGetter(visibility) ?? Visibility.Visible,
|
||||
cost: processGetter(cost),
|
||||
requiresPay: processGetter(requiresPay) ?? true,
|
||||
cumulativeCost: processGetter(cumulativeCost) ?? true,
|
||||
maxBulkAmount: processGetter(maxBulkAmount) ?? 1,
|
||||
directSum: processGetter(directSum),
|
||||
partialDisplay: (amount?: DecimalSource) => (
|
||||
<span
|
||||
style={
|
||||
Decimal.gt(unref(requirement.requirementMet), 0)
|
||||
? ""
|
||||
: "color: var(--danger)"
|
||||
}
|
||||
>
|
||||
{displayResource(
|
||||
resource,
|
||||
requirement.cost instanceof Formula
|
||||
? calculateCost(
|
||||
requirement.cost as InvertibleIntegralFormula,
|
||||
amount ?? 1,
|
||||
unref(requirement.cumulativeCost),
|
||||
unref(requirement.directSum)
|
||||
)
|
||||
: unref(requirement.cost as MaybeRef<DecimalSource>)
|
||||
)}{" "}
|
||||
{resource.displayName}
|
||||
</span>
|
||||
),
|
||||
display: (amount?: DecimalSource) => (
|
||||
<div>
|
||||
{unref(requirement.requiresPay as MaybeRef<boolean>) ? "Costs: " : "Requires: "}
|
||||
{displayResource(
|
||||
resource,
|
||||
requirement.cost instanceof Formula
|
||||
? calculateCost(
|
||||
requirement.cost as InvertibleIntegralFormula,
|
||||
amount ?? 1,
|
||||
unref(requirement.cumulativeCost),
|
||||
unref(requirement.directSum)
|
||||
)
|
||||
: unref(requirement.cost as MaybeRef<DecimalSource>)
|
||||
)}{" "}
|
||||
{resource.displayName}
|
||||
</div>
|
||||
),
|
||||
canMaximize: computed(() => {
|
||||
if (!(requirement.cost instanceof Formula)) {
|
||||
return false;
|
||||
}
|
||||
const maxBulkAmount = unref(requirement.maxBulkAmount);
|
||||
if (Decimal.lte(maxBulkAmount, 1)) {
|
||||
return false;
|
||||
}
|
||||
const cumulativeCost = unref(requirement.cumulativeCost);
|
||||
const directSum = unref(requirement.directSum) ?? (cumulativeCost ? 10 : 0);
|
||||
if (Decimal.lte(maxBulkAmount, directSum)) {
|
||||
return true;
|
||||
}
|
||||
if (!requirement.cost.isInvertible()) {
|
||||
return false;
|
||||
}
|
||||
if (cumulativeCost === true && !requirement.cost.isIntegrable()) {
|
||||
return false;
|
||||
}
|
||||
>
|
||||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost) as boolean,
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
</span>
|
||||
);
|
||||
req.display = amount => (
|
||||
<div>
|
||||
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
|
||||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost) as boolean,
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
</div>
|
||||
);
|
||||
|
||||
processComputable(req as T, "visibility");
|
||||
setDefault(req, "visibility", Visibility.Visible);
|
||||
processComputable(req as T, "cost");
|
||||
processComputable(req as T, "requiresPay");
|
||||
setDefault(req, "requiresPay", true);
|
||||
processComputable(req as T, "cumulativeCost");
|
||||
setDefault(req, "cumulativeCost", true);
|
||||
processComputable(req as T, "maxBulkAmount");
|
||||
setDefault(req, "maxBulkAmount", 1);
|
||||
processComputable(req as T, "directSum");
|
||||
setDefault(req, "pay", function (amount?: DecimalSource) {
|
||||
const cost =
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost as ProcessedComputable<boolean>),
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
||||
});
|
||||
|
||||
req.canMaximize = computed(() => {
|
||||
if (!(req.cost instanceof Formula)) {
|
||||
return false;
|
||||
}
|
||||
const maxBulkAmount = unref(req.maxBulkAmount as ProcessedComputable<DecimalSource>);
|
||||
if (Decimal.lte(maxBulkAmount, 1)) {
|
||||
return false;
|
||||
}
|
||||
const cumulativeCost = unref(req.cumulativeCost as ProcessedComputable<boolean>);
|
||||
const directSum =
|
||||
unref(req.directSum as ProcessedComputable<number>) ?? (cumulativeCost ? 10 : 0);
|
||||
if (Decimal.lte(maxBulkAmount, directSum)) {
|
||||
return true;
|
||||
}
|
||||
if (!req.cost.isInvertible()) {
|
||||
return false;
|
||||
}
|
||||
if (cumulativeCost === true && !req.cost.isIntegrable()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}),
|
||||
requirementMet:
|
||||
cost instanceof Formula
|
||||
? calculateMaxAffordable(
|
||||
cost,
|
||||
resource,
|
||||
cumulativeCost ?? true,
|
||||
directSum,
|
||||
maxBulkAmount
|
||||
)
|
||||
: computed(
|
||||
(): DecimalSource =>
|
||||
Decimal.gte(
|
||||
resource.value,
|
||||
unref(requirement.cost as MaybeRef<DecimalSource>)
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
),
|
||||
pay:
|
||||
pay ??
|
||||
function (amount?: DecimalSource) {
|
||||
const cost =
|
||||
requirement.cost instanceof Formula
|
||||
? calculateCost(
|
||||
requirement.cost,
|
||||
amount ?? 1,
|
||||
unref(requirement.cumulativeCost),
|
||||
unref(requirement.directSum)
|
||||
)
|
||||
: unref(requirement.cost as MaybeRef<DecimalSource>);
|
||||
resource.value = Decimal.sub(resource.value, cost).max(0);
|
||||
}
|
||||
} satisfies CostRequirement;
|
||||
|
||||
if (req.cost instanceof Formula) {
|
||||
req.requirementMet = calculateMaxAffordable(
|
||||
req.cost,
|
||||
req.resource,
|
||||
req.cumulativeCost ?? true,
|
||||
req.directSum,
|
||||
req.maxBulkAmount
|
||||
);
|
||||
} else {
|
||||
req.requirementMet = computed(() =>
|
||||
Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
}
|
||||
|
||||
return req as CostRequirement;
|
||||
return requirement;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -236,11 +229,11 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
|||
* Utility function for creating a requirement that a specified vue feature is visible
|
||||
* @param feature The feature to check the visibility of
|
||||
*/
|
||||
export function createVisibilityRequirement(feature: {
|
||||
visibility: ProcessedComputable<Visibility | boolean>;
|
||||
}): Requirement {
|
||||
export function createVisibilityRequirement(
|
||||
visibility: MaybeRef<Visibility | boolean>
|
||||
): Requirement {
|
||||
return createLazyProxy(() => ({
|
||||
requirementMet: computed(() => isVisible(feature.visibility)),
|
||||
requirementMet: computed(() => isVisible(visibility)),
|
||||
visibility: Visibility.None,
|
||||
requiresPay: false
|
||||
}));
|
||||
|
@ -252,16 +245,20 @@ export function createVisibilityRequirement(feature: {
|
|||
* @param display How to display this requirement to the user
|
||||
*/
|
||||
export function createBooleanRequirement(
|
||||
requirement: Computable<boolean>,
|
||||
display?: CoercableComponent
|
||||
requirement: MaybeRefOrGetter<boolean>,
|
||||
display?: MaybeRefOrGetter<Renderable>
|
||||
): Requirement {
|
||||
return createLazyProxy(() => ({
|
||||
requirementMet: convertComputable(requirement),
|
||||
partialDisplay: display == null ? undefined : jsx(() => renderJSX(display)),
|
||||
display: display == null ? undefined : jsx(() => <>Req: {renderJSX(display)}</>),
|
||||
visibility: display == null ? Visibility.None : Visibility.Visible,
|
||||
requiresPay: false
|
||||
}));
|
||||
return createLazyProxy(() => {
|
||||
const processedDisplay = processGetter(display);
|
||||
return {
|
||||
requirementMet: processGetter(requirement),
|
||||
partialDisplay: processedDisplay == null ? undefined : () => render(processedDisplay),
|
||||
display:
|
||||
processedDisplay == null ? undefined : () => <>Req: {render(processedDisplay)}</>,
|
||||
visibility: processedDisplay == null ? Visibility.None : Visibility.Visible,
|
||||
requiresPay: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -300,7 +297,7 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
|||
*/
|
||||
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||
if (Array.isArray(requirements)) {
|
||||
requirements = requirements.filter(r => isVisible(r.visibility));
|
||||
requirements = requirements.filter(r => isVisible(r.visibility ?? true));
|
||||
if (requirements.length === 1) {
|
||||
requirements = requirements[0];
|
||||
}
|
||||
|
@ -356,9 +353,9 @@ export function payByDivision(this: CostRequirement, amount?: DecimalSource) {
|
|||
? calculateCost(
|
||||
this.cost,
|
||||
amount ?? 1,
|
||||
unref(this.cumulativeCost as ProcessedComputable<boolean> | undefined) ?? true
|
||||
unref(this.cumulativeCost as MaybeRef<boolean> | undefined) ?? true
|
||||
)
|
||||
: unref(this.cost as ProcessedComputable<DecimalSource>);
|
||||
: unref(this.cost as MaybeRef<DecimalSource>);
|
||||
this.resource.value = Decimal.div(this.resource.value, cost);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import { Themes } from "data/themes";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import LZString from "lz-string";
|
||||
import { processGetter } from "util/computed";
|
||||
import { decodeSave, hardReset } from "util/save";
|
||||
import { reactive, watch } from "vue";
|
||||
import { Renderable } from "util/vue";
|
||||
import { MaybeRef, MaybeRefOrGetter, reactive, watch } from "vue";
|
||||
|
||||
/** The player's settings object. */
|
||||
export interface Settings {
|
||||
|
@ -100,22 +101,22 @@ export function loadSettings(): void {
|
|||
}
|
||||
|
||||
/** A list of fields to append to the settings modal. */
|
||||
export const settingFields: CoercableComponent[] = reactive([]);
|
||||
export const settingFields: MaybeRef<Renderable>[] = reactive([]);
|
||||
/** Register a field to be displayed in the settings modal. */
|
||||
export function registerSettingField(component: CoercableComponent) {
|
||||
settingFields.push(component);
|
||||
export function registerSettingField(component: MaybeRefOrGetter<Renderable>) {
|
||||
settingFields.push(processGetter(component));
|
||||
}
|
||||
|
||||
/** A list of components to show in the info modal. */
|
||||
export const infoComponents: CoercableComponent[] = reactive([]);
|
||||
export const infoComponents: MaybeRef<Renderable>[] = reactive([]);
|
||||
/** Register a component to be displayed in the info modal. */
|
||||
export function registerInfoComponent(component: CoercableComponent) {
|
||||
infoComponents.push(component);
|
||||
export function registerInfoComponent(component: MaybeRefOrGetter<Renderable>) {
|
||||
infoComponents.push(processGetter(component));
|
||||
}
|
||||
|
||||
/** A list of components to add to the root of the page. */
|
||||
export const gameComponents: CoercableComponent[] = reactive([]);
|
||||
export const gameComponents: MaybeRef<Renderable>[] = reactive([]);
|
||||
/** Register a component to be displayed at the root of the page. */
|
||||
export function registerGameComponent(component: CoercableComponent) {
|
||||
gameComponents.push(component);
|
||||
export function registerGameComponent(component: MaybeRefOrGetter<Renderable>) {
|
||||
gameComponents.push(processGetter(component));
|
||||
}
|
||||
|
|
29
src/mixins/bonusDecorator.ts
Normal file
29
src/mixins/bonusDecorator.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import { processGetter } from "util/computed";
|
||||
import { MaybeRefOrGetter, Ref, computed, unref } from "vue";
|
||||
|
||||
/** Allows the addition of "bonus levels" to a feature, with an accompanying "total amount". */
|
||||
export function bonusAmountMixin(
|
||||
feature: { amount: Ref<DecimalSource> },
|
||||
bonusAmount: MaybeRefOrGetter<DecimalSource>
|
||||
) {
|
||||
const processedBonusAmount = processGetter(bonusAmount);
|
||||
return {
|
||||
bonusAmount,
|
||||
totalAmount: computed(() => Decimal.add(unref(feature.amount), unref(processedBonusAmount)))
|
||||
};
|
||||
}
|
||||
|
||||
/** Allows the addition of "bonus completions" to a feature, with an accompanying "total completions". */
|
||||
export function bonusCompletionsMixin(
|
||||
feature: { completions: Ref<DecimalSource> },
|
||||
bonusCompletions: MaybeRefOrGetter<DecimalSource>
|
||||
) {
|
||||
const processedBonusCompletions = processGetter(bonusCompletions);
|
||||
return {
|
||||
bonusCompletions,
|
||||
totalCompletions: computed(() =>
|
||||
Decimal.add(unref(feature.completions), unref(processedBonusCompletions))
|
||||
)
|
||||
};
|
||||
}
|
|
@ -1,57 +1,16 @@
|
|||
import type { JSXFunction } from "features/feature";
|
||||
import { isFunction } from "util/common";
|
||||
import type { Ref } from "vue";
|
||||
import type { ComputedRef, MaybeRef, Ref, UnwrapRef } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
export const DoNotCache = Symbol("DoNotCache");
|
||||
export type ProcessedRefOrGetter<T> = T extends () => infer S
|
||||
? Ref<S>
|
||||
: T extends undefined
|
||||
? undefined
|
||||
: MaybeRef<NonNullable<UnwrapRef<T>>>;
|
||||
|
||||
export type Computable<T> = T | Ref<T> | (() => T);
|
||||
export type ProcessedComputable<T> = T | Ref<T>;
|
||||
export type GetComputableType<T> = T extends { [DoNotCache]: true }
|
||||
? T
|
||||
: T extends () => infer S
|
||||
? Ref<S>
|
||||
: undefined extends T
|
||||
? undefined
|
||||
: T;
|
||||
export type GetComputableTypeWithDefault<T, S> = undefined extends T
|
||||
? S
|
||||
: GetComputableType<NonNullable<T>>;
|
||||
export type UnwrapComputableType<T> = T extends Ref<infer S> ? S : T extends () => infer S ? S : T;
|
||||
|
||||
export type ComputableKeysOf<T> = Pick<
|
||||
T,
|
||||
{
|
||||
[K in keyof T]: T[K] extends Computable<unknown> ? K : never;
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
// TODO fix the typing of this function, such that casting isn't necessary and can be used to
|
||||
// detect if a createX function is validly written
|
||||
export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
|
||||
obj: T,
|
||||
key: S
|
||||
): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } {
|
||||
const computable = obj[key];
|
||||
if (
|
||||
isFunction(computable) &&
|
||||
computable.length === 0 &&
|
||||
!(computable as unknown as JSXFunction)[DoNotCache]
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
obj[key] = computed(computable.bind(obj));
|
||||
} else if (isFunction(computable)) {
|
||||
obj[key] = computable.bind(obj) as unknown as T[S];
|
||||
(obj[key] as unknown as JSXFunction)[DoNotCache] = true;
|
||||
export function processGetter<T>(obj: T): T extends () => infer S ? ComputedRef<S> : T {
|
||||
if (isFunction(obj)) {
|
||||
return computed(obj) as ReturnType<typeof processGetter<T>>;
|
||||
}
|
||||
}
|
||||
|
||||
export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> {
|
||||
if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
obj = computed(obj);
|
||||
}
|
||||
return obj as ProcessedComputable<T>;
|
||||
return obj as ReturnType<typeof processGetter<T>>;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ function syncSaves(
|
|||
const saves = (
|
||||
Object.keys(list)
|
||||
.map(slot => {
|
||||
const { label, content } = list[slot as unknown as number];
|
||||
const { label, content } = list[parseInt(slot)];
|
||||
try {
|
||||
return {
|
||||
slot: parseInt(slot),
|
||||
|
|
|
@ -33,9 +33,9 @@ export type Proxied<T> =
|
|||
// 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, S extends T>(
|
||||
objectFunc: (this: S, baseObject: S) => T & S,
|
||||
objectFunc: (this: S, baseObject: S) => T,
|
||||
baseObject: S = {} as S
|
||||
): T {
|
||||
): T & S {
|
||||
const obj: S & Partial<T> = baseObject;
|
||||
let calculated = false;
|
||||
let calculating = false;
|
||||
|
|
|
@ -106,7 +106,7 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
|||
|
||||
for (const layer in layers) {
|
||||
const l = layers[layer];
|
||||
if (l) {
|
||||
if (l != null) {
|
||||
removeLayer(l);
|
||||
}
|
||||
}
|
||||
|
|
258
src/util/vue.tsx
258
src/util/vue.tsx
|
@ -4,122 +4,119 @@
|
|||
// only apply to SFCs
|
||||
import Col from "components/layout/Column.vue";
|
||||
import Row from "components/layout/Row.vue";
|
||||
import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature";
|
||||
import {
|
||||
Component as ComponentKey,
|
||||
GatherProps,
|
||||
Visibility,
|
||||
isVisible,
|
||||
jsx
|
||||
} from "features/feature";
|
||||
import type { ProcessedComputable } from "util/computed";
|
||||
import { DoNotCache } from "util/computed";
|
||||
import type { Component, DefineComponent, Ref, ShallowRef, UnwrapRef } from "vue";
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
isRef,
|
||||
onUnmounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
unref,
|
||||
watchEffect
|
||||
} from "vue";
|
||||
import { getUniqueID, Visibility } from "features/feature";
|
||||
import VueFeatureComponent from "features/VueFeature.vue";
|
||||
import { processGetter } from "util/computed";
|
||||
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
|
||||
import { isRef, onUnmounted, ref, unref } from "vue";
|
||||
import { JSX } from "vue/jsx-runtime";
|
||||
import { camelToKebab } from "./common";
|
||||
|
||||
export function coerceComponent(
|
||||
component: CoercableComponent,
|
||||
defaultWrapper = "span"
|
||||
): DefineComponent {
|
||||
if (typeof component === "function") {
|
||||
return defineComponent({ render: component });
|
||||
}
|
||||
if (typeof component === "string") {
|
||||
if (component.length > 0) {
|
||||
component = component.trim();
|
||||
if (component.charAt(0) !== "<") {
|
||||
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
|
||||
}
|
||||
export const VueFeature = Symbol("VueFeature");
|
||||
|
||||
return defineComponent({ template: component });
|
||||
}
|
||||
return defineComponent({ render: () => ({}) });
|
||||
}
|
||||
return component;
|
||||
export type Renderable = JSX.Element | string;
|
||||
|
||||
export interface VueFeatureOptions {
|
||||
/** Whether this feature should be visible. */
|
||||
visibility?: MaybeRefOrGetter<Visibility | boolean>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: MaybeRefOrGetter<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: MaybeRefOrGetter<CSSProperties>;
|
||||
}
|
||||
|
||||
export interface VueFeature {
|
||||
[ComponentKey]: GenericComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||
id: string;
|
||||
/** Whether this feature should be visible. */
|
||||
visibility?: MaybeRef<Visibility | boolean>;
|
||||
/** Dictionary of CSS classes to apply to this feature. */
|
||||
classes?: MaybeRef<Record<string, boolean>>;
|
||||
/** CSS to apply to this feature. */
|
||||
style?: MaybeRef<CSSProperties>;
|
||||
/** The components to render inside the vue feature */
|
||||
components: MaybeRef<Renderable>[];
|
||||
/** The components to render wrapped around the vue feature */
|
||||
wrappers: ((el: () => Renderable) => Renderable)[];
|
||||
/** Used to identify Vue Features */
|
||||
[VueFeature]: true;
|
||||
}
|
||||
|
||||
export function render(object: VueFeature | CoercableComponent): JSX.Element | DefineComponent {
|
||||
if (isCoercableComponent(object)) {
|
||||
if (typeof object === "function") {
|
||||
return (object as JSXFunction)();
|
||||
}
|
||||
return coerceComponent(object);
|
||||
export function vueFeatureMixin(
|
||||
featureName: string,
|
||||
options: VueFeatureOptions,
|
||||
component?: MaybeRefOrGetter<Renderable>
|
||||
) {
|
||||
return {
|
||||
id: getUniqueID(featureName),
|
||||
visibility: processGetter(options.visibility),
|
||||
classes: processGetter(options.classes),
|
||||
style: processGetter(options.style),
|
||||
components: component == null ? [] : [processGetter(component)],
|
||||
wrappers: [] as ((el: () => Renderable) => Renderable)[],
|
||||
[VueFeature]: true
|
||||
} satisfies VueFeature;
|
||||
}
|
||||
|
||||
export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element;
|
||||
export function render<T extends Renderable>(
|
||||
object: MaybeRef<Renderable>,
|
||||
wrapper?: (el: Renderable) => T
|
||||
): T;
|
||||
export function render(
|
||||
object: VueFeature | MaybeRef<Renderable>,
|
||||
wrapper?: (el: Renderable) => Renderable
|
||||
): Renderable;
|
||||
export function render(
|
||||
object: VueFeature | MaybeRef<Renderable>,
|
||||
wrapper?: (el: Renderable) => Renderable
|
||||
) {
|
||||
if (typeof object === "object" && VueFeature in object) {
|
||||
const { id, visibility, style, classes, components, wrappers } = object;
|
||||
return (
|
||||
<VueFeatureComponent
|
||||
id={id}
|
||||
visibility={visibility}
|
||||
style={style}
|
||||
classes={classes}
|
||||
components={components}
|
||||
wrappers={wrappers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Component = object[ComponentKey];
|
||||
return <Component {...object[GatherProps]()} />;
|
||||
|
||||
object = unref(object);
|
||||
return wrapper?.(object) ?? object;
|
||||
}
|
||||
|
||||
export function renderRow(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
|
||||
return <Row>{objects.map(render)}</Row>;
|
||||
export function renderRow(...objects: (VueFeature | MaybeRef<Renderable>)[]): JSX.Element {
|
||||
return <Row>{objects.map(obj => render(obj))}</Row>;
|
||||
}
|
||||
|
||||
export function renderCol(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
|
||||
return <Col>{objects.map(render)}</Col>;
|
||||
export function renderCol(...objects: (VueFeature | MaybeRef<Renderable>)[]): JSX.Element {
|
||||
return <Col>{objects.map(obj => render(obj))}</Col>;
|
||||
}
|
||||
|
||||
export function renderJSX(object: VueFeature | CoercableComponent): JSX.Element {
|
||||
if (isCoercableComponent(object)) {
|
||||
if (typeof object === "function") {
|
||||
return (object as JSXFunction)();
|
||||
}
|
||||
if (typeof object === "string") {
|
||||
return <>{object}</>;
|
||||
}
|
||||
// TODO why is object typed as never?
|
||||
const Comp = object as DefineComponent;
|
||||
return <Comp />;
|
||||
}
|
||||
const Component = object[ComponentKey];
|
||||
return <Component {...object[GatherProps]()} />;
|
||||
export function joinJSX(
|
||||
objects: (VueFeature | MaybeRef<Renderable>)[],
|
||||
joiner: JSX.Element
|
||||
): JSX.Element {
|
||||
return objects.reduce<JSX.Element>(
|
||||
(acc, curr) => (
|
||||
<>
|
||||
{acc}
|
||||
{joiner}
|
||||
{render(curr)}
|
||||
</>
|
||||
),
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderRowJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
|
||||
return <Row>{objects.map(renderJSX)}</Row>;
|
||||
}
|
||||
|
||||
export function renderColJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
|
||||
return <Col>{objects.map(renderJSX)}</Col>;
|
||||
}
|
||||
|
||||
export function joinJSX(objects: JSX.Element[], joiner: JSX.Element): JSX.Element {
|
||||
return objects.reduce((acc, curr) => (
|
||||
<>
|
||||
{acc}
|
||||
{joiner}
|
||||
{curr}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
export function isCoercableComponent(component: unknown): component is CoercableComponent {
|
||||
if (typeof component === "string") {
|
||||
return true;
|
||||
} else if (typeof component === "object") {
|
||||
if (component == null) {
|
||||
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;
|
||||
export function isJSXElement(element: unknown): element is JSX.Element {
|
||||
return (
|
||||
element != null && typeof element === "object" && "type" in element && "children" in element
|
||||
);
|
||||
}
|
||||
|
||||
export function setupHoldToClick(
|
||||
|
@ -158,61 +155,6 @@ export function setupHoldToClick(
|
|||
return { start, stop, handleHolding };
|
||||
}
|
||||
|
||||
export function getFirstFeature<
|
||||
T extends VueFeature & { visibility: ProcessedComputable<Visibility | boolean> }
|
||||
>(
|
||||
features: T[],
|
||||
filter: (feature: T) => boolean
|
||||
): {
|
||||
firstFeature: Ref<T | undefined>;
|
||||
collapsedContent: JSXFunction;
|
||||
hasCollapsedContent: Ref<boolean>;
|
||||
} {
|
||||
const filteredFeatures = computed(() =>
|
||||
features.filter(feature => isVisible(feature.visibility) && filter(feature))
|
||||
);
|
||||
return {
|
||||
firstFeature: computed(() => filteredFeatures.value[0]),
|
||||
collapsedContent: jsx(() => renderCol(...filteredFeatures.value.slice(1))),
|
||||
hasCollapsedContent: computed(() => filteredFeatures.value.length > 1)
|
||||
};
|
||||
}
|
||||
|
||||
export function computeComponent(
|
||||
component: Ref<CoercableComponent>,
|
||||
defaultWrapper = "div"
|
||||
): ShallowRef<Component | ""> {
|
||||
const comp = shallowRef<Component | "">();
|
||||
watchEffect(() => {
|
||||
comp.value = coerceComponent(unref(component), defaultWrapper);
|
||||
});
|
||||
return comp as ShallowRef<Component | "">;
|
||||
}
|
||||
export function computeOptionalComponent(
|
||||
component: Ref<CoercableComponent | undefined>,
|
||||
defaultWrapper = "div"
|
||||
): ShallowRef<Component | "" | null> {
|
||||
const comp = shallowRef<Component | "" | null>(null);
|
||||
watchEffect(() => {
|
||||
const currComponent = unref(component);
|
||||
comp.value =
|
||||
currComponent === "" || currComponent == null
|
||||
? null
|
||||
: coerceComponent(currComponent, defaultWrapper);
|
||||
});
|
||||
return comp;
|
||||
}
|
||||
|
||||
export function deepUnref<T extends object>(refObject: T): { [K in keyof T]: UnwrapRef<T[K]> } {
|
||||
return (Object.keys(refObject) as (keyof T)[]).reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = unref(refObject[curr]) as UnwrapRef<T[keyof T]>;
|
||||
return acc;
|
||||
},
|
||||
{} as { [K in keyof T]: UnwrapRef<T[K]> }
|
||||
);
|
||||
}
|
||||
|
||||
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
|
||||
if (isRef(ref.value)) {
|
||||
ref.value.value = value;
|
||||
|
@ -232,12 +174,10 @@ export type PropTypes =
|
|||
export function trackHover(element: VueFeature): Ref<boolean> {
|
||||
const isHovered = ref(false);
|
||||
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
element[GatherProps] = () => ({
|
||||
...elementGatherProps(),
|
||||
onPointerenter: () => (isHovered.value = true),
|
||||
onPointerleave: () => (isHovered.value = false)
|
||||
});
|
||||
(element as unknown as { onPointerenter: VoidFunction }).onPointerenter = () =>
|
||||
(isHovered.value = true);
|
||||
(element as unknown as { onPointerleave: VoidFunction }).onPointerleave = () =>
|
||||
(isHovered.value = true);
|
||||
|
||||
return isHovered;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<div v-if="mark">
|
||||
<div v-if="mark === true" class="mark star"></div>
|
||||
<img v-else class="mark" :src="mark" />
|
||||
</div>
|
||||
<div v-if="unref(mark) === true" class="mark star"></div>
|
||||
<img v-else-if="unref(mark) !== false" class="mark" :src="unref(mark) as string" />
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ mark?: boolean | string }>();
|
||||
import { MaybeRef, unref } from 'vue';
|
||||
|
||||
defineProps<{ mark: MaybeRef<boolean | string> }>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
45
src/wrappers/marks/mark.tsx
Normal file
45
src/wrappers/marks/mark.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { type OptionsFunc } from "features/feature";
|
||||
import { processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import type { VueFeature } from "util/vue";
|
||||
import { MaybeRef, MaybeRefOrGetter, unref } from "vue";
|
||||
import MarkNode from "./MarkNode.vue";
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Mark}.
|
||||
*/
|
||||
export interface MarkOptions {
|
||||
/** Whether or not to display a star or custom string. None if falsy value */
|
||||
mark: MaybeRefOrGetter<string | boolean>;
|
||||
}
|
||||
|
||||
/** An object that represents a tooltip that appears when hovering over an element. */
|
||||
export interface Mark {
|
||||
mark: MaybeRef<string | boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mark to the top left of the given element with the given options.
|
||||
* @param element The renderable feature to display the tooltip on.
|
||||
* @param options Mark options.
|
||||
*/
|
||||
export function addMark<T extends MarkOptions>(
|
||||
element: VueFeature,
|
||||
optionsFunc: OptionsFunc<T, Mark, Mark>
|
||||
) {
|
||||
const mark = createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Mark);
|
||||
const { mark, ...props } = options;
|
||||
|
||||
return {
|
||||
...(props as Omit<typeof props, keyof MarkOptions>),
|
||||
mark: processGetter(mark)
|
||||
} satisfies Mark;
|
||||
});
|
||||
|
||||
element.wrappers.push(el =>
|
||||
Boolean(unref(mark.mark)) ? <MarkNode mark={mark.mark}>{el}</MarkNode> : <>{el}</>
|
||||
);
|
||||
|
||||
return mark;
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
@click.capture="togglePinned"
|
||||
>
|
||||
<slot />
|
||||
<component v-if="elementComp" :is="elementComp" />
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="isShown"
|
||||
|
@ -28,7 +27,7 @@
|
|||
]"
|
||||
>
|
||||
<span v-if="showPin" class="material-icons pinned">push_pin</span>
|
||||
<component v-if="comp" :is="comp" />
|
||||
<Component />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
@ -36,43 +35,27 @@
|
|||
|
||||
<script setup lang="tsx">
|
||||
import themes from "data/themes";
|
||||
import type { CoercableComponent } from "features/feature";
|
||||
import { jsx, StyleValue } from "features/feature";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import settings from "game/settings";
|
||||
import { Direction } from "util/common";
|
||||
import type { VueFeature } from "util/vue";
|
||||
import {
|
||||
coerceComponent,
|
||||
computeOptionalComponent,
|
||||
renderJSX
|
||||
} from "util/vue";
|
||||
import { render } from "util/vue";
|
||||
import type { Component } from "vue";
|
||||
import { computed, ref, shallowRef, toRef, unref } from "vue";
|
||||
import { computed, ref, unref } from "vue";
|
||||
import { Tooltip } from "./tooltip";
|
||||
|
||||
const props = defineProps<{
|
||||
element?: VueFeature;
|
||||
display: CoercableComponent;
|
||||
style?: StyleValue;
|
||||
classes?: Record<string, boolean>;
|
||||
direction?: Direction;
|
||||
xoffset?: string;
|
||||
yoffset?: string;
|
||||
pinned?: Persistent<boolean>;
|
||||
pinned?: Tooltip["pinned"];
|
||||
display: Tooltip["display"];
|
||||
style?: Tooltip["style"];
|
||||
classes?: Tooltip["classes"];
|
||||
direction: Tooltip["direction"];
|
||||
xoffset?: Tooltip["xoffset"];
|
||||
yoffset?: Tooltip["yoffset"];
|
||||
}>();
|
||||
|
||||
const isHovered = ref(false);
|
||||
const isShown = computed(() => (props.pinned?.value === true || isHovered.value) && comp.value);
|
||||
const comp = computeOptionalComponent(toRef(props, "display"));
|
||||
const isShown = computed(() => props.pinned?.value === true || isHovered.value);
|
||||
|
||||
const elementComp = shallowRef<Component | "" | null>(
|
||||
coerceComponent(
|
||||
jsx(() => {
|
||||
const currComponent = props.element;
|
||||
return currComponent == null ? "" : renderJSX(currComponent);
|
||||
})
|
||||
)
|
||||
);
|
||||
const Component = () => render(props.display);
|
||||
|
||||
function togglePinned(e: MouseEvent) {
|
||||
const isPinned = props.pinned;
|
104
src/wrappers/tooltips/tooltip.tsx
Normal file
104
src/wrappers/tooltips/tooltip.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { isVisible, type OptionsFunc, type Replace } from "features/feature";
|
||||
import { deletePersistent, persistent } from "game/persistence";
|
||||
import { Direction } from "util/common";
|
||||
import { ProcessedRefOrGetter, processGetter } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Renderable, vueFeatureMixin, type VueFeature, type VueFeatureOptions } from "util/vue";
|
||||
import { MaybeRef, MaybeRefOrGetter, type Ref } from "vue";
|
||||
import Tooltip from "wrappers/tooltips/Tooltip.vue";
|
||||
|
||||
declare module "@vue/runtime-dom" {
|
||||
interface CSSProperties {
|
||||
"--xoffset"?: string;
|
||||
"--yoffset"?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that configures a {@link Tooltip}.
|
||||
*/
|
||||
export interface TooltipOptions extends VueFeatureOptions {
|
||||
/** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
|
||||
pinnable?: boolean;
|
||||
/** The text to display inside the tooltip. */
|
||||
display: MaybeRefOrGetter<Renderable>;
|
||||
/** The direction in which to display the tooltip */
|
||||
direction?: MaybeRefOrGetter<Direction>;
|
||||
/** The x offset of the tooltip, in px. */
|
||||
xoffset?: MaybeRefOrGetter<string>;
|
||||
/** The y offset of the tooltip, in px. */
|
||||
yoffset?: MaybeRefOrGetter<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that are added onto a processed {@link TooltipOptions} to create an {@link Tooltip}.
|
||||
*/
|
||||
export interface BaseTooltip extends VueFeature {
|
||||
pinned?: Ref<boolean>;
|
||||
}
|
||||
|
||||
/** An object that represents a tooltip that appears when hovering over an element. */
|
||||
export type Tooltip = Replace<
|
||||
Replace<TooltipOptions, BaseTooltip>,
|
||||
{
|
||||
pinnable: boolean;
|
||||
pinned?: Ref<boolean>;
|
||||
display: MaybeRef<Renderable>;
|
||||
direction: MaybeRef<Direction>;
|
||||
xoffset?: ProcessedRefOrGetter<TooltipOptions["xoffset"]>;
|
||||
yoffset?: ProcessedRefOrGetter<TooltipOptions["yoffset"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a tooltip on the given element with the given options.
|
||||
* @param element The renderable feature to display the tooltip on.
|
||||
* @param options Tooltip options.
|
||||
*/
|
||||
export function addTooltip<T extends TooltipOptions>(
|
||||
element: VueFeature,
|
||||
optionsFunc: OptionsFunc<T, BaseTooltip, Tooltip>
|
||||
) {
|
||||
const pinned = persistent<boolean>(false, false);
|
||||
const tooltip = createLazyProxy(feature => {
|
||||
const options = optionsFunc.call(feature, feature as Tooltip);
|
||||
const { pinnable, display, direction, xoffset, yoffset, ...props } = options;
|
||||
|
||||
if (pinnable === false) {
|
||||
deletePersistent(pinned);
|
||||
}
|
||||
|
||||
const tooltip = {
|
||||
...(props as Omit<typeof props, keyof VueFeature | keyof TooltipOptions>),
|
||||
...vueFeatureMixin("tooltip", options),
|
||||
pinnable: pinnable ?? true,
|
||||
pinned: pinnable === false ? undefined : pinned,
|
||||
display: processGetter(display),
|
||||
direction: processGetter(direction ?? Direction.Up),
|
||||
xoffset: processGetter(xoffset),
|
||||
yoffset: processGetter(yoffset)
|
||||
} satisfies Tooltip;
|
||||
|
||||
return tooltip;
|
||||
});
|
||||
|
||||
element.wrappers.push(el =>
|
||||
isVisible(tooltip.visibility ?? true) ? (
|
||||
<Tooltip
|
||||
pinned={tooltip.pinned}
|
||||
display={tooltip.display}
|
||||
classes={tooltip.classes}
|
||||
style={tooltip.style}
|
||||
direction={tooltip.direction}
|
||||
xoffset={tooltip.xoffset}
|
||||
yoffset={tooltip.yoffset}
|
||||
>
|
||||
{el}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>{el}</>
|
||||
)
|
||||
);
|
||||
|
||||
return tooltip;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
Conversion,
|
||||
createCumulativeConversion,
|
||||
createIndependentConversion,
|
||||
GenericConversion,
|
||||
setupPassiveGeneration
|
||||
} from "features/conversion";
|
||||
import { createResource, Resource } from "features/resources/resource";
|
||||
import { InvertibleIntegralFormula } from "game/formulas/types";
|
||||
import { createLayer, GenericLayer } from "game/layers";
|
||||
import { createLayer, Layer } from "game/layers";
|
||||
import Decimal from "util/bignum";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ref, unref } from "vue";
|
||||
|
@ -27,7 +27,7 @@ describe("Creating conversion", () => {
|
|||
|
||||
describe("Cumulative conversion", () => {
|
||||
describe("Calculates currentGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createCumulativeConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -53,7 +53,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates actualGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createCumulativeConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -79,7 +79,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates currentAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createCumulativeConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -109,7 +109,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates nextAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createCumulativeConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -188,7 +188,7 @@ describe("Creating conversion", () => {
|
|||
|
||||
describe("Independent conversion", () => {
|
||||
describe("Calculates currentGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createIndependentConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -215,7 +215,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates actualGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createIndependentConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -242,7 +242,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates currentAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createIndependentConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -273,7 +273,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Calculates nextAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
beforeEach(() => {
|
||||
conversion = createIndependentConversion(() => ({
|
||||
baseResource,
|
||||
|
@ -354,7 +354,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
describe("Custom conversion", () => {
|
||||
describe("Custom cumulative", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
const convert = vi.fn();
|
||||
const spend = vi.fn();
|
||||
const onConvert = vi.fn();
|
||||
|
@ -415,7 +415,7 @@ describe("Creating conversion", () => {
|
|||
});
|
||||
});
|
||||
describe("Custom independent", () => {
|
||||
let conversion: GenericConversion;
|
||||
let conversion: Conversion;
|
||||
const convert = vi.fn();
|
||||
const spend = vi.fn();
|
||||
const onConvert = vi.fn();
|
||||
|
@ -482,8 +482,8 @@ describe("Passive generation", () => {
|
|||
let baseResource: Resource;
|
||||
let gainResource: Resource;
|
||||
let formula: (x: InvertibleIntegralFormula) => InvertibleIntegralFormula;
|
||||
let conversion: GenericConversion;
|
||||
let layer: GenericLayer;
|
||||
let conversion: Conversion;
|
||||
let layer: Layer;
|
||||
beforeEach(() => {
|
||||
baseResource = createResource(ref(10));
|
||||
gainResource = createResource(ref(1));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createHotkey, hotkeys } from "features/hotkey";
|
||||
import { afterEach, describe, expect, onTestFailed, test } from "vitest";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
import { createReset, Reset } from "features/reset";
|
||||
import {
|
||||
branchedResetPropagation,
|
||||
createTree,
|
||||
createTreeNode,
|
||||
defaultResetPropagation,
|
||||
invertedResetPropagation,
|
||||
branchedResetPropagation
|
||||
invertedResetPropagation
|
||||
} from "features/trees/tree";
|
||||
import { createReset, GenericReset } from "features/reset";
|
||||
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
describe("Reset propagation", () => {
|
||||
let shouldReset: Ref<boolean>, shouldNotReset: Ref<boolean>;
|
||||
let goodReset: GenericReset, badReset: GenericReset;
|
||||
let goodReset: Reset, badReset: Reset;
|
||||
beforeAll(() => {
|
||||
shouldReset = ref(false);
|
||||
shouldNotReset = ref(false);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { createResource, Resource } from "features/resources/resource";
|
||||
import Formula, {
|
||||
calculateCost,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { CoercableComponent, JSXFunction } from "features/feature";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import {
|
||||
createAdditiveModifier,
|
||||
|
@ -10,16 +9,17 @@ import {
|
|||
} from "game/modifiers";
|
||||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import { WithRequired } from "util/common";
|
||||
import { Computable } from "util/computed";
|
||||
import { MaybeRefOrGetter } from "util/computed";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import { Ref, ref, unref } from "vue";
|
||||
import "../utils";
|
||||
import { MaybeRefOrGetter<Renderable>, render } from "util/vue";
|
||||
|
||||
export type ModifierConstructorOptions = {
|
||||
[S in "addend" | "multiplier" | "exponent"]: Computable<DecimalSource>;
|
||||
[S in "addend" | "multiplier" | "exponent"]: MaybeRefOrGetter<DecimalSource>;
|
||||
} & {
|
||||
description?: Computable<CoercableComponent>;
|
||||
enabled?: Computable<boolean>;
|
||||
description?: MaybeRefOrGetter<Renderable>;
|
||||
enabled?: MaybeRefOrGetter<boolean>;
|
||||
smallerIsBetter?: boolean;
|
||||
};
|
||||
|
||||
|
@ -33,7 +33,7 @@ function testModifiers<
|
|||
) {
|
||||
// Util because adding [property] messes up typing
|
||||
function createModifier(
|
||||
value: Computable<DecimalSource>,
|
||||
value: MaybeRefOrGetter<DecimalSource>,
|
||||
options: Partial<ModifierConstructorOptions> = {}
|
||||
): WithRequired<Modifier, "invert" | "getFormula"> {
|
||||
options[property] = value;
|
||||
|
@ -63,7 +63,7 @@ function testModifiers<
|
|||
test("with description", () => {
|
||||
const desc = createModifier(0, { description: "test" }).description;
|
||||
expect(desc).not.toBeUndefined();
|
||||
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(desc!)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -80,47 +80,49 @@ function testModifiers<
|
|||
describe("without smallerIsBetter false", () => {
|
||||
test("negative value", () =>
|
||||
expect(
|
||||
(
|
||||
createModifier(-5, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
render(
|
||||
createModifier(-5, {
|
||||
description: "test",
|
||||
smallerIsBetter: false
|
||||
}).description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("zero value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(0, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("positive value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(5, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
});
|
||||
describe("with smallerIsBetter true", () => {
|
||||
test("negative value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(-5, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("zero value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(0, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("positive value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(5, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
});
|
||||
});
|
||||
|
@ -134,7 +136,7 @@ describe("Exponential Modifiers", () =>
|
|||
|
||||
describe("Sequential Modifiers", () => {
|
||||
function createModifier<T extends Partial<ModifierConstructorOptions>>(
|
||||
value: Computable<DecimalSource>,
|
||||
value: MaybeRefOrGetter<DecimalSource>,
|
||||
options?: T
|
||||
) {
|
||||
return createSequentialModifier(() => [
|
||||
|
@ -167,7 +169,7 @@ describe("Sequential Modifiers", () => {
|
|||
test("with description", () => {
|
||||
const desc = createModifier(0, { description: "test" }).description;
|
||||
expect(desc).not.toBeUndefined();
|
||||
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(desc!)).toMatchSnapshot();
|
||||
});
|
||||
test("with both", () => {
|
||||
const desc = createSequentialModifier(() => [
|
||||
|
@ -175,7 +177,7 @@ describe("Sequential Modifiers", () => {
|
|||
createMultiplicativeModifier(() => ({ multiplier: 0, description: "test" }))
|
||||
]).description;
|
||||
expect(desc).not.toBeUndefined();
|
||||
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(desc!)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,47 +218,47 @@ describe("Sequential Modifiers", () => {
|
|||
describe("without smallerIsBetter false", () => {
|
||||
test("negative value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(-5, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("zero value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(0, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("positive value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(5, { description: "test", smallerIsBetter: false })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
});
|
||||
describe("with smallerIsBetter true", () => {
|
||||
test("negative value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(-5, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("zero value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(0, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
test("positive value", () =>
|
||||
expect(
|
||||
(
|
||||
render(
|
||||
createModifier(5, { description: "test", smallerIsBetter: true })
|
||||
.description as JSXFunction
|
||||
)()
|
||||
.description!
|
||||
)
|
||||
).toMatchSnapshot());
|
||||
});
|
||||
describe("with both", () => {
|
||||
|
@ -279,15 +281,15 @@ describe("Sequential Modifiers", () => {
|
|||
});
|
||||
test("negative value", () => {
|
||||
value.value = -5;
|
||||
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(modifier.description!)).toMatchSnapshot();
|
||||
});
|
||||
test("zero value", () => {
|
||||
value.value = 0;
|
||||
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(modifier.description!)).toMatchSnapshot();
|
||||
});
|
||||
test("positive value", () => {
|
||||
value.value = 5;
|
||||
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||
expect(render(modifier.description!)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -197,7 +197,7 @@ describe("Creating cost requirement", () => {
|
|||
|
||||
test("Creating visibility requirement", () => {
|
||||
const visibility = ref<Visibility.None | Visibility.Visible | boolean>(Visibility.Visible);
|
||||
const requirement = createVisibilityRequirement({ visibility });
|
||||
const requirement = createVisibilityRequirement(visibility);
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
visibility.value = true;
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
|
|
|
@ -40,7 +40,7 @@ expect.extend({
|
|||
console.error = vi.fn();
|
||||
received();
|
||||
const calls = (
|
||||
console.error as unknown as Mock<
|
||||
console.error as Mock<
|
||||
Parameters<typeof console.error>,
|
||||
ReturnType<typeof console.error>
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue