Merge remote-tracking branch 'template/main'

This commit is contained in:
thepaperpilot 2022-05-23 22:47:06 -05:00
commit ff3eb932d6
34 changed files with 781 additions and 285 deletions

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,36 +8,48 @@
left left
v-if="save.error == undefined && !isConfirming" v-if="save.error == undefined && !isConfirming"
> >
<span class="material-icons">content_paste</span> <Tooltip display="Export" :direction="Direction.Left" class="info">
<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"
> >
<span class="material-icons">content_copy</span> <Tooltip display="Duplicate" :direction="Direction.Left" class="info">
<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"
> >
<span class="material-icons">edit</span> <Tooltip display="Edit Name" :direction="Direction.Left" class="info">
<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)"
> >
<span class="material-icons" style="margin: -2px">delete</span> <Tooltip display="Delete" :direction="Direction.Left" class="info">
<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">
<span class="material-icons">check</span> <Tooltip display="Save" :direction="Direction.Left" class="info">
<span class="material-icons">check</span>
</Tooltip>
</button> </button>
<button @click="isEditing = !isEditing" class="button"> <button @click="isEditing = !isEditing" class="button">
<span class="material-icons">close</span> <Tooltip display="Cancel" :direction="Direction.Left" class="info">
<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";

View file

@ -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,21 +191,21 @@ 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) { }
default: switch (projInfo.exportEncoding) {
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`); default:
case "lz": console.warn(`Unknown save encoding: ${projInfo.exportEncoding}. Defaulting to lz`);
saveToExport = LZString.compressToUTF16(saveToExport); case "lz":
break; saveToExport = LZString.compressToUTF16(saveToExport);
case "base64": break;
saveToExport = btoa(unescape(encodeURIComponent(saveToExport))); case "base64":
break; saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
case "plain": break;
break; case "plain":
} 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
@ -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) {

View file

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

View file

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

View file

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

View file

@ -19,5 +19,5 @@
"maxTickLength": 3600, "maxTickLength": 3600,
"offlineLimit": 1, "offlineLimit": 1,
"enablePausing": true, "enablePausing": true,
"saveEncoding": "lz" "exportEncoding": "base64"
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -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,20 +144,25 @@ 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 =
const genericBuyable = buyable as GenericBuyable; buyable.onClick ??
if ( buyable.purchase ??
!unref(genericBuyable.canPurchase) || function (this: GenericBuyable) {
genericBuyable.cost == null || const genericBuyable = buyable as GenericBuyable;
genericBuyable.resource == null if (!unref(genericBuyable.canPurchase)) {
) { return;
return; }
} const cost = unref(genericBuyable.cost);
const cost = unref(genericBuyable.cost); if (genericBuyable.cost != null && genericBuyable.resource != null) {
genericBuyable.resource.value = Decimal.sub(genericBuyable.resource.value, cost); genericBuyable.resource.value = Decimal.sub(
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1); genericBuyable.resource.value,
this.onPurchase?.(cost); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
}; cost!
);
genericBuyable.amount.value = Decimal.add(genericBuyable.amount.value, 1);
}
this.onPurchase?.(cost);
};
processComputable(buyable as T, "display"); processComputable(buyable as T, "display");
const display = buyable.display; const display = buyable.display;
buyable.display = jsx(() => { buyable.display = jsx(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,15 +7,15 @@
can: unref(canClick), can: unref(canClick),
...unref(classes) ...unref(classes)
}" }"
@click="onClick"
@mousedown="start"
@mouseleave="stop"
@mouseup="stop"
@touchstart.passive="start"
@touchend.passive="stop"
@touchcancel.passive="stop"
> >
<div <div
@click="onClick"
@mousedown="start"
@mouseleave="stop"
@mouseup="stop"
@touchstart="start"
@touchend="stop"
@touchcancel="stop"
:style="[ :style="[
{ {
backgroundColor: unref(color), backgroundColor: unref(color),

View file

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

View file

@ -1,125 +1,207 @@
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:
<div class="modifier-container"> description == null
<span class="modifier-amount">+{format(unref(processedAddend))}</span> ? undefined
{unref(processedDescription) ? ( : jsx(() => (
<span class="modifier-description"> <div class="modifier-container">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <span class="modifier-amount">+{format(unref(processedAddend))}</span>
{renderJSX(unref(processedDescription)!)} {unref(processedDescription) ? (
</span> <span class="modifier-description">
) : null} {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
</div> {renderJSX(unref(processedDescription)!)}
)) </span>
}; ) : null}
</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:
<div class="modifier-container"> description == null
<span class="modifier-amount">x{format(unref(processedMultiplier))}</span> ? undefined
{unref(processedDescription) ? ( : jsx(() => (
<span class="modifier-description"> <div class="modifier-container">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <span class="modifier-amount">x{format(unref(processedMultiplier))}</span>
{renderJSX(unref(processedDescription)!)} {unref(processedDescription) ? (
</span> <span class="modifier-description">
) : null} {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
</div> {renderJSX(unref(processedDescription)!)}
)) </span>
}; ) : null}
</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:
<div class="modifier-container"> description == null
<span class="modifier-amount">^{format(unref(processedExponent))}</span> ? undefined
{unref(processedDescription) ? ( : jsx(() => (
<span class="modifier-description"> <div class="modifier-container">
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} <span class="modifier-amount">^{format(unref(processedExponent))}</span>
{renderJSX(unref(processedDescription)!)} {unref(processedDescription) ? (
</span> <span class="modifier-description">
) : null} {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
</div> {renderJSX(unref(processedDescription)!)}
)) </span>
}; ) : null}
</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)
modifiers ? (gain: DecimalSource) =>
.filter(m => unref(m.enabled)) modifiers
.reduceRight((gain, modifier) => modifier.revert(gain), gain), .filter(m => unref(m.enabled) !== false)
enabled: computed(() => modifiers.filter(m => unref(m.enabled)).length > 0), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.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 />

View file

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

View file

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

View file

@ -1 +0,0 @@
declare module "particles.vue3";

View file

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

View file

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