diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml
index 7c48ad6..33df8d8 100644
--- a/.forgejo/workflows/test.yaml
+++ b/.forgejo/workflows/test.yaml
@@ -19,4 +19,3 @@ jobs:
- run: npm ci
- run: npm run build --if-present
- run: npm test
- - run: npm run lint
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8d6b548..c41d085 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -19,4 +19,3 @@ jobs:
- run: npm ci
- run: npm run build --if-present
- run: npm test
- - run: npm run lint
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 65fe597..d46602a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,7 +1,7 @@
{
"vitest.commandLine": "npx vitest",
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": "explicit"
+ "source.fixAll.eslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"git.ignoreLimitWarning": true,
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 4fc4ea1..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Contributing to Profectus
-
-Thank you for considering contributing to Profectus! We appreciate your interest in improving our project. Please take a moment to review the following guidelines to streamline the contribution process.
-
-## Getting Started
-
-For detailed instructions on setting up local development environment, please refer to the [Setup Guide](https://moddingtree.com/guide/getting-started/setup).
-
-## Issue Reporting
-
-If you encounter a bug or have a suggestion for improvement, please open an issue on Incremental Social. Provide as much detail as possible, including an example repo or steps to reproduce the issue if applicable.
-
-## Contributing
-
-Make sure to open your PR on [Incremental Social](https://code.incremental.social/profectus/Profectus) - the GitHub repo is just a mirror!
-
-### Code Review
-
-All PRs must be reviewed and approved by at least one of the project maintainers before merging. Please be patient during the review process and be open to feedback.
-
-### Testing
-
-Ensure that your changes pass all existing tests and, if applicable, add new tests to cover the changes you've made. Run `npm run test` to run all the tests.
-
-### Code Style
-
-We use ESLint and Prettier to enforce consistent code style throughout the project. Before submitting a PR, run `npm run lint:fix` to automatically fix any linting issues.
-
-## License
-
-By contributing to Profectus, you agree that your contributions will be licensed under the project's [LICENSE](./LICENSE).
diff --git a/package-lock.json b/package-lock.json
index d3c50e2..f747006 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,6 @@
"is-plain-object": "^5.0.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2",
- "unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
"vite": "^2.9.12",
"vite-plugin-pwa": "^0.12.0",
"vite-tsconfig-paths": "^3.5.0",
@@ -6879,10 +6878,6 @@
"node": ">= 4.0.0"
}
},
- "node_modules/unofficial-galaxy-sdk": {
- "version": "1.0",
- "resolved": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#97d6da6636a2fc38c14aa893d4b336ccc22314af"
- },
"node_modules/upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
diff --git a/package.json b/package.json
index eeeecf0..3c1c415 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,7 @@
"preview": "vite preview",
"test": "vitest run",
"testw": "vitest",
- "serve": "vite preview --host",
- "lint": "eslint src --max-warnings 0",
- "lint:fix": "eslint --fix --max-warnings 0 src"
+ "serve": "vite preview --host"
},
"dependencies": {
"@fontsource/material-icons": "^4.5.4",
@@ -29,7 +27,6 @@
"is-plain-object": "^5.0.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2",
- "unofficial-galaxy-sdk": "git+https://code.incremental.social/thepaperpilot/unofficial-galaxy-sdk.git#1.0.1",
"vite": "^2.9.12",
"vite-plugin-pwa": "^0.12.0",
"vite-tsconfig-paths": "^3.5.0",
diff --git a/src/App.vue b/src/App.vue
index 7e7a0aa..6a365ef 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,7 +8,6 @@
-
@@ -17,11 +16,10 @@
diff --git a/src/components/saves/Save.vue b/src/components/Save.vue
similarity index 75%
rename from src/components/saves/Save.vue
rename to src/components/Save.vue
index 8c1809b..77d2988 100644
--- a/src/components/saves/Save.vue
+++ b/src/components/Save.vue
@@ -1,7 +1,7 @@
-
- Not all saves are synced! You may need to delete stale saves.
-
, "id"> & { id: string; error?: unknown };
@@ -103,8 +90,16 @@ watch(saveToImport, importedSave => {
if (importedSave) {
nextTick(() => {
try {
- importedSave = decodeSave(importedSave) ?? "";
- if (importedSave === "") {
+ 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;
@@ -144,10 +139,48 @@ let bank = ref(
}, [])
);
+const cachedSaves = shallowReactive>({});
+function getCachedSave(id: string) {
+ if (cachedSaves[id] == null) {
+ let save = localStorage.getItem(id);
+ if (save == null) {
+ cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
+ } else if (save === "dW5kZWZpbmVk") {
+ cachedSaves[id] = { error: `Save is undefined`, id };
+ } else {
+ try {
+ 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) {
+ cachedSaves[id] = { error, id };
+ console.warn(
+ `SavesManager: Failed to load info about save with id ${id}:\n${error}\n${save}`
+ );
+ }
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return cachedSaves[id]!;
+}
// Wipe cache whenever the modal is opened
watch(isOpen, isOpen => {
if (isOpen) {
- clearCachedSaves();
+ Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
}
});
@@ -158,10 +191,6 @@ const saves = computed(() =>
}, {})
);
-const showNotSyncedWarning = computed(
- () => galaxy.value?.loggedIn === true && settings.saves.length < syncedSaves.value.length
-);
-
function exportSave(id: string) {
let saveToExport;
if (player.id === id) {
@@ -204,37 +233,20 @@ function duplicateSave(id: string) {
}
function deleteSave(id: string) {
- if (galaxy.value?.loggedIn === true) {
- galaxy.value.getSaveList().then(list => {
- const slot = Object.keys(list).find(slot => {
- const content = list[slot as unknown as number].content;
- try {
- if (JSON.parse(content).id === id) {
- return true;
- }
- } catch (e) {
- return false;
- }
- });
- if (slot != null) {
- galaxy.value?.save(parseInt(slot), "", "").catch(console.error);
- }
- });
- }
settings.saves = settings.saves.filter((save: string) => save !== id);
localStorage.removeItem(id);
- clearCachedSave(id);
+ cachedSaves[id] = undefined;
}
function openSave(id: string) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
saves.value[player.id]!.time = player.time;
save();
- clearCachedSave(player.id);
+ cachedSaves[player.id] = undefined;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadSave(saves.value[id]!);
// Delete cached version in case of opening it again
- clearCachedSave(id);
+ cachedSaves[id] = undefined;
}
function newFromPreset(preset: string) {
@@ -244,8 +256,16 @@ function newFromPreset(preset: string) {
selectedPreset.value = null;
});
- preset = decodeSave(preset) ?? "";
- if (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;
}
@@ -267,7 +287,7 @@ function editSave(id: string, newName: string) {
save();
} else {
save(currSave as Player);
- clearCachedSave(id);
+ cachedSaves[id] = undefined;
}
}
}
diff --git a/src/components/saves/CloudSaveResolver.vue b/src/components/saves/CloudSaveResolver.vue
deleted file mode 100644
index 9a2b823..0000000
--- a/src/components/saves/CloudSaveResolver.vue
+++ /dev/null
@@ -1,228 +0,0 @@
-
-
-
-
-
Cloud {{ pluralizedSave }} loaded!
-
-
-
-
- Upon loading, your cloud {{ pluralizedSave }}
- {{ conflictingSaves.length > 1 ? "appear" : "appears" }} to be out of sync with your
- local {{ pluralizedSave }}. Which
- {{ pluralizedSave }}
- do you want to keep?
-
: null}
{render(tree)}
>
diff --git a/src/features/action.tsx b/src/features/action.tsx
index 2919a9e..1fbb8d3 100644
--- a/src/features/action.tsx
+++ b/src/features/action.tsx
@@ -31,7 +31,7 @@ import { coerceComponent, isCoercableComponent, render } from "util/vue";
import { computed, Ref, ref, unref } from "vue";
import { BarOptions, createBar, GenericBar } from "./bars/bar";
import { ClickableOptions } from "./clickables/clickable";
-import { GenericDecorator } from "./decorators/common";
+import { Decorator, GenericDecorator } from "./decorators/common";
/** A symbol used to identify {@link Action} features. */
export const ActionType = Symbol("Action");
diff --git a/src/features/hotkey.tsx b/src/features/hotkey.tsx
index 80eebbd..51fafbb 100644
--- a/src/features/hotkey.tsx
+++ b/src/features/hotkey.tsx
@@ -108,7 +108,7 @@ document.onkeydown = function (e) {
if (e.ctrlKey) {
key = "ctrl+" + key;
}
- const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
+ const hotkey = hotkeys[key];
if (hotkey && unref(hotkey.enabled)) {
e.preventDefault();
hotkey.onPress();
diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts
index 8d65efd..54d782c 100644
--- a/src/features/tooltips/tooltip.ts
+++ b/src/features/tooltips/tooltip.ts
@@ -1,6 +1,6 @@
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
import { Component, GatherProps, setDefault } from "features/feature";
-import { persistent } from "game/persistence";
+import { deletePersistent, Persistent, persistent } from "game/persistence";
import { Direction } from "util/common";
import type {
Computable,
diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts
index 5a03189..da77f60 100644
--- a/src/features/trees/tree.ts
+++ b/src/features/trees/tree.ts
@@ -1,4 +1,4 @@
-import { GenericDecorator } from "features/decorators/common";
+import { Decorator, GenericDecorator } from "features/decorators/common";
import type {
CoercableComponent,
GenericComponent,
@@ -224,7 +224,7 @@ export interface BaseTree {
id: string;
/** The link objects for each of the branches of the tree. */
links: Ref;
- /** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */
+ /** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
reset: (node: GenericTreeNode) => void;
/** A flag that is true while the reset is still propagating through the tree. */
isResetting: Ref;
@@ -338,21 +338,34 @@ export const branchedResetPropagation = function (
tree: GenericTree,
resettingNode: GenericTreeNode
): void {
- const links = unref(tree.branches);
- if (links == null) return;
- const reset: GenericTreeNode[] = [];
- let current = [resettingNode];
- while (current.length != 0) {
- const next: GenericTreeNode[] = [];
- for (const node of current) {
- for (const link of links.filter(link => link.startNode === node)) {
- if ([...reset, ...current].includes(link.endNode)) continue;
- next.push(link.endNode);
- link.endNode.reset?.reset();
- }
+ const visitedNodes = [resettingNode];
+ let currentNodes = [resettingNode];
+ if (tree.branches != null) {
+ const branches = unref(tree.branches);
+ while (currentNodes.length > 0) {
+ const nextNodes: GenericTreeNode[] = [];
+ currentNodes.forEach(node => {
+ branches
+ .filter(branch => branch.startNode === node || branch.endNode === node)
+ .map(branch => {
+ if (branch.startNode === node) {
+ return branch.endNode;
+ }
+ return branch.startNode;
+ })
+ .filter(node => !visitedNodes.includes(node))
+ .forEach(node => {
+ // Check here instead of in the filter because this check's results may
+ // change as we go through each node
+ if (!nextNodes.includes(node)) {
+ nextNodes.push(node);
+ node.reset?.reset();
+ }
+ });
+ });
+ currentNodes = nextNodes;
+ visitedNodes.push(...currentNodes);
}
- reset.push(...current);
- current = next;
}
};
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index 7210cfb..586319e 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -552,9 +552,7 @@ export function tetrate(
export function invertTetrate(
value: DecimalSource,
base: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
height: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
payload: FormulaSource
) {
if (hasVariable(base)) {
@@ -578,7 +576,6 @@ export function invertIteratedExp(
value: DecimalSource,
lhs: FormulaSource,
height: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
payload: FormulaSource
) {
if (hasVariable(lhs)) {
@@ -629,7 +626,6 @@ export function invertLayeradd(
value: DecimalSource,
lhs: FormulaSource,
diff: FormulaSource,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
base: FormulaSource
) {
if (hasVariable(lhs)) {
diff --git a/src/game/modifiers.tsx b/src/game/modifiers.tsx
index 55efccb..b65e7fc 100644
--- a/src/game/modifiers.tsx
+++ b/src/game/modifiers.tsx
@@ -4,7 +4,7 @@ import { jsx } from "features/feature";
import settings from "game/settings";
import type { DecimalSource } from "util/bignum";
import Decimal, { formatSmall } from "util/bignum";
-import type { RequiredKeys, WithRequired } from "util/common";
+import type { WithRequired } from "util/common";
import type { Computable, ProcessedComputable } from "util/computed";
import { convertComputable } from "util/computed";
import { createLazyProxy } from "util/proxies";
@@ -38,11 +38,16 @@ export interface Modifier {
description?: ProcessedComputable;
}
-/** Utility type that represents the output of all modifiers that represent a single operation. */
-export type OperationModifier = WithRequired<
- Modifier,
- "invert" | "getFormula" | Extract, keyof Modifier>
->;
+/**
+ * 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).
+ */
+export type ModifierFromOptionalParams = undefined extends T
+ ? undefined extends S
+ ? Omit, "description" | "enabled">
+ : Omit, "description">
+ : undefined extends S
+ ? Omit, "enabled">
+ : WithRequired;
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
export interface AdditiveModifierOptions {
@@ -60,9 +65,9 @@ export interface AdditiveModifierOptions {
* Create a modifier that adds some value to the input value.
* @param optionsFunc Additive modifier options.
*/
-export function createAdditiveModifier>(
+export function createAdditiveModifier(
optionsFunc: OptionsFunc
-) {
+): ModifierFromOptionalParams {
return createLazyProxy(feature => {
const { addend, description, enabled, smallerIsBetter } = optionsFunc.call(
feature,
@@ -106,7 +111,7 @@ export function createAdditiveModifier
))
};
- }) as S;
+ }) as unknown as ModifierFromOptionalParams;
}
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
@@ -125,10 +130,9 @@ export interface MultiplicativeModifierOptions {
* Create a modifier that multiplies the input value by some value.
* @param optionsFunc Multiplicative modifier options.
*/
-export function createMultiplicativeModifier<
- T extends MultiplicativeModifierOptions,
- S = OperationModifier
->(optionsFunc: OptionsFunc) {
+export function createMultiplicativeModifier(
+ optionsFunc: OptionsFunc
+): ModifierFromOptionalParams {
return createLazyProxy(feature => {
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call(
feature,
@@ -171,7 +175,7 @@ export function createMultiplicativeModifier<
))
};
- }) as S;
+ }) as unknown as ModifierFromOptionalParams;
}
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */
@@ -192,10 +196,9 @@ export interface ExponentialModifierOptions {
* Create a modifier that raises the input value to the power of some value.
* @param optionsFunc Exponential modifier options.
*/
-export function createExponentialModifier<
- T extends ExponentialModifierOptions,
- S = OperationModifier
->(optionsFunc: OptionsFunc) {
+export function createExponentialModifier(
+ optionsFunc: OptionsFunc
+): ModifierFromOptionalParams {
return createLazyProxy(feature => {
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
optionsFunc.call(feature, feature);
@@ -260,7 +263,7 @@ export function createExponentialModifier<
))
};
- }) as S;
+ }) as unknown as ModifierFromOptionalParams;
}
/**
@@ -271,9 +274,11 @@ export function createExponentialModifier<
* @see {@link createModifierSection}.
*/
export function createSequentialModifier<
- T extends Modifier,
- S = WithRequired, keyof Modifier>>
->(modifiersFunc: () => T[]) {
+ T extends Modifier[],
+ S = T extends WithRequired[]
+ ? WithRequired
+ : Omit, "invert">
+>(modifiersFunc: () => T): S {
return createLazyProxy(() => {
const modifiers = modifiersFunc();
@@ -291,14 +296,10 @@ export function createSequentialModifier<
: undefined,
getFormula: modifiers.every(m => m.getFormula != null)
? (gain: FormulaSource) =>
- modifiers.reduce((acc, curr) => {
- if (curr.enabled == null || curr.enabled === true) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return curr.getFormula!(acc);
- }
+ modifiers
+ .filter(m => unref(m.enabled) !== false)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return Formula.if(acc, curr.enabled, acc => curr.getFormula!(acc));
- }, gain)
+ .reduce((acc, curr) => curr.getFormula!(acc), gain)
: undefined,
enabled: modifiers.some(m => m.enabled != null)
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
@@ -316,7 +317,7 @@ export function createSequentialModifier<
))
: undefined
};
- }) as S;
+ }) as unknown as S;
}
/** An object that configures a modifier section via {@link createModifierSection}. */
diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 363fccd..ea82a64 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -222,9 +222,7 @@ export function createCostRequirement(
Decimal.gte(
req.resource.value,
unref(req.cost as ProcessedComputable)
- )
- ? 1
- : 0
+ ) ? 1 : 0
);
}
diff --git a/src/game/routing.ts b/src/game/routing.ts
new file mode 100644
index 0000000..693c1d6
--- /dev/null
+++ b/src/game/routing.ts
@@ -0,0 +1,68 @@
+import { globalBus } from "game/events";
+import { DecimalSource } from "util/bignum";
+import { Ref } from "vue";
+import player from "./player";
+
+// https://stackoverflow.com/questions/2090551/parse-query-string-in-javascript
+function parseQuery(queryString = window.location.search) {
+ const query: Record = {};
+ const pairs = (queryString[0] === "?" ? queryString.substring(1) : queryString).split("&");
+ for (let i = 0; i < pairs.length; i++) {
+ const pair = pairs[i].split("=");
+ query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
+ }
+ return query;
+}
+const params = parseQuery();
+
+/**
+ * Register a handler to be called when creating new saves based on a query param
+ * @param key The query param to regster
+ * @param handler The callback function when the query param is present
+ * @param newSavesOnly If set to true, only call the handler on the /new path
+ */
+export function registerQueryParam(
+ key: string,
+ handler: (value: string) => void,
+ newSavesOnly?: boolean
+): void;
+/**
+ * Register a ref to have its value set based on a query param
+ * @param key The query param to regster
+ * @param ref The ref to set the value of
+ * @param newSavesOnly If set to true, only overwrite values on the /new path
+ * @see {@link numberHandler}.
+ */
+export function registerQueryParam(
+ key: string,
+ ref: Ref,
+ newSavesOnly?: boolean
+): void;
+export function registerQueryParam(
+ key: string,
+ handlerOrRef: ((value: string) => void) | Ref,
+ newSavesOnly = false
+) {
+ globalBus.on("onLoad", () => {
+ if (newSavesOnly && player.timePlayed > 0) {
+ return;
+ }
+ if (key in params) {
+ if (typeof handlerOrRef === "function") {
+ handlerOrRef(params[key]);
+ } else {
+ if (typeof handlerOrRef.value === "boolean") {
+ (handlerOrRef.value as boolean) = params[key].toLowerCase() === "true";
+ } else {
+ (handlerOrRef.value as string | DecimalSource) = params[key];
+ }
+ }
+ }
+ });
+}
+
+export function numberHandler(ref: Ref) {
+ return function (value: string) {
+ ref.value = parseFloat(value);
+ };
+}
diff --git a/src/game/settings.ts b/src/game/settings.ts
index 6f3a435..0748d68 100644
--- a/src/game/settings.ts
+++ b/src/game/settings.ts
@@ -3,7 +3,7 @@ import { Themes } from "data/themes";
import type { CoercableComponent } from "features/feature";
import { globalBus } from "game/events";
import LZString from "lz-string";
-import { decodeSave, hardReset } from "util/save";
+import { hardReset } from "util/save";
import { reactive, watch } from "vue";
/** The player's settings object. */
@@ -78,8 +78,16 @@ export function loadSettings(): void {
try {
let item: string | null = localStorage.getItem(projInfo.id);
if (item != null && item !== "") {
- item = decodeSave(item);
- if (item == null) {
+ 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;
}
diff --git a/src/main.css b/src/main.css
index f84ba5f..60188bd 100644
--- a/src/main.css
+++ b/src/main.css
@@ -66,7 +66,3 @@ ul {
.Vue-Toastification__toast {
margin: unset;
}
-
-:disabled {
- pointer-events: none;
-}
diff --git a/src/main.ts b/src/main.ts
index 3b5de9f..1e24deb 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,12 +3,12 @@ import App from "App.vue";
import projInfo from "data/projInfo.json";
import "game/notifications";
import state from "game/state";
-import { load } from "util/save";
+import { loadSettings } from "game/settings";
+import { load, loadSave, newSave } from "util/save";
import { useRegisterSW } from "virtual:pwa-register/vue";
import type { App as VueApp } from "vue";
import { createApp, nextTick } from "vue";
import { useToast } from "vue-toastification";
-import "util/galaxy";
declare global {
/**
@@ -60,7 +60,17 @@ requestAnimationFrame(async () => {
"font-weight: bold; font-size: 24px; color: #A3BE8C; background: #2E3440; padding: 4px 8px; border-radius: 8px;",
"padding: 4px;"
);
- await load();
+
+ // Load global settings
+ loadSettings();
+
+ if (window.location.pathname === "/new") {
+ await loadSave(newSave());
+ } else {
+ await load();
+ }
+ window.history.replaceState({}, document.title, "/");
+
const { globalBus } = await import("./game/events");
const { startGameLoop } = await import("./game/gameLoop");
diff --git a/src/util/common.ts b/src/util/common.ts
index 00847e6..dbbe233 100644
--- a/src/util/common.ts
+++ b/src/util/common.ts
@@ -1,11 +1,3 @@
-export type RequiredKeys = {
- [K in keyof T]-?: NonNullable extends Pick ? never : K;
-}[keyof T];
-export type OptionalKeys = {
- [K in keyof T]-?: NonNullable extends Pick ? K : never;
-}[keyof T];
-
-export type OmitOptional = Pick>;
export type WithRequired = T & { [P in K]-?: T[P] };
export type ArrayElements> = T extends ReadonlyArray
diff --git a/src/util/galaxy.ts b/src/util/galaxy.ts
deleted file mode 100644
index 384ed3d..0000000
--- a/src/util/galaxy.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-import { LoadablePlayerData } from "components/saves/SavesManager.vue";
-import player, { Player, stringifySave } from "game/player";
-import settings from "game/settings";
-import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
-import LZString from "lz-string";
-import { ref } from "vue";
-import { decodeSave, loadSave, save, setupInitialStore } from "./save";
-
-export const galaxy = ref();
-export const conflictingSaves = ref<
- { id: string; local: LoadablePlayerData; cloud: LoadablePlayerData; slot: number }[]
->([]);
-export const syncedSaves = ref([]);
-
-export function sync() {
- if (galaxy.value?.loggedIn !== true) {
- return;
- }
- if (conflictingSaves.value.length > 0) {
- // Pause syncing while resolving conflicted saves
- return;
- }
- galaxy.value
- .getSaveList()
- .then(syncSaves)
- .then(list => {
- syncedSaves.value = list.map(s => s.content.id);
- })
- .catch(console.error);
-}
-
-// Setup Galaxy API
-initGalaxy({
- supportsSaving: true,
- supportsSaveManager: true,
- onLoggedInChanged
-})
- .then(g => {
- galaxy.value = g;
- onLoggedInChanged(g);
- })
- .catch(console.error);
-
-function onLoggedInChanged(g: GalaxyApi) {
- if (g.loggedIn !== true) {
- return;
- }
- if (conflictingSaves.value.length > 0) {
- // Pause syncing while resolving conflicted saves
- return;
- }
-
- g.getSaveList()
- .then(list => {
- const saves = syncSaves(list);
- syncedSaves.value = saves.map(s => s.content.id);
-
- // If our current save has under 2 minutes of playtime, load the cloud save with the most recent time.
- if (player.timePlayed < 120 * 1000 && saves.length > 0) {
- const longestSave = saves.reduce((acc, curr) =>
- acc.content.time < curr.content.time ? curr : acc
- );
- loadSave(longestSave.content);
- }
- })
- .catch(console.error);
-
- setInterval(sync, 60 * 1000);
-}
-
-function syncSaves(
- list: Record<
- number,
- {
- label: string;
- content: string;
- }
- >
-) {
- const savesToUpload = new Set(settings.saves.slice());
- const availableSlots = new Set(new Array(11).fill(0).map((_, i) => i));
- const saves = (
- Object.keys(list)
- .map(slot => {
- const { label, content } = list[slot as unknown as number];
- try {
- return {
- slot: parseInt(slot),
- label,
- content: JSON.parse(decodeSave(content) ?? "")
- };
- } catch (e) {
- return null;
- }
- })
- .filter(
- n =>
- n != null &&
- typeof n.content.id === "string" &&
- typeof n.content.time === "number" &&
- typeof n.content.timePlayed === "number"
- ) as {
- slot: number;
- label?: string;
- content: Partial & { id: string; time: number; timePlayed: number };
- }[]
- ).filter(cloudSave => {
- if (cloudSave.label != null) {
- cloudSave.content.name = cloudSave.label;
- }
- availableSlots.delete(cloudSave.slot);
- const localSaveId = settings.saves.find(id => id === cloudSave.content.id);
- if (localSaveId == undefined) {
- settings.saves.push(cloudSave.content.id);
- save(setupInitialStore(cloudSave.content));
- } else {
- savesToUpload.delete(localSaveId);
- try {
- const localSave = JSON.parse(
- decodeSave(localStorage.getItem(localSaveId) ?? "") ?? ""
- ) as Partial | null;
- if (localSave == null) {
- return false;
- }
- localSave.id = localSaveId;
- localSave.time = localSave.time ?? 0;
- localSave.timePlayed = localSave.timePlayed ?? 0;
-
- const timePlayedDiff = Math.abs(
- localSave.timePlayed - cloudSave.content.timePlayed
- );
- const timeDiff = Math.abs(localSave.time - cloudSave.content.time);
- // If their last played time and total time played are both within 2 minutes, just use the newer save (very unlikely to be coincidence)
- // Otherwise, ask the player
- if (timePlayedDiff < 120 * 1000 && timeDiff < 120 * 1000) {
- if (localSave.time < cloudSave.content.time) {
- save(setupInitialStore(cloudSave.content));
- if (settings.active === localSaveId) {
- loadSave(cloudSave.content);
- }
- } else {
- galaxy.value
- ?.save(
- cloudSave.slot,
- LZString.compressToUTF16(
- stringifySave(setupInitialStore(localSave))
- ),
- localSave.name ?? cloudSave.label
- )
- .catch(console.error);
- // Update cloud save content for the return value
- cloudSave.content = localSave as Player;
- }
- } else {
- conflictingSaves.value.push({
- id: localSaveId,
- cloud: cloudSave.content,
- local: localSave as LoadablePlayerData,
- slot: cloudSave.slot
- });
- }
- } catch (e) {
- return false;
- }
- }
- return true;
- });
-
- savesToUpload.forEach(id => {
- try {
- if (availableSlots.size > 0) {
- const localSave = localStorage.getItem(id) ?? "";
- const parsedLocalSave = JSON.parse(decodeSave(localSave) ?? "");
- const slot = availableSlots.values().next().value;
- galaxy.value
- ?.save(slot, localSave, parsedLocalSave.name)
- .then(() => syncedSaves.value.push(parsedLocalSave.id))
- .catch(console.error);
- availableSlots.delete(slot);
- }
- } catch (e) {}
- });
-
- return saves;
-}
diff --git a/src/util/save.ts b/src/util/save.ts
index a7830cd..3f7da26 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -1,11 +1,10 @@
-import { LoadablePlayerData } from "components/saves/SavesManager.vue";
import projInfo from "data/projInfo.json";
import { globalBus } from "game/events";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
-import settings, { loadSettings } from "game/settings";
+import settings from "game/settings";
import LZString from "lz-string";
-import { ref, shallowReactive } from "vue";
+import { ref } from "vue";
export function setupInitialStore(player: Partial = {}): Player {
return Object.assign(
@@ -34,18 +33,23 @@ export function save(playerData?: Player): string {
}
export async function load(): Promise {
- // Load global settings
- loadSettings();
-
try {
let save = localStorage.getItem(settings.active);
if (save == null) {
await loadSave(newSave());
return;
}
- save = decodeSave(save);
- if (save == null) {
- throw "Unable to determine save encoding";
+ 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) {
@@ -60,23 +64,6 @@ export async function load(): Promise {
}
}
-export function decodeSave(save: string) {
- 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);
- return null;
- }
- return save;
-}
-
export function newSave(): Player {
const id = getUniqueID();
const player = setupInitialStore({ id });
@@ -137,40 +124,6 @@ export async function loadSave(playerObj: Partial): Promise {
globalBus.emit("onLoad");
}
-const cachedSaves = shallowReactive>({});
-export function getCachedSave(id: string) {
- if (cachedSaves[id] == null) {
- let save = localStorage.getItem(id);
- if (save == null) {
- cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
- } else if (save === "dW5kZWZpbmVk") {
- cachedSaves[id] = { error: `Save is undefined`, id };
- } else {
- try {
- save = decodeSave(save);
- if (save == null) {
- console.warn("Unable to determine preset encoding", save);
- 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) {
- cachedSaves[id] = { error, id };
- console.warn(`Failed to load info about save with id ${id}:\n${error}\n${save}`);
- }
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return cachedSaves[id]!;
-}
-export function clearCachedSaves() {
- Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
-}
-export function clearCachedSave(id: string) {
- cachedSaves[id] = undefined;
-}
-
setInterval(() => {
if (player.autosave) {
save();
diff --git a/tests/features/conversions.test.ts b/tests/features/conversions.test.ts
index 58e2e99..09d6569 100644
--- a/tests/features/conversions.test.ts
+++ b/tests/features/conversions.test.ts
@@ -47,10 +47,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.currentGain)).compare_tolerance(0);
- });
});
describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion;
@@ -73,10 +69,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(100);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.actualGain)).compare_tolerance(0);
- });
});
describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion;
@@ -103,10 +95,6 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10)
);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.currentAt)).compare_tolerance(0);
- });
});
describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion;
@@ -129,10 +117,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.nextAt)).compare_tolerance(Decimal.dTen);
- });
});
test("Converts correctly", () => {
const conversion = createCumulativeConversion(() => ({
@@ -209,10 +193,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.currentGain)).compare_tolerance(1);
- });
});
describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion;
@@ -236,10 +216,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(99);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.actualGain)).compare_tolerance(0);
- });
});
describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion;
@@ -267,10 +243,6 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10)
);
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.currentAt)).compare_tolerance(Decimal.pow(1, 2).times(10));
- });
});
describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion;
@@ -294,10 +266,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
});
- test("Zero", () => {
- baseResource.value = Decimal.dZero;
- expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(2, 2).times(10));
- });
});
test("Converts correctly", () => {
const conversion = createIndependentConversion(() => ({
diff --git a/tests/features/tree.test.ts b/tests/features/tree.test.ts
deleted file mode 100644
index 206988f..0000000
--- a/tests/features/tree.test.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
-import { Ref, ref } from "vue";
-import "../utils";
-import {
- createTree,
- createTreeNode,
- defaultResetPropagation,
- invertedResetPropagation,
- branchedResetPropagation
-} from "features/trees/tree";
-import { createReset, GenericReset } from "features/reset";
-
-describe("Reset propagation", () => {
- let shouldReset: Ref, shouldNotReset: Ref;
- let goodReset: GenericReset, badReset: GenericReset;
- beforeAll(() => {
- shouldReset = ref(false);
- shouldNotReset = ref(false);
- goodReset = createReset(() => ({
- thingsToReset: [],
- onReset() {
- shouldReset.value = true;
- }
- }));
- badReset = createReset(() => ({
- thingsToReset: [],
- onReset() {
- shouldNotReset.value = true;
- }
- }));
- });
- beforeEach(() => {
- shouldReset.value = false;
- shouldNotReset.value = false;
- });
- test("No resets", () => {
- expect(() => {
- const a = createTreeNode(() => ({}));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({}));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]]
- }));
- tree.reset(a);
- }).not.toThrowError();
- });
-
- test("Do not propagate resets", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({ reset: badReset }));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]]
- }));
- tree.reset(b);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Default propagation", () => {
- const a = createTreeNode(() => ({ reset: goodReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]],
- resetPropagation: defaultResetPropagation
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Inverted propagation", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: goodReset }));
- const tree = createTree(() => ({
- nodes: [[a], [b], [c]],
- resetPropagation: invertedResetPropagation
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Branched propagation", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: goodReset }));
- const tree = createTree(() => ({
- nodes: [[a, b, c]],
- resetPropagation: branchedResetPropagation,
- branches: [{ startNode: b, endNode: c }]
- }));
- tree.reset(b);
- expect(shouldReset.value).toBe(true);
- expect(shouldNotReset.value).toBe(false);
- });
-
- test("Branched propagation not bi-directional", () => {
- const a = createTreeNode(() => ({ reset: badReset }));
- const b = createTreeNode(() => ({}));
- const c = createTreeNode(() => ({ reset: badReset }));
- const tree = createTree(() => ({
- nodes: [[a, b, c]],
- resetPropagation: branchedResetPropagation,
- branches: [{ startNode: c, endNode: b }]
- }));
- tree.reset(b);
- expect(shouldNotReset.value).toBe(false);
- });
-});
diff --git a/tests/game/modifiers.test.ts b/tests/game/modifiers.test.ts
index dd019e1..d5e186d 100644
--- a/tests/game/modifiers.test.ts
+++ b/tests/game/modifiers.test.ts
@@ -133,14 +133,14 @@ describe("Exponential Modifiers", () =>
testModifiers(createExponentialModifier, "exponent", Decimal.pow));
describe("Sequential Modifiers", () => {
- function createModifier>(
+ function createModifier(
value: Computable,
- options?: T
- ) {
+ options: Partial = {}
+ ): WithRequired {
return createSequentialModifier(() => [
- createAdditiveModifier(() => ({ ...(options ?? {}), addend: value })),
- createMultiplicativeModifier(() => ({ ...(options ?? {}), multiplier: value })),
- createExponentialModifier(() => ({ ...(options ?? {}), exponent: value }))
+ createAdditiveModifier(() => ({ ...options, addend: value })),
+ createMultiplicativeModifier(() => ({ ...options, multiplier: value })),
+ createExponentialModifier(() => ({ ...options, exponent: value }))
]);
}
@@ -199,17 +199,6 @@ describe("Sequential Modifiers", () => {
// So long as one is true or undefined, enable should be true
expect(unref(modifier.enabled)).toBe(true);
});
- test("respects enabled", () => {
- const value = ref(10);
- const enabled = ref(false);
- const modifier = createSequentialModifier(() => [
- createMultiplicativeModifier(() => ({ multiplier: 5, enabled }))
- ]);
- const formula = modifier.getFormula(Formula.variable(value));
- expect(formula.evaluate()).compare_tolerance(value.value);
- enabled.value = true;
- expect(formula.evaluate()).not.compare_tolerance(value.value);
- });
});
describe("applies smallerIsBetter correctly", () => {