Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
ff3eb932d6
34 changed files with 781 additions and 285 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -6,6 +6,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.1] - 2022-05-10
|
||||||
|
### Added
|
||||||
|
- findFeatures can now accept multiple feature types
|
||||||
|
- excludeFeatures can now be used to find features with a feature type _blacklist_
|
||||||
|
- All the icons in the saves manager now have tooltips
|
||||||
|
### Changed
|
||||||
|
- All touch events that can be passive now are
|
||||||
|
- Layers' style and classes attributes are now applied to the tab element rather than the layer-tab
|
||||||
|
- Saving now always uses lz-string, and saveEncoding has been renamed to exportEncoding
|
||||||
|
- The property will now only affect exports, and defaults to base64 so exports can be shared in more places without issues
|
||||||
|
- Buyables can now have their onClick/purchase function overwritten
|
||||||
|
### Fixed
|
||||||
|
- Arrays in player were not being wrapped in proxies for things like NaN detection
|
||||||
|
- Error when switching between saves with different layers
|
||||||
|
- Links would sometimes error from trying to use nodes that were removed earlier that frame
|
||||||
|
- createModifierSection would require modifiers to have revert and enabled properties despite not using them
|
||||||
|
- Tab buttons would not use the style property if it was a ref
|
||||||
|
- Typings on the Board vue component were incorrect
|
||||||
|
- Offline time would always show, if offlineLimit is set to 0
|
||||||
|
- Buyables will now call onPurchase() when cost and/or resource were not set
|
||||||
|
- Presets dropdown wouldn't deselect the option after creating the save
|
||||||
|
### Documented
|
||||||
|
- feature.ts
|
||||||
|
|
||||||
|
## [0.4.0] - 2022-05-01
|
||||||
|
### Added
|
||||||
|
- Saves can now be encoded in two new options: plaintext and lz compressed, determined by a new "saveEncoding" property in projInfo
|
||||||
|
- Saves will be loaded in whatever format is detected. The setting only applies when writing saves
|
||||||
|
- createModifierSection has new parameter to override the label used for the base value
|
||||||
|
- createCollapsibleModifierSections utility function to display `createModifierSection`s in collapsible forms
|
||||||
|
### Fixed
|
||||||
|
- Saves manager would not clear the current save from its cache when switching saves, leading to progress loss if flipping between saves
|
||||||
|
- Layer.minWidth being ignored
|
||||||
|
- Separators between tabs (player.tabs) would not extend to the bottom of the screen when scrolling
|
||||||
|
- Tree nodes not being clicked on their edges
|
||||||
|
### Changed
|
||||||
|
- **BREAKING** No features extend persistent anymore
|
||||||
|
- This will break ALL existing saves that aren't manually dealt with in fixOldSave
|
||||||
|
- Affected features: Achievement, Buyable, Grid, Infobox, Milestone, TabFamily, and Upgrade
|
||||||
|
- Affected features will now have a property within them where the persistent ref is stored. This means new persistent refs can now be safely added to these features
|
||||||
|
- Features with option functions with 0 required properties now don't require passing in an options function
|
||||||
|
- Improved the look of the goBack and minimize buttons (and made them more consistent with each other)
|
||||||
|
- Newly created saves are immediately switched to
|
||||||
|
- TooltipDirection and Direction have been merged into one enum
|
||||||
|
- Made layers shallow reactive, so it works better with dynamic layers
|
||||||
|
- Modifier functions all have more explicit types now
|
||||||
|
- Scaling functions take computables instead of processed computables
|
||||||
|
### Removed
|
||||||
|
- Unused tsParticles.d.ts file
|
||||||
|
### Documented
|
||||||
|
- modifiers.ts
|
||||||
|
- conversions.ts
|
||||||
|
|
||||||
## [0.3.3] - 2022-04-24
|
## [0.3.3] - 2022-04-24
|
||||||
### Fixed
|
### Fixed
|
||||||
- Spacing between rows in Tree components
|
- Spacing between rows in Tree components
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.3.3",
|
"version": "0.4.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.3.3",
|
"version": "0.4.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pixi/particle-emitter": "^5.0.4",
|
"@pixi/particle-emitter": "^5.0.4",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.3.3",
|
"version": "0.4.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vue-cli-service serve",
|
"start": "vue-cli-service serve",
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tabs-container" :class="{ useHeader }">
|
<div class="tabs-container" :class="{ useHeader }">
|
||||||
<div v-for="(tab, index) in tabs" :key="index" class="tab" :ref="`tab-${index}`">
|
<div
|
||||||
|
v-for="(tab, index) in tabs"
|
||||||
|
:key="index"
|
||||||
|
class="tab"
|
||||||
|
:ref="`tab-${index}`"
|
||||||
|
:style="unref(layers[tab]?.style)"
|
||||||
|
:class="unref(layers[tab]?.classes)"
|
||||||
|
>
|
||||||
<Nav v-if="index === 0 && !useHeader" />
|
<Nav v-if="index === 0 && !useHeader" />
|
||||||
<div class="inner-tab">
|
<div class="inner-tab">
|
||||||
<Layer
|
<Layer
|
||||||
|
@ -19,7 +26,7 @@
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { GenericLayer, layers } from "game/layers";
|
import { GenericLayer, layers } from "game/layers";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { computed, toRef } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
import Layer from "./Layer.vue";
|
import Layer from "./Layer.vue";
|
||||||
import Nav from "./Nav.vue";
|
import Nav from "./Nav.vue";
|
||||||
|
|
||||||
|
@ -28,8 +35,8 @@ const layerKeys = computed(() => Object.keys(layers));
|
||||||
const useHeader = projInfo.useHeader;
|
const useHeader = projInfo.useHeader;
|
||||||
|
|
||||||
function gatherLayerProps(layer: GenericLayer) {
|
function gatherLayerProps(layer: GenericLayer) {
|
||||||
const { display, minimized, minWidth, name, color, style, classes, minimizable, nodes } = layer;
|
const { display, minimized, minWidth, name, color, minimizable, nodes } = layer;
|
||||||
return { display, minimized, minWidth, name, color, style, classes, minimizable, nodes };
|
return { display, minimized, minWidth, name, color, minimizable, nodes };
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,7 @@
|
||||||
<button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false">
|
<button class="layer-tab minimized" v-if="minimized.value" @click="minimized.value = false">
|
||||||
<div>{{ unref(name) }}</div>
|
<div>{{ unref(name) }}</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div class="layer-tab" :class="{ showGoBack }" v-else>
|
||||||
class="layer-tab"
|
|
||||||
:style="unref(style)"
|
|
||||||
:class="[{ showGoBack }, unref(classes)]"
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<Context ref="contextRef">
|
<Context ref="contextRef">
|
||||||
<component :is="component" />
|
<component :is="component" />
|
||||||
</Context>
|
</Context>
|
||||||
|
@ -22,7 +17,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import { CoercableComponent, StyleValue } from "features/feature";
|
import { CoercableComponent } from "features/feature";
|
||||||
import { FeatureNode } from "game/layers";
|
import { FeatureNode } from "game/layers";
|
||||||
import { Persistent } from "game/persistence";
|
import { Persistent } from "game/persistence";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
|
@ -58,8 +53,6 @@ export default defineComponent({
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
color: processedPropType<string>(String),
|
color: processedPropType<string>(String),
|
||||||
style: processedPropType<StyleValue>(String, Object, Array),
|
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
|
||||||
minimizable: processedPropType<boolean>(Boolean),
|
minimizable: processedPropType<boolean>(Boolean),
|
||||||
nodes: {
|
nodes: {
|
||||||
type: Object as PropType<Ref<Record<string, FeatureNode | undefined>>>,
|
type: Object as PropType<Ref<Record<string, FeatureNode | undefined>>>,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<img v-if="banner" :src="banner" class="banner" :alt="title" />
|
<img v-if="banner" :src="banner" class="banner" :alt="title" />
|
||||||
<div v-else class="title">{{ title }}</div>
|
<div v-else class="title">{{ title }}</div>
|
||||||
<div @click="changelog?.open()" class="version-container">
|
<div @click="changelog?.open()" class="version-container">
|
||||||
<Tooltip display="Changelog" :direction="TooltipDirection.DOWN" class="version"
|
<Tooltip display="Changelog" :direction="Direction.Down" class="version"
|
||||||
><span>v{{ versionNumber }}</span></Tooltip
|
><span>v{{ versionNumber }}</span></Tooltip
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,56 +26,51 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://forums.moddingtree.com/" target="_blank">
|
<a href="https://forums.moddingtree.com/" target="_blank">
|
||||||
<Tooltip display="Forums" :direction="TooltipDirection.DOWN" yoffset="5px">
|
<Tooltip display="Forums" :direction="Direction.Down" yoffset="5px">
|
||||||
<span class="material-icons">forum</span>
|
<span class="material-icons">forum</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div @click="info?.open()">
|
<div @click="info?.open()">
|
||||||
<Tooltip display="Info" :direction="TooltipDirection.DOWN" class="info">
|
<Tooltip display="Info" :direction="Direction.Down" class="info">
|
||||||
<span class="material-icons">info</span>
|
<span class="material-icons">info</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="savesManager?.open()">
|
<div @click="savesManager?.open()">
|
||||||
<Tooltip display="Saves" :direction="TooltipDirection.DOWN" xoffset="-20px">
|
<Tooltip display="Saves" :direction="Direction.Down" xoffset="-20px">
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons">library_books</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="options?.open()">
|
<div @click="options?.open()">
|
||||||
<Tooltip display="Options" :direction="TooltipDirection.DOWN" xoffset="-66px">
|
<Tooltip display="Options" :direction="Direction.Down" xoffset="-66px">
|
||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="overlay-nav" v-bind="$attrs">
|
<div v-else class="overlay-nav" v-bind="$attrs">
|
||||||
<div @click="changelog?.open()" class="version-container">
|
<div @click="changelog?.open()" class="version-container">
|
||||||
<Tooltip
|
<Tooltip display="Changelog" :direction="Direction.Right" xoffset="25%" class="version">
|
||||||
display="Changelog"
|
|
||||||
:direction="TooltipDirection.RIGHT"
|
|
||||||
xoffset="25%"
|
|
||||||
class="version"
|
|
||||||
>
|
|
||||||
<span>v{{ versionNumber }}</span>
|
<span>v{{ versionNumber }}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="savesManager?.open()">
|
<div @click="savesManager?.open()">
|
||||||
<Tooltip display="Saves" :direction="TooltipDirection.RIGHT">
|
<Tooltip display="Saves" :direction="Direction.Right">
|
||||||
<span class="material-icons">library_books</span>
|
<span class="material-icons">library_books</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="options?.open()">
|
<div @click="options?.open()">
|
||||||
<Tooltip display="Options" :direction="TooltipDirection.RIGHT">
|
<Tooltip display="Options" :direction="Direction.Right">
|
||||||
<span class="material-icons">settings</span>
|
<span class="material-icons">settings</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @click="info?.open()">
|
<div @click="info?.open()">
|
||||||
<Tooltip display="Info" :direction="TooltipDirection.RIGHT">
|
<Tooltip display="Info" :direction="Direction.Right">
|
||||||
<span class="material-icons">info</span>
|
<span class="material-icons">info</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://forums.moddingtree.com/" target="_blank">
|
<a href="https://forums.moddingtree.com/" target="_blank">
|
||||||
<Tooltip display="Forums" :direction="TooltipDirection.RIGHT" xoffset="7px">
|
<Tooltip display="Forums" :direction="Direction.Right" xoffset="7px">
|
||||||
<span class="material-icons">forum</span>
|
<span class="material-icons">forum</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
|
@ -111,7 +106,7 @@ import Info from "./Info.vue";
|
||||||
import Options from "./Options.vue";
|
import Options from "./Options.vue";
|
||||||
import SavesManager from "./SavesManager.vue";
|
import SavesManager from "./SavesManager.vue";
|
||||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import { TooltipDirection } from "features/tooltips/tooltip";
|
import { Direction } from "util/common";
|
||||||
|
|
||||||
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
||||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||||
|
|
|
@ -8,36 +8,48 @@
|
||||||
left
|
left
|
||||||
v-if="save.error == undefined && !isConfirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
|
<Tooltip display="Export" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">content_paste</span>
|
<span class="material-icons">content_paste</span>
|
||||||
|
</Tooltip>
|
||||||
</FeedbackButton>
|
</FeedbackButton>
|
||||||
<button
|
<button
|
||||||
@click="emit('duplicate')"
|
@click="emit('duplicate')"
|
||||||
class="button"
|
class="button"
|
||||||
v-if="save.error == undefined && !isConfirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
|
<Tooltip display="Duplicate" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">content_copy</span>
|
<span class="material-icons">content_copy</span>
|
||||||
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="isEditing = !isEditing"
|
@click="isEditing = !isEditing"
|
||||||
class="button"
|
class="button"
|
||||||
v-if="save.error == undefined && !isConfirming"
|
v-if="save.error == undefined && !isConfirming"
|
||||||
>
|
>
|
||||||
|
<Tooltip display="Edit Name" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">edit</span>
|
<span class="material-icons">edit</span>
|
||||||
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
<DangerButton
|
<DangerButton
|
||||||
:disabled="isActive"
|
:disabled="isActive"
|
||||||
@click="emit('delete')"
|
@click="emit('delete')"
|
||||||
@confirmingChanged="value => (isConfirming = value)"
|
@confirmingChanged="value => (isConfirming = value)"
|
||||||
>
|
>
|
||||||
|
<Tooltip display="Delete" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons" style="margin: -2px">delete</span>
|
<span class="material-icons" style="margin: -2px">delete</span>
|
||||||
|
</Tooltip>
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-else>
|
<div class="actions" v-else>
|
||||||
<button @click="changeName" class="button">
|
<button @click="changeName" class="button">
|
||||||
|
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">check</span>
|
<span class="material-icons">check</span>
|
||||||
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button @click="isEditing = !isEditing" class="button">
|
<button @click="isEditing = !isEditing" class="button">
|
||||||
|
<Tooltip display="Cancel" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons">close</span>
|
<span class="material-icons">close</span>
|
||||||
|
</Tooltip>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="details" v-if="save.error == undefined && !isEditing">
|
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||||
|
@ -58,7 +70,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
|
import { Direction } from "util/common";
|
||||||
import { computed, ref, toRefs, watch } from "vue";
|
import { computed, ref, toRefs, watch } from "vue";
|
||||||
import DangerButton from "./fields/DangerButton.vue";
|
import DangerButton from "./fields/DangerButton.vue";
|
||||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
import FeedbackButton from "./fields/FeedbackButton.vue";
|
||||||
|
|
|
@ -33,11 +33,11 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-title">Create Save</span>
|
<span class="field-title">Create Save</span>
|
||||||
<div class="field-buttons">
|
<div class="field-buttons">
|
||||||
<button class="button" @click="newSave">New Game</button>
|
<button class="button" @click="openSave(newSave().id)">New Game</button>
|
||||||
<Select
|
<Select
|
||||||
v-if="Object.keys(bank).length > 0"
|
v-if="Object.keys(bank).length > 0"
|
||||||
:options="bank"
|
:options="bank"
|
||||||
:modelValue="undefined"
|
:modelValue="selectedPreset"
|
||||||
@update:modelValue="preset => newFromPreset(preset as string)"
|
@update:modelValue="preset => newFromPreset(preset as string)"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
placeholder="Select preset"
|
placeholder="Select preset"
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import Modal from "components/Modal.vue";
|
import Modal from "components/Modal.vue";
|
||||||
import player, { PlayerData } from "game/player";
|
import player, { PlayerData, stringifySave } from "game/player";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { getUniqueID, loadSave, save, newSave } from "util/save";
|
import { getUniqueID, loadSave, save, newSave } from "util/save";
|
||||||
import { ComponentPublicInstance, computed, nextTick, ref, shallowReactive, watch } from "vue";
|
import { ComponentPublicInstance, computed, nextTick, ref, shallowReactive, watch } from "vue";
|
||||||
|
@ -68,6 +68,7 @@ import Text from "./fields/Text.vue";
|
||||||
import Save from "./Save.vue";
|
import Save from "./Save.vue";
|
||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import LZString from "lz-string";
|
import LZString from "lz-string";
|
||||||
|
import { ProxyState } from "util/proxies";
|
||||||
|
|
||||||
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
|
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
|
||||||
|
|
||||||
|
@ -82,6 +83,7 @@ defineExpose({
|
||||||
|
|
||||||
const importingFailed = ref(false);
|
const importingFailed = ref(false);
|
||||||
const saveToImport = ref("");
|
const saveToImport = ref("");
|
||||||
|
const selectedPreset = ref<string | null>(null);
|
||||||
|
|
||||||
watch(saveToImport, importedSave => {
|
watch(saveToImport, importedSave => {
|
||||||
if (importedSave) {
|
if (importedSave) {
|
||||||
|
@ -189,12 +191,13 @@ const saves = computed(() =>
|
||||||
function exportSave(id: string) {
|
function exportSave(id: string) {
|
||||||
let saveToExport;
|
let saveToExport;
|
||||||
if (player.id === id) {
|
if (player.id === id) {
|
||||||
saveToExport = save();
|
saveToExport = stringifySave(player[ProxyState]);
|
||||||
} else {
|
} else {
|
||||||
saveToExport = JSON.stringify(saves.value[id]);
|
saveToExport = JSON.stringify(saves.value[id]);
|
||||||
switch (projInfo.saveEncoding) {
|
}
|
||||||
|
switch (projInfo.exportEncoding) {
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
|
console.warn(`Unknown save encoding: ${projInfo.exportEncoding}. Defaulting to lz`);
|
||||||
case "lz":
|
case "lz":
|
||||||
saveToExport = LZString.compressToUTF16(saveToExport);
|
saveToExport = LZString.compressToUTF16(saveToExport);
|
||||||
break;
|
break;
|
||||||
|
@ -204,7 +207,6 @@ function exportSave(id: string) {
|
||||||
case "plain":
|
case "plain":
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
||||||
const el = document.createElement("textarea");
|
const el = document.createElement("textarea");
|
||||||
|
@ -245,6 +247,12 @@ function openSave(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function newFromPreset(preset: string) {
|
function newFromPreset(preset: string) {
|
||||||
|
// Reset preset dropdown
|
||||||
|
selectedPreset.value = preset;
|
||||||
|
nextTick(() => {
|
||||||
|
selectedPreset.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
if (preset[0] === "{") {
|
if (preset[0] === "{") {
|
||||||
// plaintext. No processing needed
|
// plaintext. No processing needed
|
||||||
} else if (preset[0] === "e") {
|
} else if (preset[0] === "e") {
|
||||||
|
@ -263,6 +271,8 @@ function newFromPreset(preset: string) {
|
||||||
save(playerData as PlayerData);
|
save(playerData as PlayerData);
|
||||||
|
|
||||||
settings.saves.push(playerData.id);
|
settings.saves.push(playerData.id);
|
||||||
|
|
||||||
|
openSave(playerData.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editSave(id: string, newName: string) {
|
function editSave(id: string, newName: string) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import "components/common/fields.css";
|
import "components/common/fields.css";
|
||||||
import { CoercableComponent } from "features/feature";
|
import { CoercableComponent } from "features/feature";
|
||||||
import { computeOptionalComponent } from "util/vue";
|
import { computeOptionalComponent, unwrapRef } from "util/vue";
|
||||||
import { ref, toRef, watch } from "vue";
|
import { ref, toRef, watch } from "vue";
|
||||||
import VueNextSelect from "vue-next-select";
|
import VueNextSelect from "vue-next-select";
|
||||||
import "vue-next-select/dist/index.css";
|
import "vue-next-select/dist/index.css";
|
||||||
|
@ -36,12 +36,12 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
|
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
|
||||||
|
|
||||||
const value = ref<SelectOption | undefined>(
|
const value = ref<SelectOption | null>(
|
||||||
props.options.find(option => option.value === props.modelValue)
|
props.options.find(option => option.value === props.modelValue) ?? null
|
||||||
);
|
);
|
||||||
watch(toRef(props, "modelValue"), modelValue => {
|
watch(toRef(props, "modelValue"), modelValue => {
|
||||||
if (value.value?.value !== modelValue) {
|
if (unwrapRef(value) !== modelValue) {
|
||||||
value.value = props.options.find(option => option.value === modelValue);
|
value.value = props.options.find(option => option.value === modelValue) ?? null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
9
src/data/common.css
Normal file
9
src/data/common.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.modifier-toggle {
|
||||||
|
padding-right: 10px;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifier-toggle.collapsed {
|
||||||
|
transform: translate(-5px, -5px) rotate(-90deg);
|
||||||
|
}
|
|
@ -5,7 +5,14 @@ import {
|
||||||
GenericClickable
|
GenericClickable
|
||||||
} from "features/clickables/clickable";
|
} from "features/clickables/clickable";
|
||||||
import { GenericConversion } from "features/conversion";
|
import { GenericConversion } from "features/conversion";
|
||||||
import { CoercableComponent, jsx, OptionsFunc, Replace, setDefault } from "features/feature";
|
import {
|
||||||
|
CoercableComponent,
|
||||||
|
jsx,
|
||||||
|
JSXFunction,
|
||||||
|
OptionsFunc,
|
||||||
|
Replace,
|
||||||
|
setDefault
|
||||||
|
} from "features/feature";
|
||||||
import { displayResource } from "features/resources/resource";
|
import { displayResource } from "features/resources/resource";
|
||||||
import {
|
import {
|
||||||
createTreeNode,
|
createTreeNode,
|
||||||
|
@ -14,16 +21,22 @@ import {
|
||||||
TreeNode,
|
TreeNode,
|
||||||
TreeNodeOptions
|
TreeNodeOptions
|
||||||
} from "features/trees/tree";
|
} from "features/trees/tree";
|
||||||
|
import { Modifier } from "game/modifiers";
|
||||||
|
import { Persistent, persistent } from "game/persistence";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import Decimal, { DecimalSource } from "util/bignum";
|
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||||
|
import { WithRequired } from "util/common";
|
||||||
import {
|
import {
|
||||||
Computable,
|
Computable,
|
||||||
|
convertComputable,
|
||||||
GetComputableType,
|
GetComputableType,
|
||||||
GetComputableTypeWithDefault,
|
GetComputableTypeWithDefault,
|
||||||
processComputable,
|
processComputable,
|
||||||
ProcessedComputable
|
ProcessedComputable
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
|
import { renderJSX } from "util/vue";
|
||||||
import { computed, Ref, unref } from "vue";
|
import { computed, Ref, unref } from "vue";
|
||||||
|
import "./common.css";
|
||||||
|
|
||||||
export interface ResetButtonOptions extends ClickableOptions {
|
export interface ResetButtonOptions extends ClickableOptions {
|
||||||
conversion: GenericConversion;
|
conversion: GenericConversion;
|
||||||
|
@ -177,3 +190,68 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
||||||
};
|
};
|
||||||
}) as unknown as LayerTreeNode<T>;
|
}) as unknown as LayerTreeNode<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCollapsibleModifierSections(
|
||||||
|
sections: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
modifier: WithRequired<Modifier, "description">;
|
||||||
|
base?: Computable<DecimalSource>;
|
||||||
|
unit?: string;
|
||||||
|
baseText?: Computable<CoercableComponent>;
|
||||||
|
visible?: Computable<boolean>;
|
||||||
|
}[]
|
||||||
|
): [JSXFunction, Persistent<boolean>[]] {
|
||||||
|
const processedBase = sections.map(s => convertComputable(s.base));
|
||||||
|
const processedBaseText = sections.map(s => convertComputable(s.baseText));
|
||||||
|
const processedVisible = sections.map(s => convertComputable(s.visible));
|
||||||
|
const collapsed = sections.map(() => persistent<boolean>(false));
|
||||||
|
const jsxFunc = jsx(() => {
|
||||||
|
const sectionJSX = sections.map((s, i) => {
|
||||||
|
if (unref(processedVisible[i]) === false) return null;
|
||||||
|
const header = (
|
||||||
|
<h3
|
||||||
|
onClick={() => (collapsed[i].value = !collapsed[i].value)}
|
||||||
|
style="cursor: pointer"
|
||||||
|
>
|
||||||
|
<span class={"modifier-toggle" + (unref(collapsed[i]) ? " collapsed" : "")}>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
|
{s.title}
|
||||||
|
{s.subtitle ? <span class="subtitle"> ({s.subtitle})</span> : null}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
|
||||||
|
const modifiers = unref(collapsed[i]) ? null : (
|
||||||
|
<>
|
||||||
|
<div class="modifier-container">
|
||||||
|
<span class="modifier-amount">
|
||||||
|
{format(unref(processedBase[i]) ?? 1)}
|
||||||
|
{s.unit}
|
||||||
|
</span>
|
||||||
|
<span class="modifier-description">
|
||||||
|
{renderJSX(unref(processedBaseText[i]) ?? "Base")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{renderJSX(unref(s.modifier.description))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{i === 0 ? null : <br />}
|
||||||
|
<div>
|
||||||
|
{header}
|
||||||
|
<br />
|
||||||
|
{modifiers}
|
||||||
|
<hr />
|
||||||
|
Total: {format(s.modifier.apply(unref(processedBase[i]) ?? 1))}
|
||||||
|
{s.unit}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <>{sectionJSX}</>;
|
||||||
|
});
|
||||||
|
return [jsxFunc, collapsed];
|
||||||
|
}
|
||||||
|
|
|
@ -67,10 +67,10 @@ export const main = createLayer("main", () => {
|
||||||
<>
|
<>
|
||||||
{player.devSpeed === 0 ? <div>Game Paused</div> : null}
|
{player.devSpeed === 0 ? <div>Game Paused</div> : null}
|
||||||
{player.devSpeed && player.devSpeed !== 1 ? (
|
{player.devSpeed && player.devSpeed !== 1 ? (
|
||||||
<div>Dev Speed: {format(player.devSpeed || 0)}x</div>
|
<div>Dev Speed: {format(player.devSpeed)}x</div>
|
||||||
) : null}
|
) : null}
|
||||||
{player.offlineTime != undefined ? (
|
{player.offlineTime ? (
|
||||||
<div>Offline Time: {formatTime(player.offlineTime || 0)}</div>
|
<div>Offline Time: {formatTime(player.offlineTime)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div>
|
||||||
{Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null}
|
{Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null}
|
||||||
|
|
|
@ -19,5 +19,5 @@
|
||||||
"maxTickLength": 3600,
|
"maxTickLength": 3600,
|
||||||
"offlineLimit": 1,
|
"offlineLimit": 1,
|
||||||
"enablePausing": true,
|
"enablePausing": true,
|
||||||
"saveEncoding": "lz"
|
"exportEncoding": "base64"
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,13 +45,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Direction } from "./bar";
|
|
||||||
import { CoercableComponent, Visibility } from "features/feature";
|
import { CoercableComponent, Visibility } from "features/feature";
|
||||||
import Decimal, { DecimalSource } from "util/bignum";
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
import { computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
import { computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
||||||
import { computed, CSSProperties, defineComponent, StyleValue, toRefs, unref } from "vue";
|
import { computed, CSSProperties, defineComponent, StyleValue, toRefs, unref } from "vue";
|
||||||
import Node from "components/Node.vue";
|
import Node from "components/Node.vue";
|
||||||
import MarkNode from "components/MarkNode.vue";
|
import MarkNode from "components/MarkNode.vue";
|
||||||
|
import { Direction } from "util/common";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Visibility
|
Visibility
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import { DecimalSource } from "util/bignum";
|
import { DecimalSource } from "util/bignum";
|
||||||
|
import { Direction } from "util/common";
|
||||||
import {
|
import {
|
||||||
Computable,
|
Computable,
|
||||||
GetComputableType,
|
GetComputableType,
|
||||||
|
@ -23,14 +24,6 @@ import { unref } from "vue";
|
||||||
|
|
||||||
export const BarType = Symbol("Bar");
|
export const BarType = Symbol("Bar");
|
||||||
|
|
||||||
export enum Direction {
|
|
||||||
Up = "Up",
|
|
||||||
Down = "Down",
|
|
||||||
Left = "Left",
|
|
||||||
Right = "Right",
|
|
||||||
Default = "Up"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BarOptions {
|
export interface BarOptions {
|
||||||
visibility?: Computable<Visibility>;
|
visibility?: Computable<Visibility>;
|
||||||
width: Computable<number>;
|
width: Computable<number>;
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
||||||
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
||||||
@mouseup="() => endDragging(dragging)"
|
@mouseup="() => endDragging(dragging)"
|
||||||
@touchend="() => endDragging(dragging)"
|
@touchend.passive="() => endDragging(dragging)"
|
||||||
@mouseleave="() => endDragging(dragging)"
|
@mouseleave="() => endDragging(dragging)"
|
||||||
>
|
>
|
||||||
<svg class="stage" width="100%" height="100%">
|
<svg class="stage" width="100%" height="100%">
|
||||||
<g class="g1">
|
<g class="g1">
|
||||||
<transition-group name="link" appear>
|
<transition-group name="link" appear>
|
||||||
<g v-for="(link, i) in links || []" :key="i">
|
<g v-for="(link, i) in unref(links) || []" :key="i">
|
||||||
<BoardLinkVue :link="link" />
|
<BoardLinkVue :link="link" />
|
||||||
</g>
|
</g>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
@ -38,8 +38,8 @@
|
||||||
:dragged="dragged"
|
:dragged="dragged"
|
||||||
:hasDragged="hasDragged"
|
:hasDragged="hasDragged"
|
||||||
:receivingNode="receivingNode?.id === node.id"
|
:receivingNode="receivingNode?.id === node.id"
|
||||||
:selectedNode="selectedNode"
|
:selectedNode="unref(selectedNode)"
|
||||||
:selectedAction="selectedAction"
|
:selectedAction="unref(selectedAction)"
|
||||||
@mouseDown="mouseDown"
|
@mouseDown="mouseDown"
|
||||||
@endDragging="endDragging"
|
@endDragging="endDragging"
|
||||||
/>
|
/>
|
||||||
|
@ -51,15 +51,35 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BoardNode, GenericBoard, getNodeProperty } from "features/boards/board";
|
import {
|
||||||
import { FeatureComponent, Visibility } from "features/feature";
|
BoardData,
|
||||||
|
BoardNode,
|
||||||
|
BoardNodeLink,
|
||||||
|
GenericBoardNodeAction,
|
||||||
|
GenericNodeType,
|
||||||
|
getNodeProperty
|
||||||
|
} from "features/boards/board";
|
||||||
|
import { StyleValue, Visibility } from "features/feature";
|
||||||
import { PersistentState } from "game/persistence";
|
import { PersistentState } from "game/persistence";
|
||||||
import { computed, ref, toRefs } from "vue";
|
import { ProcessedComputable } from "util/computed";
|
||||||
|
import { computed, Ref, ref, toRefs, unref } from "vue";
|
||||||
import panZoom from "vue-panzoom";
|
import panZoom from "vue-panzoom";
|
||||||
import BoardLinkVue from "./BoardLink.vue";
|
import BoardLinkVue from "./BoardLink.vue";
|
||||||
import BoardNodeVue from "./BoardNode.vue";
|
import BoardNodeVue from "./BoardNode.vue";
|
||||||
|
|
||||||
const _props = defineProps<FeatureComponent<GenericBoard>>();
|
const _props = defineProps<{
|
||||||
|
nodes: Ref<BoardNode[]>;
|
||||||
|
types: Record<string, GenericNodeType>;
|
||||||
|
[PersistentState]: Ref<BoardData>;
|
||||||
|
visibility: ProcessedComputable<Visibility>;
|
||||||
|
width?: ProcessedComputable<string>;
|
||||||
|
height?: ProcessedComputable<string>;
|
||||||
|
style?: ProcessedComputable<StyleValue>;
|
||||||
|
classes?: ProcessedComputable<Record<string, boolean>>;
|
||||||
|
links: Ref<BoardNodeLink[] | null>;
|
||||||
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
|
selectedNode: Ref<BoardNode | null>;
|
||||||
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
|
|
||||||
const lastMousePosition = ref({ x: 0, y: 0 });
|
const lastMousePosition = ref({ x: 0, y: 0 });
|
||||||
|
|
|
@ -46,9 +46,9 @@
|
||||||
@mouseenter="isHovering = true"
|
@mouseenter="isHovering = true"
|
||||||
@mouseleave="isHovering = false"
|
@mouseleave="isHovering = false"
|
||||||
@mousedown="mouseDown"
|
@mousedown="mouseDown"
|
||||||
@touchstart="mouseDown"
|
@touchstart.passive="mouseDown"
|
||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
@touchend="mouseUp"
|
@touchend.passive="mouseUp"
|
||||||
>
|
>
|
||||||
<g v-if="shape === Shape.Circle">
|
<g v-if="shape === Shape.Circle">
|
||||||
<circle
|
<circle
|
||||||
|
|
|
@ -46,7 +46,7 @@ export interface BuyableOptions {
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
small?: Computable<boolean>;
|
small?: Computable<boolean>;
|
||||||
display?: Computable<BuyableDisplay>;
|
display?: Computable<BuyableDisplay>;
|
||||||
onPurchase?: (cost: DecimalSource) => void;
|
onPurchase?: (cost: DecimalSource | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseBuyable {
|
export interface BaseBuyable {
|
||||||
|
@ -144,18 +144,23 @@ export function createBuyable<T extends BuyableOptions>(
|
||||||
});
|
});
|
||||||
processComputable(buyable as T, "canPurchase");
|
processComputable(buyable as T, "canPurchase");
|
||||||
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
|
buyable.canClick = buyable.canPurchase as ProcessedComputable<boolean>;
|
||||||
buyable.onClick = buyable.purchase = function () {
|
buyable.onClick = buyable.purchase =
|
||||||
|
buyable.onClick ??
|
||||||
|
buyable.purchase ??
|
||||||
|
function (this: GenericBuyable) {
|
||||||
const genericBuyable = buyable as GenericBuyable;
|
const genericBuyable = buyable as GenericBuyable;
|
||||||
if (
|
if (!unref(genericBuyable.canPurchase)) {
|
||||||
!unref(genericBuyable.canPurchase) ||
|
|
||||||
genericBuyable.cost == null ||
|
|
||||||
genericBuyable.resource == null
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cost = unref(genericBuyable.cost);
|
const cost = unref(genericBuyable.cost);
|
||||||
genericBuyable.resource.value = Decimal.sub(genericBuyable.resource.value, cost);
|
if (genericBuyable.cost != null && genericBuyable.resource != null) {
|
||||||
|
genericBuyable.resource.value = Decimal.sub(
|
||||||
|
genericBuyable.resource.value,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
cost!
|
||||||
|
);
|
||||||
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
|
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
|
||||||
|
}
|
||||||
this.onPurchase?.(cost);
|
this.onPurchase?.(cost);
|
||||||
};
|
};
|
||||||
processComputable(buyable as T, "display");
|
processComputable(buyable as T, "display");
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
@mousedown="start"
|
@mousedown="start"
|
||||||
@mouseleave="stop"
|
@mouseleave="stop"
|
||||||
@mouseup="stop"
|
@mouseup="stop"
|
||||||
@touchstart="start"
|
@touchstart.passive="start"
|
||||||
@touchend="stop"
|
@touchend.passive="stop"
|
||||||
@touchcancel="stop"
|
@touchcancel.passive="stop"
|
||||||
:class="{
|
:class="{
|
||||||
feature: true,
|
feature: true,
|
||||||
clickable: true,
|
clickable: true,
|
||||||
|
|
|
@ -1,36 +1,91 @@
|
||||||
import { GenericLayer } from "game/layers";
|
import { GenericLayer } from "game/layers";
|
||||||
import { Modifier } from "game/modifiers";
|
import { Modifier } from "game/modifiers";
|
||||||
import Decimal, { DecimalSource } from "util/bignum";
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
import { isFunction } from "util/common";
|
import { WithRequired } from "util/common";
|
||||||
import {
|
import {
|
||||||
Computable,
|
Computable,
|
||||||
|
convertComputable,
|
||||||
GetComputableTypeWithDefault,
|
GetComputableTypeWithDefault,
|
||||||
processComputable,
|
processComputable,
|
||||||
ProcessedComputable
|
ProcessedComputable
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { computed, isRef, Ref, unref } from "vue";
|
import { computed, Ref, unref } from "vue";
|
||||||
import { OptionsFunc, Replace, setDefault } from "./feature";
|
import { OptionsFunc, Replace, setDefault } from "./feature";
|
||||||
import { Resource } from "./resources/resource";
|
import { Resource } from "./resources/resource";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link conversion}.
|
||||||
|
*/
|
||||||
export interface ConversionOptions {
|
export interface ConversionOptions {
|
||||||
|
/**
|
||||||
|
* The scaling function that is used to determine the rate of conversion from one {@link resource} to the other.
|
||||||
|
*/
|
||||||
scaling: ScalingFunction;
|
scaling: ScalingFunction;
|
||||||
|
/**
|
||||||
|
* 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?: Computable<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?: Computable<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?: Computable<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?: Computable<DecimalSource>;
|
||||||
|
/**
|
||||||
|
* The input {@link resource} for this conversion.
|
||||||
|
*/
|
||||||
baseResource: Resource;
|
baseResource: Resource;
|
||||||
|
/**
|
||||||
|
* The output {@link resource} for this conversion. i.e. the resource being generated.
|
||||||
|
*/
|
||||||
gainResource: Resource;
|
gainResource: Resource;
|
||||||
|
/**
|
||||||
|
* Whether or not to cap the amount of the output resource gained by converting at 1.
|
||||||
|
*/
|
||||||
buyMax?: Computable<boolean>;
|
buyMax?: Computable<boolean>;
|
||||||
|
/**
|
||||||
|
* Whether or not to round up the cost to generate a given amount of the output resource.
|
||||||
|
*/
|
||||||
roundUpCost?: Computable<boolean>;
|
roundUpCost?: Computable<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.
|
||||||
|
*/
|
||||||
convert?: VoidFunction;
|
convert?: VoidFunction;
|
||||||
gainModifier?: Modifier;
|
/**
|
||||||
|
* An addition modifier that will be applied to the gain amounts.
|
||||||
|
* Must be reversible in order to correctly calculate {@link nextAt}.
|
||||||
|
* @see {@link createSequentialModifier} if you want to apply multiple modifiers.
|
||||||
|
*/
|
||||||
|
gainModifier?: WithRequired<Modifier, "revert">;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ConversionOptions} to create a {@link Conversion}.
|
||||||
|
*/
|
||||||
export interface BaseConversion {
|
export interface BaseConversion {
|
||||||
|
/**
|
||||||
|
* The function that performs the actual conversion.
|
||||||
|
*/
|
||||||
convert: VoidFunction;
|
convert: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that converts one {@link resource} into another at a given rate.
|
||||||
|
*/
|
||||||
export type Conversion<T extends ConversionOptions> = Replace<
|
export type Conversion<T extends ConversionOptions> = Replace<
|
||||||
T & BaseConversion,
|
T & BaseConversion,
|
||||||
{
|
{
|
||||||
|
@ -43,6 +98,9 @@ export type Conversion<T extends ConversionOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type that matches any {@link conversion} object.
|
||||||
|
*/
|
||||||
export type GenericConversion = Replace<
|
export type GenericConversion = Replace<
|
||||||
Conversion<ConversionOptions>,
|
Conversion<ConversionOptions>,
|
||||||
{
|
{
|
||||||
|
@ -55,6 +113,13 @@ export type GenericConversion = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a conversion with the given options.
|
||||||
|
* You typically shouldn't use this function directly. Instead use one of the other conversion constructors, which will then call this.
|
||||||
|
* @param optionsFunc Conversion options.
|
||||||
|
* @see {@link createCumulativeConversion}.
|
||||||
|
* @see {@link createIndependentConversion}.
|
||||||
|
*/
|
||||||
export function createConversion<T extends ConversionOptions>(
|
export function createConversion<T extends ConversionOptions>(
|
||||||
optionsFunc: OptionsFunc<T, Conversion<T>, BaseConversion>
|
optionsFunc: OptionsFunc<T, Conversion<T>, BaseConversion>
|
||||||
): Conversion<T> {
|
): Conversion<T> {
|
||||||
|
@ -118,27 +183,64 @@ export function createConversion<T extends ConversionOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScalingFunction = {
|
/**
|
||||||
|
* A collection of functions that allow a conversion to scale the amount of resources gained based on the input resource.
|
||||||
|
* This typically shouldn't be created directly. Instead use one of the scaling function constructors.
|
||||||
|
* @see {@link createLinearScaling}.
|
||||||
|
* @see {@link createPolynomialScaling}.
|
||||||
|
*/
|
||||||
|
export interface ScalingFunction {
|
||||||
|
/**
|
||||||
|
* Calculates the amount of the output resource a conversion should be able to currently produce.
|
||||||
|
* This should be based off of `conversion.baseResource.value`.
|
||||||
|
* The conversion is responsible for applying the gainModifier, so this function should be un-modified.
|
||||||
|
* It does not need to be clamped or rounded.
|
||||||
|
*/
|
||||||
currentGain: (conversion: GenericConversion) => DecimalSource;
|
currentGain: (conversion: GenericConversion) => DecimalSource;
|
||||||
|
/**
|
||||||
|
* Calculates the amount of the input resource that is required for the current value of `conversion.currentGain`.
|
||||||
|
* Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
|
||||||
|
* The conversion is responsible for rounding up the amount as appropriate.
|
||||||
|
* The returned value should not be below 0.
|
||||||
|
*/
|
||||||
currentAt: (conversion: GenericConversion) => DecimalSource;
|
currentAt: (conversion: GenericConversion) => DecimalSource;
|
||||||
|
/**
|
||||||
|
* Calculates the amount of the input resource that would be required for the current value of `conversion.currentGain` to increase.
|
||||||
|
* Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
|
||||||
|
* The conversion is responsible for rounding up the amount as appropriate.
|
||||||
|
* The returned value should not be below 0.
|
||||||
|
*/
|
||||||
nextAt: (conversion: GenericConversion) => DecimalSource;
|
nextAt: (conversion: GenericConversion) => DecimalSource;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Gain formula is (baseResource - base) * coefficient
|
/**
|
||||||
// e.g. if base is 10 and coefficient is 0.5, 10 points makes 1 gain, 12 points is 2
|
* Creates a scaling function based off the formula `(baseResource - base) * coefficient`.
|
||||||
|
* If the baseResource value is less than base then the currentGain will be 0.
|
||||||
|
* @param base The base variable in the scaling formula.
|
||||||
|
* @param coefficient The coefficient variable in the scaling formula.
|
||||||
|
* @example
|
||||||
|
* A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values:
|
||||||
|
* | Base Resource | Current Gain |
|
||||||
|
* | ------------- | ------------ |
|
||||||
|
* | 10 | 1 |
|
||||||
|
* | 12 | 2 |
|
||||||
|
* | 20 | 6 |
|
||||||
|
*/
|
||||||
export function createLinearScaling(
|
export function createLinearScaling(
|
||||||
base: DecimalSource | Ref<DecimalSource>,
|
base: Computable<DecimalSource>,
|
||||||
coefficient: DecimalSource | Ref<DecimalSource>
|
coefficient: Computable<DecimalSource>
|
||||||
): ScalingFunction {
|
): ScalingFunction {
|
||||||
|
const processedBase = convertComputable(base);
|
||||||
|
const processedCoefficient = convertComputable(coefficient);
|
||||||
return {
|
return {
|
||||||
currentGain(conversion) {
|
currentGain(conversion) {
|
||||||
if (Decimal.lt(conversion.baseResource.value, unref(base))) {
|
if (Decimal.lt(conversion.baseResource.value, unref(processedBase))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Decimal.sub(conversion.baseResource.value, unref(base))
|
return Decimal.sub(conversion.baseResource.value, unref(processedBase))
|
||||||
.sub(1)
|
.sub(1)
|
||||||
.times(unref(coefficient))
|
.times(unref(processedCoefficient))
|
||||||
.add(1);
|
.add(1);
|
||||||
},
|
},
|
||||||
currentAt(conversion) {
|
currentAt(conversion) {
|
||||||
|
@ -147,7 +249,9 @@ export function createLinearScaling(
|
||||||
current = conversion.gainModifier.revert(current);
|
current = conversion.gainModifier.revert(current);
|
||||||
}
|
}
|
||||||
current = Decimal.max(0, current);
|
current = Decimal.max(0, current);
|
||||||
return Decimal.times(current, unref(coefficient)).add(unref(base));
|
return Decimal.sub(current, 1)
|
||||||
|
.div(unref(processedCoefficient))
|
||||||
|
.add(unref(processedBase));
|
||||||
},
|
},
|
||||||
nextAt(conversion) {
|
nextAt(conversion) {
|
||||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
||||||
|
@ -155,21 +259,41 @@ export function createLinearScaling(
|
||||||
next = conversion.gainModifier.revert(next);
|
next = conversion.gainModifier.revert(next);
|
||||||
}
|
}
|
||||||
next = Decimal.max(0, next);
|
next = Decimal.max(0, next);
|
||||||
return Decimal.times(next, unref(coefficient)).add(unref(base)).max(unref(base));
|
return Decimal.sub(next, 1)
|
||||||
|
.div(unref(processedCoefficient))
|
||||||
|
.add(unref(processedBase))
|
||||||
|
.max(unref(processedBase));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gain formula is (baseResource / base) ^ exponent
|
/**
|
||||||
// e.g. if exponent is 0.5 and base is 10, then having 10 points makes gain 1, and 40 points is 2
|
* Creates a scaling function based off the formula `(baseResource / base) ^ exponent`.
|
||||||
|
* If the baseResource value is less than base then the currentGain will be 0.
|
||||||
|
* @param base The base variable in the scaling formula.
|
||||||
|
* @param exponent The exponent variable in the scaling formula.
|
||||||
|
* @example
|
||||||
|
* A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values:
|
||||||
|
* | Base Resource | Current Gain |
|
||||||
|
* | ------------- | ------------ |
|
||||||
|
* | 10 | 1 |
|
||||||
|
* | 40 | 2 |
|
||||||
|
* | 250 | 5 |
|
||||||
|
*/
|
||||||
export function createPolynomialScaling(
|
export function createPolynomialScaling(
|
||||||
base: DecimalSource | Ref<DecimalSource>,
|
base: Computable<DecimalSource>,
|
||||||
exponent: DecimalSource | Ref<DecimalSource>
|
exponent: Computable<DecimalSource>
|
||||||
): ScalingFunction {
|
): ScalingFunction {
|
||||||
|
const processedBase = convertComputable(base);
|
||||||
|
const processedExponent = convertComputable(exponent);
|
||||||
return {
|
return {
|
||||||
currentGain(conversion) {
|
currentGain(conversion) {
|
||||||
const gain = Decimal.div(conversion.baseResource.value, unref(base)).pow(
|
if (Decimal.lt(conversion.baseResource.value, unref(processedBase))) {
|
||||||
unref(exponent)
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gain = Decimal.div(conversion.baseResource.value, unref(processedBase)).pow(
|
||||||
|
unref(processedExponent)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (gain.isNan()) {
|
if (gain.isNan()) {
|
||||||
|
@ -183,7 +307,7 @@ export function createPolynomialScaling(
|
||||||
current = conversion.gainModifier.revert(current);
|
current = conversion.gainModifier.revert(current);
|
||||||
}
|
}
|
||||||
current = Decimal.max(0, current);
|
current = Decimal.max(0, current);
|
||||||
return Decimal.root(current, unref(exponent)).times(unref(base));
|
return Decimal.root(current, unref(processedExponent)).times(unref(processedBase));
|
||||||
},
|
},
|
||||||
nextAt(conversion) {
|
nextAt(conversion) {
|
||||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
||||||
|
@ -191,17 +315,30 @@ export function createPolynomialScaling(
|
||||||
next = conversion.gainModifier.revert(next);
|
next = conversion.gainModifier.revert(next);
|
||||||
}
|
}
|
||||||
next = Decimal.max(0, next);
|
next = Decimal.max(0, next);
|
||||||
return Decimal.root(next, unref(exponent)).times(unref(base)).max(unref(base));
|
return Decimal.root(next, unref(processedExponent))
|
||||||
|
.times(unref(processedBase))
|
||||||
|
.max(unref(processedBase));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a conversion that simply adds to the gainResource amount upon converting.
|
||||||
|
* This is similar to the behavior of "normal" layers in The Modding Tree.
|
||||||
|
* This is equivalent to just calling createConversion directly.
|
||||||
|
* @param optionsFunc Conversion options.
|
||||||
|
*/
|
||||||
export function createCumulativeConversion<S extends ConversionOptions>(
|
export function createCumulativeConversion<S extends ConversionOptions>(
|
||||||
optionsFunc: OptionsFunc<S, Conversion<S>>
|
optionsFunc: OptionsFunc<S, Conversion<S>>
|
||||||
): Conversion<S> {
|
): Conversion<S> {
|
||||||
return createConversion(optionsFunc);
|
return createConversion(optionsFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a conversion that will replace the gainResource amount with the new amount upon converting.
|
||||||
|
* This is similar to the behavior of "static" layers in The Modding Tree.
|
||||||
|
* @param optionsFunc Converison options.
|
||||||
|
*/
|
||||||
export function createIndependentConversion<S extends ConversionOptions>(
|
export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
optionsFunc: OptionsFunc<S, Conversion<S>>
|
optionsFunc: OptionsFunc<S, Conversion<S>>
|
||||||
): Conversion<S> {
|
): Conversion<S> {
|
||||||
|
@ -254,13 +391,22 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will automatically increase the value of conversion.gainResource without lowering the value of the input resource.
|
||||||
|
* It will by default perform 100% of a conversion's currentGain per second.
|
||||||
|
* If you use a ref for the rate you can set it's value to 0 when passive generation should be disabled.
|
||||||
|
* @param layer The layer this passive generation will be associated with.
|
||||||
|
* @param conversion The conversion that will determine how much generation there is.
|
||||||
|
* @param rate A multiplier to multiply against the conversion's currentGain.
|
||||||
|
*/
|
||||||
export function setupPassiveGeneration(
|
export function setupPassiveGeneration(
|
||||||
layer: GenericLayer,
|
layer: GenericLayer,
|
||||||
conversion: GenericConversion,
|
conversion: GenericConversion,
|
||||||
rate: ProcessedComputable<DecimalSource> = 1
|
rate: Computable<DecimalSource> = 1
|
||||||
): void {
|
): void {
|
||||||
|
const processedRate = convertComputable(rate);
|
||||||
layer.on("preUpdate", diff => {
|
layer.on("preUpdate", diff => {
|
||||||
const currRate = isRef(rate) ? rate.value : rate;
|
const currRate = unref(processedRate);
|
||||||
if (Decimal.neq(currRate, 0)) {
|
if (Decimal.neq(currRate, 0)) {
|
||||||
conversion.gainResource.value = Decimal.add(
|
conversion.gainResource.value = Decimal.add(
|
||||||
conversion.gainResource.value,
|
conversion.gainResource.value,
|
||||||
|
@ -270,7 +416,22 @@ export function setupPassiveGeneration(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function softcap(
|
/**
|
||||||
|
* Given a value, this function finds the amount above a certain value and raises it to a power.
|
||||||
|
* If the power is <1, this will effectively make the value scale slower after the cap.
|
||||||
|
* @param value The raw value.
|
||||||
|
* @param cap The value after which the softcap should be applied.
|
||||||
|
* @param power The power to raise value above the cap to.
|
||||||
|
* @example
|
||||||
|
* A softcap added via `addSoftcap(scaling, 100, 0.5)` would produce the following values:
|
||||||
|
* | Raw Value | Softcapped Value |
|
||||||
|
* | --------- | ---------------- |
|
||||||
|
* | 1 | 1 |
|
||||||
|
* | 100 | 100 |
|
||||||
|
* | 125 | 105 |
|
||||||
|
* | 200 | 110 |
|
||||||
|
*/
|
||||||
|
export function softcap(
|
||||||
value: DecimalSource,
|
value: DecimalSource,
|
||||||
cap: DecimalSource,
|
cap: DecimalSource,
|
||||||
power: DecimalSource = 0.5
|
power: DecimalSource = 0.5
|
||||||
|
@ -282,6 +443,15 @@ function softcap(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a scaling function based off an existing scaling function, with a softcap applied to it.
|
||||||
|
* The softcap will take any value above a certain value and raise it to a power.
|
||||||
|
* If the power is <1, this will effectively make the value scale slower after the cap.
|
||||||
|
* @param scaling The raw scaling function.
|
||||||
|
* @param cap The value after which the softcap should be applied.
|
||||||
|
* @param power The power to raise value about the cap to.
|
||||||
|
* @see {@link softcap}.
|
||||||
|
*/
|
||||||
export function addSoftcap(
|
export function addSoftcap(
|
||||||
scaling: ScalingFunction,
|
scaling: ScalingFunction,
|
||||||
cap: ProcessedComputable<DecimalSource>,
|
cap: ProcessedComputable<DecimalSource>,
|
||||||
|
@ -294,6 +464,12 @@ export function addSoftcap(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a scaling function off an existing function, with a hardcap applied to it.
|
||||||
|
* The harcap will ensure that the currentGain will stop at a given cap.
|
||||||
|
* @param scaling The raw scaling function.
|
||||||
|
* @param cap The maximum value the scaling function can output.
|
||||||
|
*/
|
||||||
export function addHardcap(
|
export function addHardcap(
|
||||||
scaling: ScalingFunction,
|
scaling: ScalingFunction,
|
||||||
cap: ProcessedComputable<DecimalSource>
|
cap: ProcessedComputable<DecimalSource>
|
||||||
|
|
|
@ -1,55 +1,84 @@
|
||||||
import { DefaultValue } from "game/persistence";
|
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
import { DoNotCache, ProcessedComputable } from "util/computed";
|
import { DoNotCache } from "util/computed";
|
||||||
import { CSSProperties, DefineComponent, isRef } from "vue";
|
import { CSSProperties, DefineComponent, isRef } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A symbol to use as a key for a vue component a feature can be rendered with
|
||||||
|
* @see {@link VueFeature}
|
||||||
|
*/
|
||||||
export const Component = Symbol("Component");
|
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 VueFeature}
|
||||||
|
*/
|
||||||
export const GatherProps = Symbol("GatherProps");
|
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 };
|
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;
|
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>;
|
export type StyleValue = string | CSSProperties | Array<string | CSSProperties>;
|
||||||
|
|
||||||
// TODO if importing .vue components in .tsx can become type safe,
|
/** A type that refers to any vue component */
|
||||||
// this type can probably be safely removed
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type GenericComponent = DefineComponent<any, any, any>;
|
export type GenericComponent = DefineComponent<any, any, any>;
|
||||||
|
|
||||||
export type FeatureComponent<T> = Omit<
|
/** Utility type that is S, with any properties from T that aren't already present in S */
|
||||||
{
|
|
||||||
[K in keyof T]: T[K] extends ProcessedComputable<infer S> ? S : T[K];
|
|
||||||
},
|
|
||||||
typeof Component | typeof DefaultValue
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type Replace<T, S> = S & Omit<T, keyof S>;
|
export type Replace<T, S> = S & Omit<T, keyof S>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for a function that returns an object of a given type,
|
||||||
|
* with "this" bound to what the type will eventually be processed into.
|
||||||
|
* Intended for making lazily evaluated objects.
|
||||||
|
*/
|
||||||
export type OptionsFunc<T, S = T, R = Record<string, unknown>> = () => T & ThisType<S> & Partial<R>;
|
export type OptionsFunc<T, S = T, R = Record<string, unknown>> = () => T & ThisType<S> & Partial<R>;
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
// Get a unique ID to allow a feature to be found for creating branches
|
/**
|
||||||
// and any other uses requiring unique identifiers for each feature
|
* Gets a unique ID to give to each feature, used for any sort of system that needs to identify
|
||||||
// IDs are gauranteed unique, but should not be saved as they are not
|
* elements in the DOM rather than references to the feature itself. (For example, branches)
|
||||||
// guaranteed to be persistent through updates and such
|
* IDs are guaranteed unique, but _NOT_ persistent - they likely will change between updates.
|
||||||
|
* @param prefix A string to prepend to the id to make it more readable in the inspector tools
|
||||||
|
*/
|
||||||
export function getUniqueID(prefix = "feature-"): string {
|
export function getUniqueID(prefix = "feature-"): string {
|
||||||
return prefix + id++;
|
return prefix + id++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Enum for what the visibility of a feature or component should be */
|
||||||
export enum Visibility {
|
export enum Visibility {
|
||||||
|
/** The feature or component should be visible */
|
||||||
Visible,
|
Visible,
|
||||||
|
/** The feature or component should not appear but still take up space */
|
||||||
Hidden,
|
Hidden,
|
||||||
|
/** The feature or component should not appear not take up space */
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export function jsx(func: () => JSX.Element | ""): JSXFunction {
|
||||||
(func as Partial<JSXFunction>)[DoNotCache] = true;
|
(func as Partial<JSXFunction>)[DoNotCache] = true;
|
||||||
return func as JSXFunction;
|
return func as JSXFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility function to convert a boolean value into a Visbility value */
|
||||||
export function showIf(condition: boolean, otherwise = Visibility.None): Visibility {
|
export function showIf(condition: boolean, otherwise = Visibility.None): Visibility {
|
||||||
return condition ? Visibility.Visible : otherwise;
|
return condition ? Visibility.Visible : otherwise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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>(
|
export function setDefault<T, K extends keyof T>(
|
||||||
object: T,
|
object: T,
|
||||||
key: K,
|
key: K,
|
||||||
|
@ -60,13 +89,48 @@ export function setDefault<T, K extends keyof T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findFeatures(obj: Record<string, unknown>, type: symbol): unknown[] {
|
/**
|
||||||
|
* Traverses an object and returns all features of the given type(s)
|
||||||
|
* @param obj The object to traverse
|
||||||
|
* @param types The feature types that will be searched for
|
||||||
|
*/
|
||||||
|
export function findFeatures(obj: Record<string, unknown>, ...types: symbol[]): unknown[] {
|
||||||
const objects: unknown[] = [];
|
const objects: unknown[] = [];
|
||||||
const handleObject = (obj: Record<string, unknown>) => {
|
const handleObject = (obj: Record<string, unknown>) => {
|
||||||
Object.keys(obj).forEach(key => {
|
Object.keys(obj).forEach(key => {
|
||||||
const value = obj[key];
|
const value = obj[key];
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
if ((value as Record<string, unknown>).type === type) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
if (types.includes((value as Record<string, any>).type)) {
|
||||||
|
objects.push(value);
|
||||||
|
} else if (!(value instanceof Decimal) && !isRef(value)) {
|
||||||
|
handleObject(value as Record<string, unknown>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
handleObject(obj);
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param obj The object to traverse
|
||||||
|
* @param types The feature types that will be skipped over
|
||||||
|
*/
|
||||||
|
export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]): unknown[] {
|
||||||
|
const objects: unknown[] = [];
|
||||||
|
const handleObject = (obj: Record<string, unknown>) => {
|
||||||
|
Object.keys(obj).forEach(key => {
|
||||||
|
const value = obj[key];
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
if (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
typeof (value as Record<string, any>).type == "symbol" &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
!types.includes((value as Record<string, any>).type)
|
||||||
|
) {
|
||||||
objects.push(value);
|
objects.push(value);
|
||||||
} else if (!(value instanceof Decimal) && !isRef(value)) {
|
} else if (!(value instanceof Decimal) && !isRef(value)) {
|
||||||
handleObject(value as Record<string, unknown>);
|
handleObject(value as Record<string, unknown>);
|
||||||
|
|
|
@ -12,9 +12,9 @@
|
||||||
@mousedown="start"
|
@mousedown="start"
|
||||||
@mouseleave="stop"
|
@mouseleave="stop"
|
||||||
@mouseup="stop"
|
@mouseup="stop"
|
||||||
@touchstart="start"
|
@touchstart.passive="start"
|
||||||
@touchend="stop"
|
@touchend.passive="stop"
|
||||||
@touchcancel="stop"
|
@touchcancel.passive="stop"
|
||||||
>
|
>
|
||||||
<div v-if="title"><component :is="titleComponent" /></div>
|
<div v-if="title"><component :is="titleComponent" /></div>
|
||||||
<component :is="component" style="white-space: pre-line" />
|
<component :is="component" style="white-space: pre-line" />
|
||||||
|
|
|
@ -43,9 +43,9 @@ function updateNodes() {
|
||||||
isDirty = false;
|
isDirty = false;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
boundingRect.value = resizeListener.value?.getBoundingClientRect();
|
boundingRect.value = resizeListener.value?.getBoundingClientRect();
|
||||||
(Object.values(nodes.value) as FeatureNode[]).forEach(
|
(Object.values(nodes.value) as FeatureNode[])
|
||||||
node => (node.rect = node.element.getBoundingClientRect())
|
.filter(n => n) // Sometimes the values become undefined
|
||||||
);
|
.forEach(node => (node.rect = node.element.getBoundingClientRect()));
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,16 @@ const validLinks = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.resize-listener,
|
.resize-listener {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
z-index: -10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
|
|
@ -115,7 +115,7 @@ export default defineComponent({
|
||||||
|
|
||||||
function gatherButtonProps(button: GenericTabButton) {
|
function gatherButtonProps(button: GenericTabButton) {
|
||||||
const { display, style, classes, glowColor, visibility } = button;
|
const { display, style, classes, glowColor, visibility } = button;
|
||||||
return { display, style, classes, glowColor, visibility };
|
return { display, style: unref(style), classes, glowColor, visibility };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
v-if="isShown"
|
v-if="isShown"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:class="{
|
:class="{
|
||||||
top: unref(direction) === TooltipDirection.UP,
|
top: unref(direction) === Direction.Up,
|
||||||
left: unref(direction) === TooltipDirection.LEFT,
|
left: unref(direction) === Direction.Left,
|
||||||
right: unref(direction) === TooltipDirection.RIGHT,
|
right: unref(direction) === Direction.Right,
|
||||||
bottom: unref(direction) === TooltipDirection.DOWN,
|
bottom: unref(direction) === Direction.Down,
|
||||||
...unref(classes)
|
...unref(classes)
|
||||||
}"
|
}"
|
||||||
:style="[
|
:style="[
|
||||||
|
@ -39,6 +39,7 @@ import themes from "data/themes";
|
||||||
import { CoercableComponent, jsx, StyleValue } from "features/feature";
|
import { CoercableComponent, jsx, StyleValue } from "features/feature";
|
||||||
import { Persistent } from "game/persistence";
|
import { Persistent } from "game/persistence";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
|
import { Direction } from "util/common";
|
||||||
import {
|
import {
|
||||||
coerceComponent,
|
coerceComponent,
|
||||||
computeOptionalComponent,
|
computeOptionalComponent,
|
||||||
|
@ -58,7 +59,6 @@ import {
|
||||||
unref,
|
unref,
|
||||||
watchEffect
|
watchEffect
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { TooltipDirection } from "./tooltip";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -69,7 +69,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
style: processedPropType<StyleValue>(Object, String, Array),
|
style: processedPropType<StyleValue>(Object, String, Array),
|
||||||
classes: processedPropType<Record<string, boolean>>(Object),
|
classes: processedPropType<Record<string, boolean>>(Object),
|
||||||
direction: processedPropType<TooltipDirection>(Number),
|
direction: processedPropType<Direction>(String),
|
||||||
xoffset: processedPropType<string>(String),
|
xoffset: processedPropType<string>(String),
|
||||||
yoffset: processedPropType<string>(String),
|
yoffset: processedPropType<string>(String),
|
||||||
pinned: Object as PropType<Persistent<boolean>>
|
pinned: Object as PropType<Persistent<boolean>>
|
||||||
|
@ -102,7 +102,7 @@ export default defineComponent({
|
||||||
const showPin = computed(() => unwrapRef(pinned) && themes[settings.theme].showPin);
|
const showPin = computed(() => unwrapRef(pinned) && themes[settings.theme].showPin);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
TooltipDirection,
|
Direction,
|
||||||
isHovered,
|
isHovered,
|
||||||
isShown,
|
isShown,
|
||||||
comp,
|
comp,
|
||||||
|
@ -120,6 +120,7 @@ export default defineComponent({
|
||||||
position: relative;
|
position: relative;
|
||||||
--xoffset: 0px;
|
--xoffset: 0px;
|
||||||
--yoffset: 0px;
|
--yoffset: 0px;
|
||||||
|
text-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip,
|
.tooltip,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
import { VueFeature } from "util/vue";
|
import { VueFeature } from "util/vue";
|
||||||
import { nextTick, Ref, unref } from "vue";
|
import { nextTick, Ref, unref } from "vue";
|
||||||
import { persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
|
import { Direction } from "util/common";
|
||||||
|
|
||||||
declare module "@vue/runtime-dom" {
|
declare module "@vue/runtime-dom" {
|
||||||
interface CSSProperties {
|
interface CSSProperties {
|
||||||
|
@ -25,19 +26,12 @@ declare module "@vue/runtime-dom" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TooltipDirection {
|
|
||||||
UP,
|
|
||||||
LEFT,
|
|
||||||
RIGHT,
|
|
||||||
DOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TooltipOptions {
|
export interface TooltipOptions {
|
||||||
pinnable?: boolean;
|
pinnable?: boolean;
|
||||||
display: Computable<CoercableComponent>;
|
display: Computable<CoercableComponent>;
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
direction?: Computable<TooltipDirection>;
|
direction?: Computable<Direction>;
|
||||||
xoffset?: Computable<string>;
|
xoffset?: Computable<string>;
|
||||||
yoffset?: Computable<string>;
|
yoffset?: Computable<string>;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +48,7 @@ export type Tooltip<T extends TooltipOptions> = Replace<
|
||||||
display: GetComputableType<T["display"]>;
|
display: GetComputableType<T["display"]>;
|
||||||
classes: GetComputableType<T["classes"]>;
|
classes: GetComputableType<T["classes"]>;
|
||||||
style: GetComputableType<T["style"]>;
|
style: GetComputableType<T["style"]>;
|
||||||
direction: GetComputableTypeWithDefault<T["direction"], TooltipDirection.UP>;
|
direction: GetComputableTypeWithDefault<T["direction"], Direction.Up>;
|
||||||
xoffset: GetComputableType<T["xoffset"]>;
|
xoffset: GetComputableType<T["xoffset"]>;
|
||||||
yoffset: GetComputableType<T["yoffset"]>;
|
yoffset: GetComputableType<T["yoffset"]>;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +59,7 @@ export type GenericTooltip = Replace<
|
||||||
{
|
{
|
||||||
pinnable: boolean;
|
pinnable: boolean;
|
||||||
pinned: Ref<boolean> | undefined;
|
pinned: Ref<boolean> | undefined;
|
||||||
direction: ProcessedComputable<TooltipDirection>;
|
direction: ProcessedComputable<Direction>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -77,7 +71,7 @@ export function addTooltip<T extends TooltipOptions>(
|
||||||
processComputable(options as T, "classes");
|
processComputable(options as T, "classes");
|
||||||
processComputable(options as T, "style");
|
processComputable(options as T, "style");
|
||||||
processComputable(options as T, "direction");
|
processComputable(options as T, "direction");
|
||||||
setDefault(options, "direction", TooltipDirection.UP);
|
setDefault(options, "direction", Direction.Up);
|
||||||
processComputable(options as T, "xoffset");
|
processComputable(options as T, "xoffset");
|
||||||
processComputable(options as T, "yoffset");
|
processComputable(options as T, "yoffset");
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
can: unref(canClick),
|
can: unref(canClick),
|
||||||
...unref(classes)
|
...unref(classes)
|
||||||
}"
|
}"
|
||||||
>
|
|
||||||
<div
|
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@mousedown="start"
|
@mousedown="start"
|
||||||
@mouseleave="stop"
|
@mouseleave="stop"
|
||||||
@mouseup="stop"
|
@mouseup="stop"
|
||||||
@touchstart="start"
|
@touchstart.passive="start"
|
||||||
@touchend="stop"
|
@touchend.passive="stop"
|
||||||
@touchcancel="stop"
|
@touchcancel.passive="stop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
:style="[
|
:style="[
|
||||||
{
|
{
|
||||||
backgroundColor: unref(color),
|
backgroundColor: unref(color),
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { createNanoEvents, Emitter } from "nanoevents";
|
import { createNanoEvents, Emitter } from "nanoevents";
|
||||||
import { InjectionKey, Ref, ref, unref } from "vue";
|
import { InjectionKey, Ref, ref, shallowReactive, unref } from "vue";
|
||||||
import { globalBus } from "./events";
|
import { globalBus } from "./events";
|
||||||
import { Persistent, persistent } from "./persistence";
|
import { Persistent, persistent } from "./persistence";
|
||||||
import player from "./player";
|
import player from "./player";
|
||||||
|
@ -44,7 +44,7 @@ export interface LayerEvents {
|
||||||
postUpdate: (diff: number) => void;
|
postUpdate: (diff: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const layers: Record<string, Readonly<GenericLayer> | undefined> = {};
|
export const layers: Record<string, Readonly<GenericLayer> | undefined> = shallowReactive({});
|
||||||
window.layers = layers;
|
window.layers = layers;
|
||||||
|
|
||||||
declare module "@vue/runtime-dom" {
|
declare module "@vue/runtime-dom" {
|
||||||
|
|
|
@ -1,30 +1,68 @@
|
||||||
|
import "components/common/modifiers.css";
|
||||||
import { CoercableComponent, jsx } from "features/feature";
|
import { CoercableComponent, jsx } from "features/feature";
|
||||||
import Decimal, { DecimalSource, format } from "util/bignum";
|
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||||
|
import { WithRequired } from "util/common";
|
||||||
import { Computable, convertComputable, ProcessedComputable } from "util/computed";
|
import { Computable, convertComputable, ProcessedComputable } from "util/computed";
|
||||||
import { renderJSX } from "util/vue";
|
import { renderJSX } from "util/vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
import "components/common/modifiers.css";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that can be used to apply or unapply some modification to a number.
|
||||||
|
* Being reversible requires the operation being invertible, but some features may rely on that.
|
||||||
|
* Descriptions can be optionally included for displaying them to the player.
|
||||||
|
* The built-in modifier creators are designed to display the modifiers using.
|
||||||
|
* {@link createModifierSection}.
|
||||||
|
*/
|
||||||
export interface Modifier {
|
export interface Modifier {
|
||||||
|
/** Applies some operation on the input and returns the result. */
|
||||||
apply: (gain: DecimalSource) => DecimalSource;
|
apply: (gain: DecimalSource) => DecimalSource;
|
||||||
revert: (gain: DecimalSource) => DecimalSource;
|
/** Reverses the operation applied by the apply property. Required by some features. */
|
||||||
enabled: ProcessedComputable<boolean>;
|
revert?: (gain: DecimalSource) => DecimalSource;
|
||||||
|
/**
|
||||||
|
* Whether or not this modifier should be considered enabled.
|
||||||
|
* Typically for use with modifiers passed into {@link createSequentialModifier}.
|
||||||
|
*/
|
||||||
|
enabled?: ProcessedComputable<boolean>;
|
||||||
|
/**
|
||||||
|
* A description of this modifier.
|
||||||
|
* @see {@link createModifierSection}.
|
||||||
|
*/
|
||||||
description?: ProcessedComputable<CoercableComponent>;
|
description?: ProcessedComputable<CoercableComponent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAdditiveModifier(
|
/**
|
||||||
addend: Computable<DecimalSource>,
|
* Utility type used to narrow down a modifier type that will have a description and/or enabled property based on optional parameters, T and S (respectively).
|
||||||
description?: Computable<CoercableComponent>,
|
*/
|
||||||
enabled?: Computable<boolean>
|
export type ModifierFromOptionalParams<T, S> = T extends undefined
|
||||||
): Modifier {
|
? S extends undefined
|
||||||
|
? Omit<WithRequired<Modifier, "revert">, "description" | "enabled">
|
||||||
|
: Omit<WithRequired<Modifier, "revert" | "enabled">, "description">
|
||||||
|
: S extends undefined
|
||||||
|
? Omit<WithRequired<Modifier, "revert" | "description">, "enabled">
|
||||||
|
: WithRequired<Modifier, "revert" | "enabled" | "description">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a modifier that adds some value to the input value.
|
||||||
|
* @param addend The amount to add to the input value.
|
||||||
|
* @param description Description of what this modifier is doing.
|
||||||
|
* @param enabled A computable that will be processed and passed directly into the returned modifier.
|
||||||
|
*/
|
||||||
|
export function createAdditiveModifier<
|
||||||
|
T extends Computable<CoercableComponent> | undefined,
|
||||||
|
S extends Computable<boolean> | undefined,
|
||||||
|
R = ModifierFromOptionalParams<T, S>
|
||||||
|
>(addend: Computable<DecimalSource>, description?: T, enabled?: S): R {
|
||||||
const processedAddend = convertComputable(addend);
|
const processedAddend = convertComputable(addend);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
const processedEnabled = convertComputable(enabled == null ? true : enabled);
|
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||||
return {
|
return {
|
||||||
apply: gain => Decimal.add(gain, unref(processedAddend)),
|
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
|
||||||
revert: gain => Decimal.sub(gain, unref(processedAddend)),
|
revert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description: jsx(() => (
|
description:
|
||||||
|
description == null
|
||||||
|
? undefined
|
||||||
|
: jsx(() => (
|
||||||
<div class="modifier-container">
|
<div class="modifier-container">
|
||||||
<span class="modifier-amount">+{format(unref(processedAddend))}</span>
|
<span class="modifier-amount">+{format(unref(processedAddend))}</span>
|
||||||
{unref(processedDescription) ? (
|
{unref(processedDescription) ? (
|
||||||
|
@ -35,22 +73,31 @@ export function createAdditiveModifier(
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
};
|
} as unknown as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMultiplicativeModifier(
|
/**
|
||||||
multiplier: Computable<DecimalSource>,
|
* Create a modifier that multiplies the input value by some value.
|
||||||
description?: Computable<CoercableComponent>,
|
* @param multiplier The value to multiply the input value by.
|
||||||
enabled?: Computable<boolean>
|
* @param description Description of what this modifier is doing.
|
||||||
): Modifier {
|
* @param enabled A computable that will be processed and passed directly into the returned modifier.
|
||||||
|
*/
|
||||||
|
export function createMultiplicativeModifier<
|
||||||
|
T extends Computable<CoercableComponent> | undefined,
|
||||||
|
S extends Computable<boolean> | undefined,
|
||||||
|
R = ModifierFromOptionalParams<T, S>
|
||||||
|
>(multiplier: Computable<DecimalSource>, description?: T, enabled?: S): R {
|
||||||
const processedMultiplier = convertComputable(multiplier);
|
const processedMultiplier = convertComputable(multiplier);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
const processedEnabled = convertComputable(enabled == null ? true : enabled);
|
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||||
return {
|
return {
|
||||||
apply: gain => Decimal.times(gain, unref(processedMultiplier)),
|
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
|
||||||
revert: gain => Decimal.div(gain, unref(processedMultiplier)),
|
revert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description: jsx(() => (
|
description:
|
||||||
|
description == null
|
||||||
|
? undefined
|
||||||
|
: jsx(() => (
|
||||||
<div class="modifier-container">
|
<div class="modifier-container">
|
||||||
<span class="modifier-amount">x{format(unref(processedMultiplier))}</span>
|
<span class="modifier-amount">x{format(unref(processedMultiplier))}</span>
|
||||||
{unref(processedDescription) ? (
|
{unref(processedDescription) ? (
|
||||||
|
@ -61,22 +108,31 @@ export function createMultiplicativeModifier(
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
};
|
} as unknown as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createExponentialModifier(
|
/**
|
||||||
exponent: Computable<DecimalSource>,
|
* Create a modifier that raises the input value to the power of some value.
|
||||||
description?: Computable<CoercableComponent>,
|
* @param exponent The value to raise the input value to the power of.
|
||||||
enabled?: Computable<boolean>
|
* @param description Description of what this modifier is doing.
|
||||||
): Modifier {
|
* @param enabled A computable that will be processed and passed directly into the returned modifier.
|
||||||
|
*/
|
||||||
|
export function createExponentialModifier<
|
||||||
|
T extends Computable<CoercableComponent> | undefined,
|
||||||
|
S extends Computable<boolean> | undefined,
|
||||||
|
R = ModifierFromOptionalParams<T, S>
|
||||||
|
>(exponent: Computable<DecimalSource>, description?: T, enabled?: S): R {
|
||||||
const processedExponent = convertComputable(exponent);
|
const processedExponent = convertComputable(exponent);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
const processedEnabled = convertComputable(enabled == null ? true : enabled);
|
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||||
return {
|
return {
|
||||||
apply: gain => Decimal.pow(gain, unref(processedExponent)),
|
apply: (gain: DecimalSource) => Decimal.pow(gain, unref(processedExponent)),
|
||||||
revert: gain => Decimal.root(gain, unref(processedExponent)),
|
revert: (gain: DecimalSource) => Decimal.root(gain, unref(processedExponent)),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description: jsx(() => (
|
description:
|
||||||
|
description == null
|
||||||
|
? undefined
|
||||||
|
: jsx(() => (
|
||||||
<div class="modifier-container">
|
<div class="modifier-container">
|
||||||
<span class="modifier-amount">^{format(unref(processedExponent))}</span>
|
<span class="modifier-amount">^{format(unref(processedExponent))}</span>
|
||||||
{unref(processedDescription) ? (
|
{unref(processedDescription) ? (
|
||||||
|
@ -87,39 +143,65 @@ export function createExponentialModifier(
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
};
|
} as unknown as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSequentialModifier(...modifiers: Modifier[]): Required<Modifier> {
|
/**
|
||||||
|
* Takes an array of modifiers and applies and reverses them in order.
|
||||||
|
* Modifiers that are not enabled will not be applied nor reversed.
|
||||||
|
* Also joins their descriptions together.
|
||||||
|
* @param modifiers The modifiers to perform sequentially.
|
||||||
|
* @see {@link createModifierSection}.
|
||||||
|
*/
|
||||||
|
export function createSequentialModifier<
|
||||||
|
T extends Modifier[],
|
||||||
|
S = T extends WithRequired<Modifier, "revert">[]
|
||||||
|
? WithRequired<Modifier, "description" | "revert">
|
||||||
|
: Omit<WithRequired<Modifier, "description">, "revert">
|
||||||
|
>(...modifiers: T): S {
|
||||||
return {
|
return {
|
||||||
apply: gain =>
|
apply: (gain: DecimalSource) =>
|
||||||
modifiers
|
modifiers
|
||||||
.filter(m => unref(m.enabled))
|
.filter(m => unref(m.enabled) !== false)
|
||||||
.reduce((gain, modifier) => modifier.apply(gain), gain),
|
.reduce((gain, modifier) => modifier.apply(gain), gain),
|
||||||
revert: gain =>
|
revert: modifiers.every(m => m.revert != null)
|
||||||
|
? (gain: DecimalSource) =>
|
||||||
modifiers
|
modifiers
|
||||||
.filter(m => unref(m.enabled))
|
.filter(m => unref(m.enabled) !== false)
|
||||||
.reduceRight((gain, modifier) => modifier.revert(gain), gain),
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
enabled: computed(() => modifiers.filter(m => unref(m.enabled)).length > 0),
|
.reduceRight((gain, modifier) => modifier.revert!(gain), gain)
|
||||||
|
: undefined,
|
||||||
|
enabled: computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0),
|
||||||
description: jsx(() => (
|
description: jsx(() => (
|
||||||
<>
|
<>
|
||||||
{(
|
{(
|
||||||
modifiers
|
modifiers
|
||||||
.filter(m => unref(m.enabled))
|
.filter(m => unref(m.enabled) !== false)
|
||||||
.map(m => unref(m.description))
|
.map(m => unref(m.description))
|
||||||
.filter(d => d) as CoercableComponent[]
|
.filter(d => d) as CoercableComponent[]
|
||||||
).map(renderJSX)}
|
).map(renderJSX)}
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
};
|
} as unknown as S;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JSX element that displays a modifier.
|
||||||
|
* Intended to be used with the output from {@link createSequentialModifier}.
|
||||||
|
* @param title The header for the section.
|
||||||
|
* @param subtitle Smaller text that appears in the header after the title.
|
||||||
|
* @param modifier The modifier to render.
|
||||||
|
* @param base The base value that'll be passed into the modifier.
|
||||||
|
* @param unit The unit of the value being modified, if any.
|
||||||
|
* @param baseText The label to use for the base value.
|
||||||
|
*/
|
||||||
export function createModifierSection(
|
export function createModifierSection(
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string,
|
subtitle: string,
|
||||||
modifier: Required<Modifier>,
|
modifier: WithRequired<Modifier, "description">,
|
||||||
base: DecimalSource = 1,
|
base: DecimalSource = 1,
|
||||||
unit = ""
|
unit = "",
|
||||||
|
baseText: CoercableComponent = "Base"
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -133,7 +215,7 @@ export function createModifierSection(
|
||||||
{format(base)}
|
{format(base)}
|
||||||
{unit}
|
{unit}
|
||||||
</span>
|
</span>
|
||||||
<span class="modifier-description">Base</span>
|
<span class="modifier-description">{renderJSX(baseText)}</span>
|
||||||
</div>
|
</div>
|
||||||
{renderJSX(unref(modifier.description))}
|
{renderJSX(unref(modifier.description))}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
@ -51,7 +51,11 @@ const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = target[ProxyState][key];
|
const value = target[ProxyState][key];
|
||||||
if (key !== "value" && isPlainObject(value) && !(value instanceof Decimal)) {
|
if (
|
||||||
|
key !== "value" &&
|
||||||
|
(isPlainObject(value) || Array.isArray(value)) &&
|
||||||
|
!(value instanceof Decimal)
|
||||||
|
) {
|
||||||
if (value !== target[key]?.[ProxyState]) {
|
if (value !== target[key]?.[ProxyState]) {
|
||||||
const path = [...target[ProxyPath], key];
|
const path = [...target[ProxyPath], key];
|
||||||
target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler);
|
target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler);
|
||||||
|
|
|
@ -25,19 +25,7 @@ const state = reactive<Partial<Settings>>({
|
||||||
watch(
|
watch(
|
||||||
state,
|
state,
|
||||||
state => {
|
state => {
|
||||||
let stringifiedSettings = JSON.stringify(state);
|
const stringifiedSettings = LZString.compressToUTF16(JSON.stringify(state));
|
||||||
switch (projInfo.saveEncoding) {
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
|
|
||||||
case "lz":
|
|
||||||
stringifiedSettings = LZString.compressToUTF16(stringifiedSettings);
|
|
||||||
break;
|
|
||||||
case "base64":
|
|
||||||
stringifiedSettings = btoa(unescape(encodeURIComponent(stringifiedSettings)));
|
|
||||||
break;
|
|
||||||
case "plain":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
localStorage.setItem(projInfo.id, stringifiedSettings);
|
localStorage.setItem(projInfo.id, stringifiedSettings);
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
|
|
1
src/lib/tsParticles.d.ts
vendored
1
src/lib/tsParticles.d.ts
vendored
|
@ -1 +0,0 @@
|
||||||
declare module "particles.vue3";
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||||
|
|
||||||
// Reference:
|
// Reference:
|
||||||
// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
|
// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
|
||||||
export function camelToTitle(camel: string): string {
|
export function camelToTitle(camel: string): string {
|
||||||
|
@ -14,3 +16,11 @@ export function isPlainObject(object: unknown): boolean {
|
||||||
export function isFunction(func: unknown): func is Function {
|
export function isFunction(func: unknown): func is Function {
|
||||||
return typeof func === "function";
|
return typeof func === "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Direction {
|
||||||
|
Up = "Up",
|
||||||
|
Down = "Down",
|
||||||
|
Left = "Left",
|
||||||
|
Right = "Right",
|
||||||
|
Default = "Up"
|
||||||
|
}
|
||||||
|
|
|
@ -25,19 +25,9 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function save(playerData?: PlayerData): string {
|
export function save(playerData?: PlayerData): string {
|
||||||
let stringifiedSave = stringifySave(playerData ?? player[ProxyState]);
|
const stringifiedSave = LZString.compressToUTF16(
|
||||||
switch (projInfo.saveEncoding) {
|
stringifySave(playerData ?? player[ProxyState])
|
||||||
default:
|
);
|
||||||
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
|
|
||||||
case "lz":
|
|
||||||
stringifiedSave = LZString.compressToUTF16(stringifiedSave);
|
|
||||||
break;
|
|
||||||
case "base64":
|
|
||||||
stringifiedSave = btoa(unescape(encodeURIComponent(stringifiedSave)));
|
|
||||||
break;
|
|
||||||
case "plain":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
|
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
|
||||||
return stringifiedSave;
|
return stringifiedSave;
|
||||||
}
|
}
|
||||||
|
@ -102,8 +92,10 @@ export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> {
|
||||||
const { fixOldSave, getInitialLayers } = await import("data/projEntry");
|
const { fixOldSave, getInitialLayers } = await import("data/projEntry");
|
||||||
|
|
||||||
for (const layer in layers) {
|
for (const layer in layers) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
const l = layers[layer];
|
||||||
removeLayer(layers[layer]!);
|
if (l) {
|
||||||
|
removeLayer(l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getInitialLayers(playerObj).forEach(layer => addLayer(layer, playerObj));
|
getInitialLayers(playerObj).forEach(layer => addLayer(layer, playerObj));
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue