Merge remote-tracking branch 'template/main'

This commit is contained in:
thepaperpilot 2022-04-24 21:02:58 -05:00
commit 44901754f4
28 changed files with 339 additions and 145 deletions

View file

@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.3.3] - 2022-04-24
### Fixed
- Spacing between rows in Tree components
- Computed style attributes on tooltips were ignored
- Tooltips could cause infinite loops due to cyclical dependencies
## [0.3.2] - 2022-04-23
### Fixed
- Clickables and several other elements would not register clicks sometimes, if the display is updating rapidly
- createLayerTreeNode wasn't using display option correctly
## [0.3.1] - 2022-04-23
### Added
- Render utility methods that always return JSX Elements
### Changed
- **BREAKING** Tooltips overhaul
- Tree Nodes no longer have tooltips related properties
- Tooltips can now be added to any feature with a Vue component using the `addTooltip` function
- Any tooltip can be made pinnable by setting pinnable to true in the addTooltip options, or by passing a `Ref<boolean>` to a Tooltip component
- Pinned tooltips have an icon to represent that. It can be disabled by setting the theme's `showPin` property to false
- Modifiers are now their own features rather than a part of conversions
- Including utilities to display the current state of all the modifiers
- TabFamilies' options function is now optional
- Layer.minWidth can take string values
- If parseable into a number, it'll have "px" appended. Otherwise it'll be un-processed
- TreeNodes now have Vue components attached to them
- `createResourceTooltip` now shows the resource name
- Made classic and aqua theme's `feature-foreground` color dark rather than light
## [0.3.0] - 2022-04-10 ## [0.3.0] - 2022-04-10
### Added ### Added
- conversion.currentAt [#4](https://github.com/profectus-engine/Profectus/pull/4) - conversion.currentAt [#4](https://github.com/profectus-engine/Profectus/pull/4)

31
package-lock.json generated
View file

@ -1,16 +1,17 @@
{ {
"name": "profectus", "name": "profectus",
"version": "0.3.0", "version": "0.3.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "profectus", "name": "profectus",
"version": "0.3.0", "version": "0.3.3",
"dependencies": { "dependencies": {
"@pixi/particle-emitter": "^5.0.4", "@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"pixi.js": "^6.3.0", "pixi.js": "^6.3.0",
"vue": "^3.2.26", "vue": "^3.2.26",
@ -26,6 +27,7 @@
"@jetblack/operator-overloading": "^0.2.0", "@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0", "@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1", "@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3", "@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3", "@vue/cli-plugin-eslint": "^5.0.3",
@ -2461,6 +2463,12 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"node_modules/@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -8592,6 +8600,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -15010,6 +15026,12 @@
"@types/lodash": "*" "@types/lodash": "*"
} }
}, },
"@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"@types/mime": { "@types/mime": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -19594,6 +19616,11 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
},
"magic-string": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "profectus", "name": "profectus",
"version": "0.3.0", "version": "0.3.3",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "vue-cli-service serve", "start": "vue-cli-service serve",
@ -12,6 +12,7 @@
"@pixi/particle-emitter": "^5.0.4", "@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"pixi.js": "^6.3.0", "pixi.js": "^6.3.0",
"vue": "^3.2.26", "vue": "^3.2.26",
@ -27,6 +28,7 @@
"@jetblack/operator-overloading": "^0.2.0", "@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0", "@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1", "@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3", "@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3", "@vue/cli-plugin-eslint": "^5.0.3",

View file

@ -57,6 +57,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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 } from "game/player";
import settings from "game/settings"; import settings from "game/settings";
@ -66,6 +67,7 @@ import Select from "./fields/Select.vue";
import Text from "./fields/Text.vue"; import Text from "./fields/Text.vue";
import Save from "./Save.vue"; import Save from "./Save.vue";
import Draggable from "vuedraggable"; import Draggable from "vuedraggable";
import LZString from "lz-string";
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown }; export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
@ -81,21 +83,32 @@ defineExpose({
const importingFailed = ref(false); const importingFailed = ref(false);
const saveToImport = ref(""); const saveToImport = ref("");
watch(saveToImport, save => { watch(saveToImport, importedSave => {
if (save) { if (importedSave) {
nextTick(() => { nextTick(() => {
try { try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(save)))); if (importedSave[0] === "{") {
// plaintext. No processing needed
} else if (importedSave[0] === "e") {
// Assumed to be base64, which starts with e
importedSave = decodeURIComponent(escape(atob(importedSave)));
} else if (importedSave[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
importedSave = LZString.decompressFromUTF16(importedSave)!;
} else {
console.warn("Unable to determine preset encoding", importedSave);
importingFailed.value = true;
return;
}
const playerData = JSON.parse(importedSave);
if (typeof playerData !== "object") { if (typeof playerData !== "object") {
importingFailed.value = true; importingFailed.value = true;
return; return;
} }
const id = getUniqueID(); const id = getUniqueID();
playerData.id = id; playerData.id = id;
localStorage.setItem( save(playerData);
id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
saveToImport.value = ""; saveToImport.value = "";
importingFailed.value = false; importingFailed.value = false;
@ -124,14 +137,30 @@ let bank = ref(
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({}); const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
function getCachedSave(id: string) { function getCachedSave(id: string) {
if (cachedSaves[id] == null) { if (cachedSaves[id] == null) {
const save = localStorage.getItem(id); let save = localStorage.getItem(id);
if (save == null) { if (save == null) {
cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id }; cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
} else if (save === "dW5kZWZpbmVk") { } else if (save === "dW5kZWZpbmVk") {
cachedSaves[id] = { error: `Save is undefined`, id }; cachedSaves[id] = { error: `Save is undefined`, id };
} else { } else {
try { try {
cachedSaves[id] = { ...JSON.parse(decodeURIComponent(escape(atob(save)))), id }; if (save[0] === "{") {
// plaintext. No processing needed
} else if (save[0] === "e") {
// Assumed to be base64, which starts with e
save = decodeURIComponent(escape(atob(save)));
} else if (save[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
save = LZString.decompressFromUTF16(save)!;
} else {
console.warn("Unable to determine preset encoding", save);
importingFailed.value = true;
cachedSaves[id] = { error: "Unable to determine preset encoding", id };
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cachedSaves[id]!;
}
cachedSaves[id] = { ...JSON.parse(save), id };
} catch (error) { } catch (error) {
cachedSaves[id] = { error, id }; cachedSaves[id] = { error, id };
console.warn( console.warn(
@ -162,7 +191,19 @@ function exportSave(id: string) {
if (player.id === id) { if (player.id === id) {
saveToExport = save(); saveToExport = save();
} else { } else {
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id])))); saveToExport = JSON.stringify(saves.value[id]);
switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
saveToExport = LZString.compressToUTF16(saveToExport);
break;
case "base64":
saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
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
@ -181,10 +222,7 @@ function duplicateSave(id: string) {
} }
const playerData = { ...saves.value[id], id: getUniqueID() }; const playerData = { ...saves.value[id], id: getUniqueID() };
localStorage.setItem( save(playerData as PlayerData);
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
} }
@ -207,12 +245,22 @@ function openSave(id: string) {
} }
function newFromPreset(preset: string) { function newFromPreset(preset: string) {
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset)))); if (preset[0] === "{") {
// plaintext. No processing needed
} else if (preset[0] === "e") {
// Assumed to be base64, which starts with e
preset = decodeURIComponent(escape(atob(preset)));
} else if (preset[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
preset = LZString.decompressFromUTF16(preset)!;
} else {
console.warn("Unable to determine preset encoding", preset);
return;
}
const playerData = JSON.parse(preset);
playerData.id = getUniqueID(); playerData.id = getUniqueID();
localStorage.setItem( save(playerData as PlayerData);
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
settings.saves.push(playerData.id); settings.saves.push(playerData.id);
} }
@ -225,7 +273,7 @@ function editSave(id: string, newName: string) {
player.name = newName; player.name = newName;
save(); save();
} else { } else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave))))); save(currSave as PlayerData);
cachedSaves[id] = undefined; cachedSaves[id] = undefined;
} }
} }

View file

@ -70,7 +70,7 @@ function onUpdate(value: SelectOption) {
} }
.vue-dropdown-item { .vue-dropdown-item {
color: var(--feature-foreground); color: var(--foreground);
} }
.vue-dropdown-item, .vue-dropdown-item,

View file

@ -133,7 +133,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
export interface LayerTreeNodeOptions extends TreeNodeOptions { export interface LayerTreeNodeOptions extends TreeNodeOptions {
layerID: string; layerID: string;
color: Computable<string>; // marking as required color: Computable<string>; // marking as required
display?: Computable<string>; display?: Computable<CoercableComponent>;
append?: Computable<boolean>; append?: Computable<boolean>;
} }
export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace< export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
@ -146,7 +146,7 @@ export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
export type GenericLayerTreeNode = Replace< export type GenericLayerTreeNode = Replace<
LayerTreeNode<LayerTreeNodeOptions>, LayerTreeNode<LayerTreeNodeOptions>,
{ {
display: ProcessedComputable<string>; display: ProcessedComputable<CoercableComponent>;
append?: ProcessedComputable<boolean>; append?: ProcessedComputable<boolean>;
} }
>; >;
@ -161,7 +161,7 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
processComputable(options as T, "append"); processComputable(options as T, "append");
return { return {
...options, ...options,
display: options.layerID, display: options.display,
onClick: unref((options as unknown as GenericLayerTreeNode).append) onClick: unref((options as unknown as GenericLayerTreeNode).append)
? function () { ? function () {
if (player.tabs.includes(options.layerID)) { if (player.tabs.includes(options.layerID)) {

View file

@ -8,6 +8,8 @@ import { jsx } from "features/feature";
import { createReset } from "features/reset"; import { createReset } from "features/reset";
import MainDisplay from "features/resources/MainDisplay.vue"; import MainDisplay from "features/resources/MainDisplay.vue";
import { createResource } from "features/resources/resource"; import { createResource } from "features/resources/resource";
import { addTooltip } from "features/tooltips/tooltip";
import { createResourceTooltip } from "features/trees/tree";
import { createLayer } from "game/layers"; import { createLayer } from "game/layers";
import { DecimalSource } from "util/bignum"; import { DecimalSource } from "util/bignum";
import { render } from "util/vue"; import { render } from "util/vue";
@ -35,6 +37,10 @@ const layer = createLayer(id, () => {
color, color,
reset reset
})); }));
addTooltip(treeNode, {
display: createResourceTooltip(points),
pinnable: true
});
const resetButton = createResetButton(() => ({ const resetButton = createResetButton(() => ({
conversion, conversion,

View file

@ -18,5 +18,6 @@
"maxTickLength": 3600, "maxTickLength": 3600,
"offlineLimit": 1, "offlineLimit": 1,
"enablePausing": true "enablePausing": true,
"saveEncoding": "lz"
} }

View file

@ -35,7 +35,7 @@ const defaultTheme: Theme = {
variables: { variables: {
"--foreground": "#dfdfdf", "--foreground": "#dfdfdf",
"--background": "#0f0f0f", "--background": "#0f0f0f",
"--feature-foreground": "#eee", "--feature-foreground": "#0f0f0f",
"--tooltip-background": "rgba(0, 0, 0, 0.75)", "--tooltip-background": "rgba(0, 0, 0, 0.75)",
"--raised-background": "#0f0f0f", "--raised-background": "#0f0f0f",
"--points": "#ffffff", "--points": "#ffffff",

View file

@ -11,7 +11,7 @@ import {
Visibility Visibility
} from "features/feature"; } from "features/feature";
import "game/notifications"; import "game/notifications";
import { Persistent, PersistentState, persistent } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -21,7 +21,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { coerceComponent } from "util/vue"; import { coerceComponent } from "util/vue";
import { Ref, unref, watchEffect } from "vue"; import { unref, watchEffect } from "vue";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
const toast = useToast(); const toast = useToast();
@ -39,9 +39,9 @@ export interface AchievementOptions {
onComplete?: VoidFunction; onComplete?: VoidFunction;
} }
export interface BaseAchievement extends Persistent<boolean> { export interface BaseAchievement {
id: string; id: string;
earned: Ref<boolean>; earned: Persistent<boolean>;
complete: VoidFunction; complete: VoidFunction;
type: typeof AchievementType; type: typeof AchievementType;
[Component]: typeof AchievementComponent; [Component]: typeof AchievementComponent;
@ -68,17 +68,18 @@ export type GenericAchievement = Replace<
>; >;
export function createAchievement<T extends AchievementOptions>( export function createAchievement<T extends AchievementOptions>(
optionsFunc: OptionsFunc<T, Achievement<T>, BaseAchievement> optionsFunc?: OptionsFunc<T, Achievement<T>, BaseAchievement>
): Achievement<T> { ): Achievement<T> {
return createLazyProxy(persistent => { const earned = persistent<boolean>(false);
const achievement = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
achievement.id = getUniqueID("achievement-"); achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType; achievement.type = AchievementType;
achievement[Component] = AchievementComponent; achievement[Component] = AchievementComponent;
achievement.earned = achievement[PersistentState]; achievement.earned = earned;
achievement.complete = function () { achievement.complete = function () {
achievement[PersistentState].value = true; earned.value = true;
}; };
processComputable(achievement as T, "visibility"); processComputable(achievement as T, "visibility");
@ -122,5 +123,5 @@ export function createAchievement<T extends AchievementOptions>(
} }
return achievement as unknown as Achievement<T>; return achievement as unknown as Achievement<T>;
}, persistent<boolean>(false)); });
} }

View file

@ -1,6 +1,6 @@
import ClickableComponent from "features/clickables/Clickable.vue"; import ClickableComponent from "features/clickables/Clickable.vue";
import { Resource } from "features/resources/resource"; import { Resource } from "features/resources/resource";
import { Persistent, PersistentState, persistent } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import { import {
Computable, Computable,
@ -49,9 +49,9 @@ export interface BuyableOptions {
onPurchase?: (cost: DecimalSource) => void; onPurchase?: (cost: DecimalSource) => void;
} }
export interface BaseBuyable extends Persistent<DecimalSource> { export interface BaseBuyable {
id: string; id: string;
amount: Ref<DecimalSource>; amount: Persistent<DecimalSource>;
maxed: Ref<boolean>; maxed: Ref<boolean>;
canAfford: Ref<boolean>; canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>; canClick: ProcessedComputable<boolean>;
@ -90,8 +90,9 @@ export type GenericBuyable = Replace<
export function createBuyable<T extends BuyableOptions>( export function createBuyable<T extends BuyableOptions>(
optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable> optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable>
): Buyable<T> { ): Buyable<T> {
return createLazyProxy(persistent => { const amount = persistent<DecimalSource>(0);
const buyable = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const buyable = optionsFunc();
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) { if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn( console.warn(
@ -105,7 +106,7 @@ export function createBuyable<T extends BuyableOptions>(
buyable.type = BuyableType; buyable.type = BuyableType;
buyable[Component] = ClickableComponent; buyable[Component] = ClickableComponent;
buyable.amount = buyable[PersistentState]; buyable.amount = amount;
buyable.canAfford = computed(() => { buyable.canAfford = computed(() => {
const genericBuyable = buyable as GenericBuyable; const genericBuyable = buyable as GenericBuyable;
const cost = unref(genericBuyable.cost); const cost = unref(genericBuyable.cost);
@ -239,5 +240,5 @@ export function createBuyable<T extends BuyableOptions>(
}; };
return buyable as unknown as Buyable<T>; return buyable as unknown as Buyable<T>;
}, persistent<DecimalSource>(0)); });
} }

View file

@ -138,4 +138,8 @@ export default defineComponent({
.clickable.small { .clickable.small {
min-height: unset; min-height: unset;
} }
.clickable > * {
pointer-events: none;
}
</style> </style>

View file

@ -70,10 +70,10 @@ export type GenericClickable = Replace<
>; >;
export function createClickable<T extends ClickableOptions>( export function createClickable<T extends ClickableOptions>(
optionsFunc: OptionsFunc<T, Clickable<T>, BaseClickable> optionsFunc?: OptionsFunc<T, Clickable<T>, BaseClickable>
): Clickable<T> { ): Clickable<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const clickable = optionsFunc(); const clickable = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
clickable.id = getUniqueID("clickable-"); clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType; clickable.type = ClickableType;
clickable[Component] = ClickableComponent; clickable[Component] = ClickableComponent;

View file

@ -87,4 +87,8 @@ export default defineComponent({
font-size: 10px; font-size: 10px;
background-color: var(--layer-color); background-color: var(--layer-color);
} }
.tile > * {
pointer-events: none;
}
</style> </style>

View file

@ -20,7 +20,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { State, Persistent, PersistentState, persistent } from "game/persistence"; import { State, Persistent, persistent } from "game/persistence";
export const GridType = Symbol("Grid"); export const GridType = Symbol("Grid");
@ -205,12 +205,13 @@ export interface GridOptions {
onHold?: (id: string | number, state: State) => void; onHold?: (id: string | number, state: State) => void;
} }
export interface BaseGrid extends Persistent<Record<string | number, State>> { export interface BaseGrid {
id: string; id: string;
getID: (id: string | number, state: State) => string; getID: (id: string | number, state: State) => string;
getState: (id: string | number) => State; getState: (id: string | number) => State;
setState: (id: string | number, state: State) => void; setState: (id: string | number, state: State) => void;
cells: Record<string | number, GridCell>; cells: Record<string | number, GridCell>;
cellState: Persistent<Record<string | number, State>>;
type: typeof GridType; type: typeof GridType;
[Component]: typeof GridComponent; [Component]: typeof GridComponent;
[GatherProps]: () => Record<string, unknown>; [GatherProps]: () => Record<string, unknown>;
@ -244,22 +245,25 @@ export type GenericGrid = Replace<
export function createGrid<T extends GridOptions>( export function createGrid<T extends GridOptions>(
optionsFunc: OptionsFunc<T, Grid<T>, BaseGrid> optionsFunc: OptionsFunc<T, Grid<T>, BaseGrid>
): Grid<T> { ): Grid<T> {
return createLazyProxy(persistent => { const cellState = persistent<Record<string | number, State>>({});
const grid = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const grid = optionsFunc();
grid.id = getUniqueID("grid-"); grid.id = getUniqueID("grid-");
grid[Component] = GridComponent; grid[Component] = GridComponent;
grid.cellState = cellState;
grid.getID = function (this: GenericGrid, cell: string | number) { grid.getID = function (this: GenericGrid, cell: string | number) {
return grid.id + "-" + cell; return grid.id + "-" + cell;
}; };
grid.getState = function (this: GenericGrid, cell: string | number) { grid.getState = function (this: GenericGrid, cell: string | number) {
if (this[PersistentState].value[cell] != undefined) { if (this.cellState.value[cell] != undefined) {
return this[PersistentState].value[cell]; return cellState.value[cell];
} }
return this.cells[cell].startState; return this.cells[cell].startState;
}; };
grid.setState = function (this: GenericGrid, cell: string | number, state: State) { grid.setState = function (this: GenericGrid, cell: string | number, state: State) {
this[PersistentState].value[cell] = state; cellState.value[cell] = state;
}; };
grid.cells = createGridProxy(grid as GenericGrid); grid.cells = createGridProxy(grid as GenericGrid);
@ -301,5 +305,5 @@ export function createGrid<T extends GridOptions>(
}; };
return grid as unknown as Grid<T>; return grid as unknown as Grid<T>;
}, persistent({})); });
} }

View file

@ -1,15 +1,16 @@
import InfoboxComponent from "features/infoboxes/Infobox.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
OptionsFunc,
Replace, Replace,
setDefault, setDefault,
StyleValue, StyleValue,
Visibility Visibility
} from "features/feature"; } from "features/feature";
import InfoboxComponent from "features/infoboxes/Infobox.vue";
import { Persistent, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -18,8 +19,7 @@ import {
ProcessedComputable ProcessedComputable
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Ref, unref } from "vue"; import { unref } from "vue";
import { Persistent, PersistentState, persistent } from "game/persistence";
export const InfoboxType = Symbol("Infobox"); export const InfoboxType = Symbol("Infobox");
@ -34,9 +34,9 @@ export interface InfoboxOptions {
display: Computable<CoercableComponent>; display: Computable<CoercableComponent>;
} }
export interface BaseInfobox extends Persistent<boolean> { export interface BaseInfobox {
id: string; id: string;
collapsed: Ref<boolean>; collapsed: Persistent<boolean>;
type: typeof InfoboxType; type: typeof InfoboxType;
[Component]: typeof InfoboxComponent; [Component]: typeof InfoboxComponent;
[GatherProps]: () => Record<string, unknown>; [GatherProps]: () => Record<string, unknown>;
@ -66,13 +66,14 @@ export type GenericInfobox = Replace<
export function createInfobox<T extends InfoboxOptions>( export function createInfobox<T extends InfoboxOptions>(
optionsFunc: OptionsFunc<T, Infobox<T>, BaseInfobox> optionsFunc: OptionsFunc<T, Infobox<T>, BaseInfobox>
): Infobox<T> { ): Infobox<T> {
return createLazyProxy(persistent => { const collapsed = persistent<boolean>(false);
const infobox = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const infobox = optionsFunc();
infobox.id = getUniqueID("infobox-"); infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType; infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent; infobox[Component] = InfoboxComponent;
infobox.collapsed = infobox[PersistentState]; infobox.collapsed = collapsed;
processComputable(infobox as T, "visibility"); processComputable(infobox as T, "visibility");
setDefault(infobox, "visibility", Visibility.Visible); setDefault(infobox, "visibility", Visibility.Visible);
@ -112,5 +113,5 @@ export function createInfobox<T extends InfoboxOptions>(
}; };
return infobox as unknown as Infobox<T>; return infobox as unknown as Infobox<T>;
}, persistent<boolean>(false)); });
} }

View file

@ -20,7 +20,7 @@ export interface Link extends SVGAttributes {
} }
export interface LinksOptions { export interface LinksOptions {
links?: Computable<Link[]>; links: Computable<Link[]>;
} }
export interface BaseLinks { export interface BaseLinks {

View file

@ -14,7 +14,7 @@ import {
import MilestoneComponent from "features/milestones/Milestone.vue"; import MilestoneComponent from "features/milestones/Milestone.vue";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import "game/notifications"; import "game/notifications";
import { persistent, Persistent, PersistentState } from "game/persistence"; import { persistent, Persistent } from "game/persistence";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common"; import { camelToTitle } from "util/common";
import { import {
@ -26,7 +26,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { coerceComponent, isCoercableComponent } from "util/vue"; import { coerceComponent, isCoercableComponent } from "util/vue";
import { computed, Ref, unref, watchEffect } from "vue"; import { computed, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
const toast = useToast(); const toast = useToast();
@ -57,9 +57,9 @@ export interface MilestoneOptions {
onComplete?: VoidFunction; onComplete?: VoidFunction;
} }
export interface BaseMilestone extends Persistent<boolean> { export interface BaseMilestone {
id: string; id: string;
earned: Ref<boolean>; earned: Persistent<boolean>;
complete: VoidFunction; complete: VoidFunction;
type: typeof MilestoneType; type: typeof MilestoneType;
[Component]: typeof MilestoneComponent; [Component]: typeof MilestoneComponent;
@ -84,17 +84,18 @@ export type GenericMilestone = Replace<
>; >;
export function createMilestone<T extends MilestoneOptions>( export function createMilestone<T extends MilestoneOptions>(
optionsFunc: OptionsFunc<T, Milestone<T>, BaseMilestone> optionsFunc?: OptionsFunc<T, Milestone<T>, BaseMilestone>
): Milestone<T> { ): Milestone<T> {
return createLazyProxy(persistent => { const earned = persistent<boolean>(false);
const milestone = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
milestone.id = getUniqueID("milestone-"); milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType; milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent; milestone[Component] = MilestoneComponent;
milestone.earned = milestone[PersistentState]; milestone.earned = earned;
milestone.complete = function () { milestone.complete = function () {
milestone[PersistentState].value = true; earned.value = true;
}; };
processComputable(milestone as T, "visibility"); processComputable(milestone as T, "visibility");
@ -168,7 +169,7 @@ export function createMilestone<T extends MilestoneOptions>(
} }
return milestone as unknown as Milestone<T>; return milestone as unknown as Milestone<T>;
}, persistent<boolean>(false)); });
} }
declare module "game/settings" { declare module "game/settings" {

View file

@ -42,10 +42,10 @@ export type Particles<T extends ParticlesOptions> = Replace<
export type GenericParticles = Particles<ParticlesOptions>; export type GenericParticles = Particles<ParticlesOptions>;
export function createParticles<T extends ParticlesOptions>( export function createParticles<T extends ParticlesOptions>(
optionsFunc: OptionsFunc<T, Particles<T>, BaseParticles> optionsFunc?: OptionsFunc<T, Particles<T>, BaseParticles>
): Particles<T> { ): Particles<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const particles = optionsFunc(); const particles = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
particles.id = getUniqueID("particles-"); particles.id = getUniqueID("particles-");
particles.type = ParticlesType; particles.type = ParticlesType;
particles[Component] = ParticlesComponent; particles[Component] = ParticlesComponent;

View file

@ -106,4 +106,8 @@ export default defineComponent({
:not(.floating) .tabButton:not(.active) { :not(.floating) .tabButton:not(.active) {
border-bottom-color: transparent; border-bottom-color: transparent;
} }
.tabButton > * {
pointer-events: none;
}
</style> </style>

View file

@ -11,7 +11,7 @@ import {
} from "features/feature"; } from "features/feature";
import TabButtonComponent from "features/tabs/TabButton.vue"; import TabButtonComponent from "features/tabs/TabButton.vue";
import TabFamilyComponent from "features/tabs/TabFamily.vue"; import TabFamilyComponent from "features/tabs/TabFamily.vue";
import { Persistent, PersistentState, persistent } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -65,11 +65,11 @@ export interface TabFamilyOptions {
style?: Computable<StyleValue>; style?: Computable<StyleValue>;
} }
export interface BaseTabFamily extends Persistent<string> { export interface BaseTabFamily {
id: string; id: string;
tabs: Record<string, TabButtonOptions>; tabs: Record<string, TabButtonOptions>;
activeTab: Ref<GenericTab | CoercableComponent | null>; activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>; selected: Persistent<string>;
type: typeof TabFamilyType; type: typeof TabFamilyType;
[Component]: typeof TabFamilyComponent; [Component]: typeof TabFamilyComponent;
[GatherProps]: () => Record<string, unknown>; [GatherProps]: () => Record<string, unknown>;
@ -99,8 +99,9 @@ export function createTabFamily<T extends TabFamilyOptions>(
throw "Cannot create tab family with 0 tabs"; throw "Cannot create tab family with 0 tabs";
} }
return createLazyProxy(persistent => { const selected = persistent(Object.keys(tabs)[0]);
const tabFamily = Object.assign(persistent, optionsFunc?.()); return createLazyProxy(() => {
const tabFamily = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
tabFamily.id = getUniqueID("tabFamily-"); tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType; tabFamily.type = TabFamilyType;
@ -124,15 +125,14 @@ export function createTabFamily<T extends TabFamilyOptions>(
}, },
{} {}
); );
tabFamily.selected = tabFamily[PersistentState]; tabFamily.selected = selected;
tabFamily.activeTab = computed(() => { tabFamily.activeTab = computed(() => {
const tabs = unref(processedTabFamily.tabs); const tabs = unref(processedTabFamily.tabs);
if ( if (
tabFamily[PersistentState].value in tabs && selected.value in tabs &&
unref(tabs[processedTabFamily[PersistentState].value].visibility) === unref(tabs[selected.value].visibility) === Visibility.Visible
Visibility.Visible
) { ) {
return unref(tabs[processedTabFamily[PersistentState].value].tab); return unref(tabs[selected.value].tab);
} }
const firstTab = Object.values(tabs).find( const firstTab = Object.values(tabs).find(
tab => unref(tab.visibility) === Visibility.Visible tab => unref(tab.visibility) === Visibility.Visible
@ -156,5 +156,5 @@ export function createTabFamily<T extends TabFamilyOptions>(
// This is necessary because board.types is different from T and TabFamily // This is necessary because board.types is different from T and TabFamily
const processedTabFamily = tabFamily as unknown as TabFamily<T>; const processedTabFamily = tabFamily as unknown as TabFamily<T>;
return processedTabFamily; return processedTabFamily;
}, persistent(Object.keys(tabs)[0])); });
} }

View file

@ -15,7 +15,7 @@ import {
ProcessedComputable ProcessedComputable
} from "util/computed"; } from "util/computed";
import { VueFeature } from "util/vue"; import { VueFeature } from "util/vue";
import { Ref } from "vue"; import { nextTick, Ref, unref } from "vue";
import { persistent } from "game/persistence"; import { persistent } from "game/persistence";
declare module "@vue/runtime-dom" { declare module "@vue/runtime-dom" {
@ -73,6 +73,15 @@ export function addTooltip<T extends TooltipOptions>(
element: VueFeature, element: VueFeature,
options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip> options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
): Tooltip<T> { ): Tooltip<T> {
processComputable(options as T, "display");
processComputable(options as T, "classes");
processComputable(options as T, "style");
processComputable(options as T, "direction");
setDefault(options, "direction", TooltipDirection.UP);
processComputable(options as T, "xoffset");
processComputable(options as T, "yoffset");
nextTick(() => {
if (options.pinnable) { if (options.pinnable) {
if ("pinned" in element) { if ("pinned" in element) {
console.error( console.error(
@ -85,14 +94,6 @@ export function addTooltip<T extends TooltipOptions>(
} }
} }
processComputable(options as T, "display");
processComputable(options as T, "classes");
processComputable(options as T, "style");
processComputable(options as T, "direction");
setDefault(options, "direction", TooltipDirection.UP);
processComputable(options as T, "xoffset");
processComputable(options as T, "yoffset");
const elementComponent = element[Component]; const elementComponent = element[Component];
element[Component] = TooltipComponent; element[Component] = TooltipComponent;
const elementGratherProps = element[GatherProps].bind(element); const elementGratherProps = element[GatherProps].bind(element);
@ -105,13 +106,14 @@ export function addTooltip<T extends TooltipOptions>(
}, },
display, display,
classes, classes,
style, style: unref(style),
direction, direction,
xoffset, xoffset,
yoffset, yoffset,
pinned pinned
}; };
}.bind(options as GenericTooltip); }.bind(options as GenericTooltip);
});
return options as unknown as Tooltip<T>; return options as unknown as Tooltip<T>;
} }

View file

@ -112,4 +112,8 @@ export default defineComponent({
text-transform: capitalize; text-transform: capitalize;
display: flex; display: flex;
} }
.treeNode > *:first-child > * {
pointer-events: none;
}
</style> </style>

View file

@ -73,10 +73,10 @@ export type GenericTreeNode = Replace<
>; >;
export function createTreeNode<T extends TreeNodeOptions>( export function createTreeNode<T extends TreeNodeOptions>(
optionsFunc: OptionsFunc<T, TreeNode<T>, BaseTreeNode> optionsFunc?: OptionsFunc<T, TreeNode<T>, BaseTreeNode>
): TreeNode<T> { ): TreeNode<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const treeNode = optionsFunc(); const treeNode = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
treeNode.id = getUniqueID("treeNode-"); treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType; treeNode.type = TreeNodeType;
treeNode[Component] = TreeNodeComponent; treeNode[Component] = TreeNodeComponent;

View file

@ -141,4 +141,8 @@ export default defineComponent({
width: 120px; width: 120px;
font-size: 10px; font-size: 10px;
} }
.upgrade > * {
pointer-events: none;
}
</style> </style>

View file

@ -24,7 +24,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { persistent, Persistent, PersistentState } from "game/persistence"; import { persistent, Persistent } from "game/persistence";
export const UpgradeType = Symbol("Upgrade"); export const UpgradeType = Symbol("Upgrade");
@ -47,9 +47,9 @@ export interface UpgradeOptions {
onPurchase?: VoidFunction; onPurchase?: VoidFunction;
} }
export interface BaseUpgrade extends Persistent<boolean> { export interface BaseUpgrade {
id: string; id: string;
bought: Ref<boolean>; bought: Persistent<boolean>;
canPurchase: Ref<boolean>; canPurchase: Ref<boolean>;
purchase: VoidFunction; purchase: VoidFunction;
type: typeof UpgradeType; type: typeof UpgradeType;
@ -81,8 +81,9 @@ export type GenericUpgrade = Replace<
export function createUpgrade<T extends UpgradeOptions>( export function createUpgrade<T extends UpgradeOptions>(
optionsFunc: OptionsFunc<T, Upgrade<T>, BaseUpgrade> optionsFunc: OptionsFunc<T, Upgrade<T>, BaseUpgrade>
): Upgrade<T> { ): Upgrade<T> {
return createLazyProxy(persistent => { const bought = persistent<boolean>(false);
const upgrade = Object.assign(persistent, optionsFunc()); return createLazyProxy(() => {
const upgrade = optionsFunc();
upgrade.id = getUniqueID("upgrade-"); upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType; upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent; upgrade[Component] = UpgradeComponent;
@ -94,7 +95,7 @@ export function createUpgrade<T extends UpgradeOptions>(
); );
} }
upgrade.bought = upgrade[PersistentState]; upgrade.bought = bought;
if (upgrade.canAfford == null) { if (upgrade.canAfford == null) {
upgrade.canAfford = computed(() => { upgrade.canAfford = computed(() => {
const genericUpgrade = upgrade as GenericUpgrade; const genericUpgrade = upgrade as GenericUpgrade;
@ -124,7 +125,7 @@ export function createUpgrade<T extends UpgradeOptions>(
unref(genericUpgrade.cost) unref(genericUpgrade.cost)
); );
} }
genericUpgrade[PersistentState].value = true; bought.value = true;
genericUpgrade.onPurchase?.(); genericUpgrade.onPurchase?.();
}; };
@ -167,7 +168,7 @@ export function createUpgrade<T extends UpgradeOptions>(
}; };
return upgrade as unknown as Upgrade<T>; return upgrade as unknown as Upgrade<T>;
}, persistent<boolean>(false)); });
} }
export function setupAutoPurchase( export function setupAutoPurchase(

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import { Themes } from "data/themes"; import { Themes } from "data/themes";
import { CoercableComponent } from "features/feature"; import { CoercableComponent } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import LZString from "lz-string";
import { hardReset } from "util/save"; import { hardReset } from "util/save";
import { reactive, watch } from "vue"; import { reactive, watch } from "vue";
@ -23,20 +24,44 @@ const state = reactive<Partial<Settings>>({
watch( watch(
state, state,
state => state => {
localStorage.setItem( let stringifiedSettings = JSON.stringify(state);
projInfo.id, switch (projInfo.saveEncoding) {
btoa(unescape(encodeURIComponent(JSON.stringify(state)))) 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);
},
{ deep: true } { deep: true }
); );
export default window.settings = state as Settings; export default window.settings = state as Settings;
export function loadSettings(): void { export function loadSettings(): void {
try { try {
const item: string | null = localStorage.getItem(projInfo.id); let item: string | null = localStorage.getItem(projInfo.id);
if (item != null && item !== "") { if (item != null && item !== "") {
const settings = JSON.parse(decodeURIComponent(escape(atob(item)))); if (item[0] === "{") {
// plaintext. No processing needed
} else if (item[0] === "e") {
// Assumed to be base64, which starts with e
item = decodeURIComponent(escape(atob(item)));
} else if (item[0] === "ᯡ") {
// Assumed to be lz, which starts with ᯡ
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
item = LZString.decompressFromUTF16(item)!;
} else {
console.warn("Unable to determine settings encoding", item);
return;
}
const settings = JSON.parse(item);
if (typeof settings === "object") { if (typeof settings === "object") {
Object.assign(state, settings); Object.assign(state, settings);
} }

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import player, { Player, PlayerData, stringifySave } from "game/player"; import player, { Player, PlayerData, stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings"; import settings, { loadSettings } from "game/settings";
import { ProxyState } from "./proxies"; import { ProxyState } from "./proxies";
import LZString from "lz-string";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player { export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
return Object.assign( return Object.assign(
@ -23,9 +24,21 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
) as Player; ) as Player;
} }
export function save(): string { export function save(playerData?: PlayerData): string {
const stringifiedSave = btoa(unescape(encodeURIComponent(stringifySave(player[ProxyState])))); let stringifiedSave = stringifySave(playerData ?? player[ProxyState]);
localStorage.setItem(player.id, stringifiedSave); switch (projInfo.saveEncoding) {
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);
return stringifiedSave; return stringifiedSave;
} }
@ -34,12 +47,24 @@ export async function load(): Promise<void> {
loadSettings(); loadSettings();
try { try {
const save = localStorage.getItem(settings.active); let save = localStorage.getItem(settings.active);
if (save == null) { if (save == null) {
await loadSave(newSave()); await loadSave(newSave());
return; return;
} }
const player = JSON.parse(decodeURIComponent(escape(atob(save)))); if (save[0] === "{") {
// plaintext. No processing needed
} else if (save[0] === "e") {
// Assumed to be base64, which starts with e
save = decodeURIComponent(escape(atob(save)));
} else if (save[0] === "ᯡ") {
// Assumed to be lz, which starts with ᯡ
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
save = LZString.decompressFromUTF16(save)!;
} else {
throw `Unable to determine save encoding`;
}
const player = JSON.parse(save);
if (player.modID !== projInfo.id) { if (player.modID !== projInfo.id) {
await loadSave(newSave()); await loadSave(newSave());
return; return;
@ -55,7 +80,7 @@ export async function load(): Promise<void> {
export function newSave(): PlayerData { export function newSave(): PlayerData {
const id = getUniqueID(); const id = getUniqueID();
const player = setupInitialStore({ id }); const player = setupInitialStore({ id });
localStorage.setItem(id, btoa(unescape(encodeURIComponent(stringifySave(player))))); save(player);
settings.saves.push(id); settings.saves.push(id);