Compare commits
16 commits
main
...
jakub791/m
Author | SHA1 | Date | |
---|---|---|---|
|
e6b2d808a5 | ||
|
0ca42d3432 | ||
|
05c56640fe | ||
|
2e449f2b78 | ||
|
6540546432 | ||
|
cdc035b5bd | ||
|
f4b77e852d | ||
|
867695cf71 | ||
|
1f3fd06ab0 | ||
|
2fda7772e8 | ||
|
2a50b05647 | ||
|
9b527188d1 | ||
|
c833c6ac7a | ||
|
1f551afeb0 | ||
|
27084ef7a0 | ||
|
655fb9230d |
85 changed files with 7829 additions and 4667 deletions
|
@ -27,13 +27,6 @@ module.exports = {
|
|||
allowNullableObject: true,
|
||||
allowNullableBoolean: true
|
||||
}
|
||||
],
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
globals: {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Setup RSync
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y rsync
|
||||
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: https://github.com/JamesIves/github-pages-deploy-action@v4.2.5
|
||||
with:
|
||||
branch: pages # The branch the action should deploy to.
|
||||
folder: dist # The folder the action should deploy.
|
|
@ -1,22 +0,0 @@
|
|||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
- run: npm run lint
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
@ -3,7 +3,6 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||
|
|
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
|
@ -1,11 +1,11 @@
|
|||
name: Run Tests
|
||||
name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -19,4 +19,3 @@ jobs:
|
|||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
- run: npm run lint
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -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,
|
||||
|
|
70
CHANGELOG.md
70
CHANGELOG.md
|
@ -6,76 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.2] - 2024-04-01
|
||||
### Added
|
||||
- Export save button in error boundaries
|
||||
- isRendered utility function
|
||||
- Automatic galaxy.click cloud saves support
|
||||
- Support for null and undefined in persistent refs
|
||||
### Changes
|
||||
- round, floor, ceil, trunc, and add now invert as no-ops
|
||||
- "The Paper Pilot Community" renamed to "Profectus & Friends"
|
||||
- Updated CI etc. to work with Forgejo
|
||||
- Improved modifier typing
|
||||
- Rename `printFormula` to `Formula.stringify`
|
||||
### Fixed
|
||||
- Hotkeys not working correctly with most combinations of modifiers
|
||||
- Reset button using `currentAt` when not gaining
|
||||
- Formulas not using modifiers that are disabled initially
|
||||
- branchedResetPropagation logic being incorrect
|
||||
- Fixed default elementsd in the main layer not updating Context when being added or removed
|
||||
- Board links props not working in camelCase
|
||||
- Board links absorbing pointer events
|
||||
- Thrown errors not appearing in console
|
||||
- Disabled elements would eat mouse events
|
||||
- Fixed cost requirement without formula counting as being able to afford infinite purchases rather than just one
|
||||
- Pinnable tooltips causing innocuous console error
|
||||
- Bars with direction as "Left" wouldn't appear correctly
|
||||
### Documentation
|
||||
- Clarified expected progress values for board nodes
|
||||
- Added CONTRIBUTING.md and enforce eslint on all PRs
|
||||
### Tests
|
||||
- Update formula test cases
|
||||
- Tree reset propagation
|
||||
|
||||
Contributors: thepaperpilot, escapee, nif
|
||||
|
||||
## [0.6.1] - 2023-05-17
|
||||
### Added
|
||||
- Error boundaries around each layer, and errors now display on the page when in development
|
||||
- Utility for creating requirement based on whether a conversion has met a requirement
|
||||
### Changed
|
||||
- **BREAKING** Formulas/requirements refactor
|
||||
- spendResources renamed to cumulativeCost
|
||||
- summedPurchases renamed to directSum
|
||||
- calculateMaxAffordable now takes optional 'maxBulkAmount' parameter
|
||||
- cost requirements now pass cumulativeCost, maxBulkAmount, and directSum to calculateMaxAffordable
|
||||
- Non-integrable and non-invertible formulas will now work in more situations
|
||||
- Repeatable.maximize is removed
|
||||
- Challenge.maximize is removed
|
||||
- Formulas have better typing information now
|
||||
- Integrate functions now log errors if the variable input is not integrable
|
||||
- Cyclical proxies now throw errors
|
||||
- createFormulaPreview is now a JSX function
|
||||
- Tree nodes are not automatically capitalized anymore
|
||||
- upgrade.canPurchase now returns false if the upgrade is already bought
|
||||
- TPS display is simplified and more performant now
|
||||
### Fixed
|
||||
- Actions could not be constructed
|
||||
- Progress bar on actions was misaligned
|
||||
- Many different issues the Board features (and many changes/improvements)
|
||||
- Calculating max affordable could sometimes infinite loop
|
||||
- Non-integrable formulas could cause errors in cost requirements
|
||||
- estimateTime would not show "never" when production is 0
|
||||
- isInvertible and isIntegrable now properly handle nested formulas
|
||||
- Repeatables' amount display would show the literal text "joinJSX"
|
||||
- Repeatables would not buy max properly
|
||||
- Reset buttons were showing wrong "currentAt" vs "nextAt"
|
||||
- Step-wise formulas not updating their value correctly
|
||||
- Bonus amount decorator now checks for `amount` property in the post construct callback
|
||||
### Documentation
|
||||
- Various typos fixed and a few sections made more thorough
|
||||
|
||||
## [0.6.0] - 2023-04-20
|
||||
### Added
|
||||
- **BREAKING** New requirements system
|
||||
|
|
|
@ -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).
|
9089
package-lock.json
generated
9089
package-lock.json
generated
File diff suppressed because it is too large
Load diff
114
package.json
114
package.json
|
@ -1,60 +1,58 @@
|
|||
{
|
||||
"name": "profectus",
|
||||
"version": "0.6.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@pixi/app": "~6.3.2",
|
||||
"@pixi/constants": "~6.3.2",
|
||||
"@pixi/core": "~6.3.2",
|
||||
"@pixi/display": "~6.3.2",
|
||||
"@pixi/math": "~6.3.2",
|
||||
"@pixi/particle-emitter": "^5.0.7",
|
||||
"@pixi/sprite": "~6.3.2",
|
||||
"@pixi/ticker": "~6.3.2",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"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",
|
||||
"vue": "^3.2.26",
|
||||
"vue-next-select": "^2.10.2",
|
||||
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vue-transition-expand": "^0.1.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"eslint": "^8.6.0",
|
||||
"jsdom": "^20.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vitest": "^1.3.1",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.x"
|
||||
}
|
||||
"name": "profectus",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"testw": "vitest",
|
||||
"serve": "vite preview --host"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/material-icons": "^4.5.4",
|
||||
"@fontsource/roboto-mono": "^4.5.10",
|
||||
"@ivanv/vue-collapse-transition": "^1.0.2",
|
||||
"@pixi/app": "^7.2.4",
|
||||
"@pixi/constants": "^7.2.4",
|
||||
"@pixi/core": "^7.2.4",
|
||||
"@pixi/display": "^7.2.4",
|
||||
"@pixi/math": "^7.2.4",
|
||||
"@pixi/particle-emitter": "^5.0.8",
|
||||
"@pixi/sprite": "^7.2.4",
|
||||
"@pixi/ticker": "^7.2.4",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"nanoevents": "^7.0.1",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vue": "^3.2.47",
|
||||
"vue-next-select": "^2.10.5",
|
||||
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
|
||||
"vue-textarea-autosize": "^1.1.1",
|
||||
"vue-toastification": "^2.0.0-rc.1",
|
||||
"vue-transition-expand": "^0.1.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"workbox-window": "^6.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@vitejs/plugin-vue": "^4.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/runtime-dom": "^3.2.47",
|
||||
"eslint": "^8.6.0",
|
||||
"jsdom": "^22.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.4",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.31.0",
|
||||
"vue-tsc": "^1.6.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
|
|
47
src/App.vue
47
src/App.vue
|
@ -1,44 +1,34 @@
|
|||
<template>
|
||||
<div v-if="appErrors.length > 0" class="error-container" :style="theme"><Error :errors="appErrors" /></div>
|
||||
<template v-else>
|
||||
<div id="modal-root" :style="theme" />
|
||||
<div class="app" :style="theme" :class="{ useHeader }">
|
||||
<Nav v-if="useHeader" />
|
||||
<Game />
|
||||
<TPS v-if="unref(showTPS)" />
|
||||
<AddictionWarning />
|
||||
<GameOverScreen />
|
||||
<NaNScreen />
|
||||
<CloudSaveResolver />
|
||||
<component :is="gameComponent" />
|
||||
</div>
|
||||
</template>
|
||||
<div id="modal-root" :style="theme" />
|
||||
<div class="app" :style="theme" :class="{ useHeader }">
|
||||
<Nav v-if="useHeader" />
|
||||
<Game />
|
||||
<TPS v-if="unref(showTPS)" />
|
||||
<GameOverScreen />
|
||||
<NaNScreen />
|
||||
<component :is="gameComponent" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import "@fontsource/roboto-mono";
|
||||
import Error from "components/Error.vue";
|
||||
import AddictionWarning from "components/modals/AddictionWarning.vue";
|
||||
import CloudSaveResolver from "components/modals/CloudSaveResolver.vue";
|
||||
import GameOverScreen from "components/modals/GameOverScreen.vue";
|
||||
import NaNScreen from "components/modals/NaNScreen.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import state from "game/state";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { computed, toRef, unref } from "vue";
|
||||
import Game from "./components/Game.vue";
|
||||
import GameOverScreen from "./components/GameOverScreen.vue";
|
||||
import NaNScreen from "./components/NaNScreen.vue";
|
||||
import Nav from "./components/Nav.vue";
|
||||
import TPS from "./components/TPS.vue";
|
||||
import projInfo from "./data/projInfo.json";
|
||||
import themes from "./data/themes";
|
||||
import settings, { gameComponents } from "./game/settings";
|
||||
import "./main.css";
|
||||
import "@fontsource/roboto-mono";
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
const useHeader = projInfo.useHeader;
|
||||
const theme = computed(() => themes[settings.theme].variables as CSSProperties);
|
||||
const showTPS = toRef(settings, "showTPS");
|
||||
const appErrors = toRef(state, "errors");
|
||||
|
||||
const gameComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
|
||||
|
@ -61,15 +51,4 @@ const gameComponent = computed(() => {
|
|||
height: 100%;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.error-container {
|
||||
background: var(--background);
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.error-container > .error {
|
||||
position: static;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
BoundsInjectionKey
|
||||
} from "game/layers";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { nextTick, onMounted, provide, ref } from "vue";
|
||||
import { Ref, nextTick, onMounted, provide, ref } from "vue";
|
||||
import { globalBus } from "game/events";
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -21,7 +21,7 @@ const emit = defineEmits<{
|
|||
const nodes = ref<Record<string, FeatureNode | undefined>>({});
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateBounds);
|
||||
const resizeListener = ref<Element | null>(null);
|
||||
const resizeListener = ref<Element | null>(null) as Ref<Element | null>;
|
||||
onMounted(() => {
|
||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||
const resListener = resizeListener.value;
|
||||
|
@ -30,7 +30,7 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
let isDirty = true;
|
||||
let boundingRect = ref(resizeListener.value?.getBoundingClientRect());
|
||||
const boundingRect = ref(resizeListener.value?.getBoundingClientRect());
|
||||
function updateBounds() {
|
||||
if (isDirty) {
|
||||
isDirty = false;
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
<template>
|
||||
<div class="error">
|
||||
<h1 class="error-title">{{ firstError.name }}: {{ firstError.message }}</h1>
|
||||
<div class="error-details" style="margin-top: -10px">
|
||||
<div v-if="firstError.cause">
|
||||
<div v-for="row in causes[0]" :key="row">{{ row }}</div>
|
||||
</div>
|
||||
<div v-if="firstError.stack" :style="firstError.cause ? 'margin-top: 10px' : ''">
|
||||
<div v-for="row in stacks[0]" :key="row">{{ row }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
Check the console for more details, and consider sharing it with the developers on
|
||||
<a :href="projInfo.discordLink || 'https://discord.gg/yJ4fjnjU54'" class="discord-link"
|
||||
>discord</a
|
||||
>!
|
||||
<FeedbackButton @click="exportSave" class="button" style="display: inline-flex"
|
||||
><span class="material-icons" style="font-size: 16px">content_paste</span
|
||||
><span style="margin-left: 8px; font-size: medium">Copy Save</span></FeedbackButton
|
||||
><br />
|
||||
<div v-if="errors.length > 1" style="margin-top: 20px"><h3>Other errors</h3></div>
|
||||
<div v-for="(error, i) in errors.slice(1)" :key="i" style="margin-top: 20px">
|
||||
<details class="error-details">
|
||||
<summary>{{ error.name }}: {{ error.message }}</summary>
|
||||
<div v-if="error.cause" style="margin-top: 10px">
|
||||
<div v-for="row in causes[i + 1]" :key="row">{{ row }}</div>
|
||||
</div>
|
||||
<div v-if="error.stack" style="margin-top: 10px">
|
||||
<div v-for="row in stacks[i + 1]" :key="row">{{ row }}</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import player, { stringifySave } from "game/player";
|
||||
import LZString from "lz-string";
|
||||
import { computed, onMounted } from "vue";
|
||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
errors: Error[];
|
||||
}>();
|
||||
|
||||
const firstError = computed(() => props.errors[0]);
|
||||
const stacks = computed(() =>
|
||||
props.errors.map(error => (error.stack == null ? [] : error.stack.split("\n")))
|
||||
);
|
||||
const causes = computed(() =>
|
||||
props.errors.map(error =>
|
||||
error.cause == null
|
||||
? []
|
||||
: (typeof error.cause === "string" ? error.cause : JSON.stringify(error.cause)).split(
|
||||
"\n"
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
function exportSave() {
|
||||
let saveToExport = stringifySave(player);
|
||||
switch (projInfo.exportEncoding) {
|
||||
default:
|
||||
console.warn(`Unknown save encoding: ${projInfo.exportEncoding}. Defaulting to lz`);
|
||||
case "lz":
|
||||
saveToExport = LZString.compressToUTF16(saveToExport);
|
||||
break;
|
||||
case "base64":
|
||||
saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
|
||||
break;
|
||||
case "plain":
|
||||
break;
|
||||
}
|
||||
console.log(saveToExport);
|
||||
|
||||
// Put on clipboard. Using the clipboard API asks for permissions and stuff
|
||||
const el = document.createElement("textarea");
|
||||
el.value = saveToExport;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
player.autosave = false;
|
||||
player.devSpeed = 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
border: solid 10px var(--danger);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: left;
|
||||
min-height: calc(100% - 20px);
|
||||
text-align: left;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.error-title {
|
||||
background: var(--danger);
|
||||
color: var(--feature-foreground);
|
||||
display: block;
|
||||
margin: -10px 0 10px 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--raised-background);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.discord-link {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
|
@ -37,14 +37,14 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import { hasWon } from "data/projEntry";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import { formatTime } from "util/bignum";
|
||||
import { loadSave, newSave } from "util/save";
|
||||
import { computed, toRef } from "vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
|
||||
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
target="_blank"
|
||||
>
|
||||
<span class="material-icons info-modal-discord">discord</span>
|
||||
Profectus & Friends
|
||||
The Paper Pilot Community
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -60,6 +60,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import Modal from "components/Modal.vue";
|
||||
import type Changelog from "data/Changelog.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { jsx } from "features/feature";
|
||||
|
@ -68,7 +69,6 @@ import { infoComponents } from "game/settings";
|
|||
import { formatTime } from "util/bignum";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<ErrorVue v-if="errors.length > 0" :errors="errors" />
|
||||
<div class="layer-container" :style="{ '--layer-color': unref(color) }" v-bind="$attrs" v-else>
|
||||
<div class="layer-container" :style="{ '--layer-color': unref(color) }">
|
||||
<button v-if="showGoBack" class="goBack" @click="goBack">❌</button>
|
||||
|
||||
<button
|
||||
|
@ -29,12 +28,12 @@ import type { CoercableComponent } from "features/feature";
|
|||
import type { FeatureNode } from "game/layers";
|
||||
import player from "game/player";
|
||||
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
||||
import { PropType, Ref, computed, defineComponent, onErrorCaptured, ref, toRefs, unref } from "vue";
|
||||
import type { PropType, Ref } from "vue";
|
||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
||||
import Context from "./Context.vue";
|
||||
import ErrorVue from "./Error.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { Context, ErrorVue },
|
||||
components: { Context },
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
|
@ -78,23 +77,13 @@ export default defineComponent({
|
|||
props.nodes.value = nodes;
|
||||
}
|
||||
|
||||
const errors = ref<Error[]>([]);
|
||||
onErrorCaptured((err, instance, info) => {
|
||||
console.warn(`Error caught in "${props.name}" layer`, err, instance, info);
|
||||
errors.value.push(
|
||||
err instanceof Error ? (err as Error) : new Error(JSON.stringify(err))
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
component,
|
||||
minimizedComponent,
|
||||
showGoBack,
|
||||
updateNodes,
|
||||
unref,
|
||||
goBack,
|
||||
errors
|
||||
goBack
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
name="modal"
|
||||
@before-enter="isAnimating = true"
|
||||
@after-leave="isAnimating = false"
|
||||
appear
|
||||
>
|
||||
<div
|
||||
class="modal-mask"
|
||||
|
@ -13,7 +12,7 @@
|
|||
v-bind="$attrs"
|
||||
>
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-container" :width="width">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<slot name="header" :shown="isOpen"> default header </slot>
|
||||
</div>
|
||||
|
@ -42,12 +41,10 @@
|
|||
<script setup lang="ts">
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { computed, ref, toRefs, unref } from "vue";
|
||||
import Context from "../Context.vue";
|
||||
import Context from "./Context.vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
modelValue: boolean;
|
||||
preventClosing?: boolean;
|
||||
width?: string;
|
||||
}>();
|
||||
const props = toRefs(_props);
|
||||
const emit = defineEmits<{
|
||||
|
@ -56,9 +53,7 @@ const emit = defineEmits<{
|
|||
|
||||
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
|
||||
function close() {
|
||||
if (unref(props.preventClosing) !== true) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
|
||||
const isAnimating = ref(false);
|
|
@ -19,7 +19,7 @@
|
|||
class="nan-modal-discord-link"
|
||||
>
|
||||
<span class="material-icons nan-modal-discord">discord</span>
|
||||
{{ discordName || "Profectus & Friends" }}
|
||||
{{ discordName || "The Paper Pilot Community" }}
|
||||
</a>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -46,6 +46,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import state from "game/state";
|
||||
|
@ -53,8 +54,7 @@ import type { DecimalSource } from "util/bignum";
|
|||
import Decimal, { format } from "util/bignum";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, ref, toRef, watch } from "vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
const { discordName, discordLink } = projInfo;
|
|
@ -15,7 +15,9 @@
|
|||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
|
||||
>The Paper Pilot Community</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||
|
@ -36,7 +38,7 @@
|
|||
</div>
|
||||
<div @click="savesManager?.open()">
|
||||
<Tooltip display="Saves" :direction="Direction.Down" xoffset="-20px">
|
||||
<span class="material-icons" :class="{ needsSync }">library_books</span>
|
||||
<span class="material-icons">library_books</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="options?.open()">
|
||||
|
@ -53,7 +55,7 @@
|
|||
</div>
|
||||
<div @click="savesManager?.open()">
|
||||
<Tooltip display="Saves" :direction="Direction.Right">
|
||||
<span class="material-icons" :class="{ needsSync }">library_books</span>
|
||||
<span class="material-icons">library_books</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div @click="options?.open()">
|
||||
|
@ -80,7 +82,9 @@
|
|||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
|
||||
>The Paper Pilot Community</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||
|
@ -98,14 +102,12 @@
|
|||
import Changelog from "data/Changelog.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import settings from "game/settings";
|
||||
import { Direction } from "util/common";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import Info from "./modals/Info.vue";
|
||||
import Options from "./modals/Options.vue";
|
||||
import SavesManager from "./modals/SavesManager.vue";
|
||||
import { ref } from "vue";
|
||||
import Info from "./Info.vue";
|
||||
import Options from "./Options.vue";
|
||||
import SavesManager from "./SavesManager.vue";
|
||||
|
||||
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
|
||||
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
|
||||
|
@ -119,10 +121,6 @@ const { useHeader, banner, title, discordName, discordLink, versionNumber } = pr
|
|||
function openDiscord() {
|
||||
window.open(discordLink, "mywindow");
|
||||
}
|
||||
|
||||
const needsSync = computed(
|
||||
() => galaxy.value?.loggedIn === true && !syncedSaves.value.includes(settings.active)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -270,32 +268,4 @@ const needsSync = computed(
|
|||
color: var(--foreground);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.needsSync {
|
||||
color: var(--danger);
|
||||
animation: 4s wiggle ease infinite;
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0% {
|
||||
transform: rotate(-3deg);
|
||||
box-shadow: 0 2px 2px #0003;
|
||||
}
|
||||
5% {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(0);
|
||||
box-shadow: 0 2px 2px #0003;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,8 +4,15 @@
|
|||
<div class="header">
|
||||
<h2>Settings</h2>
|
||||
<div class="option-tabs">
|
||||
<button :class="{selected: isTab('behaviour')}" @click="setTab('behaviour')">Behaviour</button>
|
||||
<button :class="{selected: isTab('appearance')}" @click="setTab('appearance')">Appearance</button>
|
||||
<button :class="{ selected: isTab('behaviour') }" @click="setTab('behaviour')">
|
||||
Behaviour
|
||||
</button>
|
||||
<button
|
||||
:class="{ selected: isTab('appearance') }"
|
||||
@click="setTab('appearance')"
|
||||
>
|
||||
Appearance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -14,9 +21,10 @@
|
|||
<Toggle :title="unthrottledTitle" v-model="unthrottled" />
|
||||
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
|
||||
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
|
||||
<Toggle :title="showHealthWarningTitle" v-model="showHealthWarning" v-if="!projInfo.disableHealthWarning" />
|
||||
<Toggle :title="autosaveTitle" v-model="autosave" />
|
||||
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()">Manually save</FeedbackButton>
|
||||
<FeedbackButton v-if="!autosave" class="button save-button" @click="save()"
|
||||
>Manually save</FeedbackButton
|
||||
>
|
||||
</div>
|
||||
<div v-if="isTab('appearance')">
|
||||
<Select :title="themeTitle" :options="themes" v-model="theme" />
|
||||
|
@ -29,20 +37,20 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import { save } from "util/save";
|
||||
import rawThemes from "data/themes";
|
||||
import { jsx } from "features/feature";
|
||||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import player from "game/player";
|
||||
import settings, { settingFields } from "game/settings";
|
||||
import { camelToTitle, Direction } from "util/common";
|
||||
import { save } from "util/save";
|
||||
import { coerceComponent, render } from "util/vue";
|
||||
import { computed, ref, toRefs } from "vue";
|
||||
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Toggle from "../fields/Toggle.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Select from "./fields/Select.vue";
|
||||
import Toggle from "./fields/Toggle.vue";
|
||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
const currentTab = ref("behaviour");
|
||||
|
@ -70,10 +78,10 @@ const themes = Object.keys(rawThemes).map(theme => ({
|
|||
}));
|
||||
|
||||
const settingFieldsComponent = computed(() => {
|
||||
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
|
||||
return coerceComponent(jsx(() => <>{settingFields.map(render)}</>));
|
||||
});
|
||||
|
||||
const { showTPS, theme, unthrottled, alignUnits, showHealthWarning } = toRefs(settings);
|
||||
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings);
|
||||
const { autosave, offlineProd } = toRefs(player);
|
||||
const isPaused = computed({
|
||||
get() {
|
||||
|
@ -92,25 +100,28 @@ const unthrottledTitle = jsx(() => (
|
|||
));
|
||||
const offlineProdTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Offline production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
Offline Production
|
||||
<Tooltip display="Save-specific" direction={Direction.Right}>
|
||||
*
|
||||
</Tooltip>
|
||||
<desc>Simulate production that occurs while the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const showHealthWarningTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Show videogame addiction warning
|
||||
<desc>Show a helpful warning after playing for a long time about video game addiction and encouraging you to take a break.</desc>
|
||||
</span>
|
||||
));
|
||||
const autosaveTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
Autosave
|
||||
<Tooltip display="Save-specific" direction={Direction.Right}>
|
||||
*
|
||||
</Tooltip>
|
||||
<desc>Automatically save the game every second or when the game is closed.</desc>
|
||||
</span>
|
||||
));
|
||||
const isPausedTitle = jsx(() => (
|
||||
<span class="option-title">
|
||||
Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
|
||||
Pause game
|
||||
<Tooltip display="Save-specific" direction={Direction.Right}>
|
||||
*
|
||||
</Tooltip>
|
||||
<desc>Stop everything from moving.</desc>
|
||||
</span>
|
||||
));
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="save" :class="{ active: isActive, readonly }">
|
||||
<div class="handle material-icons" v-if="readonly !== true">drag_handle</div>
|
||||
<div class="actions" v-if="!isEditing && readonly !== true">
|
||||
<div class="save" :class="{ active: isActive }">
|
||||
<div class="handle material-icons">drag_handle</div>
|
||||
<div class="actions" v-if="!isEditing">
|
||||
<FeedbackButton
|
||||
@click="emit('export')"
|
||||
class="button"
|
||||
|
@ -40,7 +40,7 @@
|
|||
</Tooltip>
|
||||
</DangerButton>
|
||||
</div>
|
||||
<div class="actions" v-else-if="readonly !== true">
|
||||
<div class="actions" v-else>
|
||||
<button @click="changeName" class="button">
|
||||
<Tooltip display="Save" :direction="Direction.Left" class="info">
|
||||
<span class="material-icons">check</span>
|
||||
|
@ -53,17 +53,12 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="details" v-if="save.error == undefined && !isEditing">
|
||||
<Tooltip display="Synced!" :direction="Direction.Right" v-if="synced"
|
||||
><span class="material-icons synced">cloud</span></Tooltip
|
||||
>
|
||||
<button class="button open" @click="emit('open')" :disabled="readonly">
|
||||
<button class="button open" @click="emit('open')">
|
||||
<h3>{{ save.name }}</h3>
|
||||
</button>
|
||||
<span class="save-version">v{{ save.modVersion }}</span
|
||||
><br />
|
||||
<div v-if="currentTime" class="time">
|
||||
Last played {{ dateFormat.format(currentTime) }}
|
||||
</div>
|
||||
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
|
||||
</div>
|
||||
<div class="details" v-else-if="save.error == undefined && isEditing">
|
||||
<Text v-model="newName" class="editname" @submit="changeName" />
|
||||
|
@ -78,18 +73,16 @@
|
|||
import Tooltip from "features/tooltips/Tooltip.vue";
|
||||
import player from "game/player";
|
||||
import { Direction } from "util/common";
|
||||
import { computed, ref, toRefs, unref, watch } from "vue";
|
||||
import DangerButton from "../fields/DangerButton.vue";
|
||||
import FeedbackButton from "../fields/FeedbackButton.vue";
|
||||
import Text from "../fields/Text.vue";
|
||||
import { computed, ref, toRefs, watch } from "vue";
|
||||
import DangerButton from "./fields/DangerButton.vue";
|
||||
import FeedbackButton from "./fields/FeedbackButton.vue";
|
||||
import Text from "./fields/Text.vue";
|
||||
import type { LoadablePlayerData } from "./SavesManager.vue";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
|
||||
const _props = defineProps<{
|
||||
save: LoadablePlayerData;
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
const { save, readonly } = toRefs(_props);
|
||||
const { save } = toRefs(_props);
|
||||
const emit = defineEmits<{
|
||||
(e: "export"): void;
|
||||
(e: "open"): void;
|
||||
|
@ -113,18 +106,10 @@ const newName = ref("");
|
|||
|
||||
watch(isEditing, () => (newName.value = save.value.name ?? ""));
|
||||
|
||||
const isActive = computed(
|
||||
() => save.value != null && save.value.id === player.id && !unref(readonly)
|
||||
);
|
||||
const isActive = computed(() => save.value != null && save.value.id === player.id);
|
||||
const currentTime = computed(() =>
|
||||
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
|
||||
);
|
||||
const synced = computed(
|
||||
() =>
|
||||
!unref(readonly) &&
|
||||
galaxy.value?.loggedIn === true &&
|
||||
syncedSaves.value.includes(save.value.id)
|
||||
);
|
||||
|
||||
function changeName() {
|
||||
emit("editName", newName.value);
|
||||
|
@ -154,13 +139,6 @@ function changeName() {
|
|||
padding-left: 0;
|
||||
}
|
||||
|
||||
.open:disabled {
|
||||
cursor: inherit;
|
||||
color: var(--foreground);
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.handle {
|
||||
flex-grow: 0;
|
||||
margin-right: 8px;
|
||||
|
@ -174,10 +152,6 @@ function changeName() {
|
|||
margin-right: 80px;
|
||||
}
|
||||
|
||||
.save.readonly .details {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 0.8em;
|
||||
color: var(--danger);
|
||||
|
@ -202,17 +176,6 @@ function changeName() {
|
|||
.editname {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.synced {
|
||||
font-size: 100%;
|
||||
margin-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
@ -238,8 +201,4 @@ function changeName() {
|
|||
.save .field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.details > .tooltip-container {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
|
@ -4,9 +4,6 @@
|
|||
<h2>Saves Manager</h2>
|
||||
</template>
|
||||
<template #body="{ shown }">
|
||||
<div v-if="showNotSyncedWarning" style="color: var(--danger)">
|
||||
Not all saves are synced! You may need to delete stale saves.
|
||||
</div>
|
||||
<Draggable
|
||||
:list="settings.saves"
|
||||
handle=".handle"
|
||||
|
@ -60,28 +57,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/Modal.vue";
|
||||
import projInfo from "data/projInfo.json";
|
||||
import type { Player } from "game/player";
|
||||
import player, { stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { galaxy, syncedSaves } from "util/galaxy";
|
||||
import {
|
||||
clearCachedSave,
|
||||
clearCachedSaves,
|
||||
decodeSave,
|
||||
getCachedSave,
|
||||
getUniqueID,
|
||||
loadSave,
|
||||
newSave,
|
||||
save
|
||||
} from "util/save";
|
||||
import { getUniqueID, loadSave, newSave, save } from "util/save";
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { computed, nextTick, ref, shallowReactive, watch } from "vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import Select from "../fields/Select.vue";
|
||||
import Text from "../fields/Text.vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Select from "./fields/Select.vue";
|
||||
import Text from "./fields/Text.vue";
|
||||
import Save from "./Save.vue";
|
||||
|
||||
export type LoadablePlayerData = Omit<Partial<Player>, "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;
|
||||
|
@ -130,7 +125,7 @@ watch(saveToImport, importedSave => {
|
|||
}
|
||||
});
|
||||
|
||||
let bankContext = import.meta.globEager("./../../../saves/*.txt", { as: "raw" });
|
||||
let bankContext = import.meta.globEager("./../../saves/*.txt", { as: "raw" });
|
||||
let bank = ref(
|
||||
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
|
||||
acc.push({
|
||||
|
@ -144,10 +139,48 @@ let bank = ref(
|
|||
}, [])
|
||||
);
|
||||
|
||||
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,4 +331,4 @@ function editSave(id: string, newName: string) {
|
|||
.presets .vue-select[aria-expanded="true"] vue-dropdown {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<div class="tpsDisplay" v-if="!tps.isNan()">TPS: {{ formatWhole(tps) }}</div>
|
||||
<div class="tpsDisplay" v-if="!tps.isNan()">
|
||||
TPS: {{ formatWhole(tps) }}
|
||||
<transition name="fade"
|
||||
><span v-if="showLow" class="low">{{ formatWhole(low) }}</span></transition
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import state from "game/state";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatWhole } from "util/bignum";
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
|
||||
const tps = computed(() =>
|
||||
Decimal.div(
|
||||
|
@ -13,6 +19,20 @@ const tps = computed(() =>
|
|||
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
||||
)
|
||||
);
|
||||
|
||||
const lastTenFPS = ref<number[]>([]);
|
||||
watchEffect(() => {
|
||||
lastTenFPS.value.push(Math.round(tps.value.toNumber()));
|
||||
if (lastTenFPS.value.length > 10) {
|
||||
lastTenFPS.value = lastTenFPS.value.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
const low = computed(() =>
|
||||
lastTenFPS.value.reduce<DecimalSource>((acc, curr) => Decimal.max(acc, curr), 0)
|
||||
);
|
||||
|
||||
const showLow = computed(() => Decimal.sub(tps.value, low.value).gt(1));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
<template>
|
||||
<Modal v-model="isOpen" v-bind="$attrs">
|
||||
<template v-slot:header>
|
||||
<div class="vga-modal-header">
|
||||
<h2>Kindly consider taking a break.</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>
|
||||
You've been actively enjoying this game for awhile recently - and it's great that
|
||||
you've been having a good time! That said, there are dangers to games like these that you should be aware of:
|
||||
</p>
|
||||
<p>
|
||||
While incremental games can be fun and even healthy in certain contexts, they can
|
||||
exacerbate video game addiction even more than other genres. If you feel like
|
||||
playing incremental games is taking priority over other things in your life, or
|
||||
manipulating your sleep schedule, it may be prudent to seek help.
|
||||
</p>
|
||||
<p>
|
||||
<h4>Resources:</h4>
|
||||
<span>
|
||||
<a style="display: inline" href="https://www.samhsa.gov/" target="_blank">
|
||||
SAMHSA
|
||||
</a>
|
||||
(<a style="display: inline" href="tel:1-800-662-4357">1-800-662-HELP</a>)
|
||||
</span>
|
||||
<br />
|
||||
<a href="https://www.reddit.com/r/StopGaming/">r/StopGaming</a>
|
||||
</p>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="vga-footer">
|
||||
<button @click="neverShow" class="button">Never show this again</button>
|
||||
<button @click="isOpen = false" class="button">Close</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
<SavesManager ref="savesManager" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import projInfo from "data/projInfo.json";
|
||||
import settings from "game/settings";
|
||||
import state from "game/state";
|
||||
import { ref, watchEffect } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
watchEffect(() => {
|
||||
if (
|
||||
projInfo.disableHealthWarning === false &&
|
||||
settings.showHealthWarning &&
|
||||
state.mouseActivity.filter(i => i).length > 6
|
||||
) {
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
function neverShow() {
|
||||
settings.showHealthWarning = false;
|
||||
isOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vga-modal-header {
|
||||
padding-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.vga-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vga-footer button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
|
@ -1,228 +0,0 @@
|
|||
<template>
|
||||
<Modal v-model="isOpen" width="960px" ref="modal" :prevent-closing="true">
|
||||
<template v-slot:header>
|
||||
<div class="cloud-saves-modal-header">
|
||||
<h2>Cloud {{ pluralizedSave }} loaded!</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div>
|
||||
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?
|
||||
</div>
|
||||
<br />
|
||||
<div
|
||||
v-for="(conflict, i) in unref(conflictingSaves)"
|
||||
:key="conflict.id"
|
||||
class="conflict-container"
|
||||
>
|
||||
<div @click="selectCloud(i)" :class="{ selected: selectedSaves[i] === 'cloud' }">
|
||||
<h2>
|
||||
Cloud
|
||||
<span
|
||||
v-if="(conflict.cloud.time ?? 0) > (conflict.local.time ?? 0)"
|
||||
class="note"
|
||||
>(more recent)</span
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
(conflict.cloud.timePlayed ?? 0) > (conflict.local.timePlayed ?? 0)
|
||||
"
|
||||
class="note"
|
||||
>(more playtime)</span
|
||||
>
|
||||
</h2>
|
||||
<Save :save="conflict.cloud" :readonly="true" />
|
||||
</div>
|
||||
<div @click="selectLocal(i)" :class="{ selected: selectedSaves[i] === 'local' }">
|
||||
<h2>
|
||||
Local
|
||||
<span
|
||||
v-if="(conflict.cloud.time ?? 0) <= (conflict.local.time ?? 0)"
|
||||
class="note"
|
||||
>(more recent)</span
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
(conflict.cloud.timePlayed ?? 0) <= (conflict.local.timePlayed ?? 0)
|
||||
"
|
||||
class="note"
|
||||
>(more playtime)</span
|
||||
>
|
||||
</h2>
|
||||
<Save :save="conflict.local" :readonly="true" />
|
||||
</div>
|
||||
<div
|
||||
@click="selectBoth(i)"
|
||||
:class="{ selected: selectedSaves[i] === 'both' }"
|
||||
style="flex-basis: 30%"
|
||||
>
|
||||
<h2>Both</h2>
|
||||
<div class="save">Keep Both</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="cloud-saves-footer">
|
||||
<button @click="close" class="button">Confirm</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { conflictingSaves, galaxy } from "util/galaxy";
|
||||
import { getUniqueID, save, setupInitialStore } from "util/save";
|
||||
import { ComponentPublicInstance, computed, ref, unref, watch } from "vue";
|
||||
import Modal from "./Modal.vue";
|
||||
import Save from "./Save.vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
// True means replacing local save with cloud save
|
||||
const selectedSaves = ref<("cloud" | "local" | "both")[]>([]);
|
||||
|
||||
const pluralizedSave = computed(() => (conflictingSaves.value.length > 1 ? "saves" : "save"));
|
||||
|
||||
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
|
||||
|
||||
watch(
|
||||
() => conflictingSaves.value.length > 0,
|
||||
shouldOpen => {
|
||||
if (shouldOpen) {
|
||||
selectedSaves.value = conflictingSaves.value.map(({ local, cloud }) => {
|
||||
return (local.time ?? 0) < (cloud.time ?? 0) ? "cloud" : "local";
|
||||
});
|
||||
isOpen.value = true;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => modal.value?.isOpen,
|
||||
open => {
|
||||
if (open === false) {
|
||||
conflictingSaves.value = [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function selectLocal(index: number) {
|
||||
selectedSaves.value[index] = "local";
|
||||
}
|
||||
|
||||
function selectCloud(index: number) {
|
||||
selectedSaves.value[index] = "cloud";
|
||||
}
|
||||
|
||||
function selectBoth(index: number) {
|
||||
selectedSaves.value[index] = "both";
|
||||
}
|
||||
|
||||
function close() {
|
||||
for (let i = 0; i < selectedSaves.value.length; i++) {
|
||||
const { slot, local, cloud } = conflictingSaves.value[i];
|
||||
switch (selectedSaves.value[i]) {
|
||||
case "local":
|
||||
// Replace cloud save with local
|
||||
galaxy.value
|
||||
?.save(
|
||||
slot,
|
||||
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||
cloud.name
|
||||
)
|
||||
.catch(console.error);
|
||||
break;
|
||||
case "cloud":
|
||||
// Replace local save with cloud
|
||||
save(setupInitialStore(cloud));
|
||||
break;
|
||||
case "both":
|
||||
// Get a new save ID for the cloud save, and sync the local one to the cloud
|
||||
const id = getUniqueID();
|
||||
save({ ...setupInitialStore(cloud), id });
|
||||
settings.saves.push(id);
|
||||
galaxy.value
|
||||
?.save(
|
||||
slot,
|
||||
LZString.compressToUTF16(stringifySave(setupInitialStore(local))),
|
||||
cloud.name
|
||||
)
|
||||
.catch(console.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
isOpen.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cloud-saves-modal-header {
|
||||
padding: 10px 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.cloud-saves-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cloud-saves-footer button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.conflict-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.conflict-container > * {
|
||||
flex-basis: 50%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.conflict-container + .conflict-container {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.conflict-container h2 {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
height: 1.5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: x-small;
|
||||
opacity: 0.7;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.save {
|
||||
border: solid 4px var(--outline);
|
||||
padding: 4px;
|
||||
background: var(--raised-background);
|
||||
margin: var(--feature-margin);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.conflict-container .save {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conflict-container .selected .save {
|
||||
border-color: var(--bought);
|
||||
}
|
||||
</style>
|
|
@ -19,7 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Modal from "components/modals/Modal.vue";
|
||||
import Modal from "components/Modal.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Resource, displayResource } from "features/resources/resource";
|
|||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||
import { createTreeNode } from "features/trees/tree";
|
||||
import type { GenericFormula } from "game/formulas/types";
|
||||
import { BaseLayer } from "game/layers";
|
||||
import type { Modifier } from "game/modifiers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
|
@ -134,10 +133,10 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||
{displayResource(
|
||||
resetButton.conversion.baseResource,
|
||||
!unref(resetButton.conversion.buyMax) &&
|
||||
Decimal.gte(unref(resetButton.conversion.actualGain), 1)
|
||||
? unref(resetButton.conversion.currentAt)
|
||||
: unref(resetButton.conversion.nextAt)
|
||||
unref(resetButton.conversion.buyMax) ||
|
||||
Decimal.floor(unref(resetButton.conversion.actualGain)).neq(1)
|
||||
? unref(resetButton.conversion.nextAt)
|
||||
: unref(resetButton.conversion.currentAt)
|
||||
)}{" "}
|
||||
{resetButton.conversion.baseResource.displayName}
|
||||
</div>
|
||||
|
@ -460,7 +459,7 @@ export function createFormulaPreview(
|
|||
const processedShowPreview = convertComputable(showPreview);
|
||||
const processedPreviewAmount = convertComputable(previewAmount);
|
||||
if (!formula.hasVariable()) {
|
||||
console.error("Cannot create formula preview if the formula does not have a variable");
|
||||
throw new Error("Cannot create formula preview if the formula does not have a variable");
|
||||
}
|
||||
return jsx(() => {
|
||||
if (unref(processedShowPreview)) {
|
||||
|
@ -486,22 +485,3 @@ export function createFormulaPreview(
|
|||
return <>{formatSmall(formula.evaluate())}</>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||
* Note it will have a true value even if the feature is off screen.
|
||||
* @param layer The layer the feature appears within
|
||||
* @param id The ID of the feature
|
||||
*/
|
||||
export function isRendered(layer: BaseLayer, id: string): ComputedRef<boolean>;
|
||||
/**
|
||||
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||
* Note it will have a true value even if the feature is off screen.
|
||||
* @param layer The layer the feature appears within
|
||||
* @param feature The feature that may be rendered
|
||||
*/
|
||||
export function isRendered(layer: BaseLayer, feature: { id: string }): ComputedRef<boolean>;
|
||||
export function isRendered(layer: BaseLayer, idOrFeature: string | { id: string }) {
|
||||
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
|
||||
return computed(() => id in layer.nodes.value);
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
color,
|
||||
reset
|
||||
}));
|
||||
const tooltip = addTooltip(treeNode, {
|
||||
addTooltip(treeNode, {
|
||||
display: createResourceTooltip(points),
|
||||
pinnable: true
|
||||
});
|
||||
|
@ -58,7 +58,6 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
name,
|
||||
color,
|
||||
points,
|
||||
tooltip,
|
||||
display: jsx(() => (
|
||||
<>
|
||||
<MainDisplay resource={points} color={color} />
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Node from "components/Node.vue";
|
||||
import Spacer from "components/layout/Spacer.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource";
|
||||
|
@ -49,35 +48,19 @@ export const main = createLayer("main", function (this: BaseLayer) {
|
|||
links: tree.links,
|
||||
display: jsx(() => (
|
||||
<>
|
||||
{player.devSpeed === 0 ? (
|
||||
<div>
|
||||
Game Paused
|
||||
<Node id="paused" />
|
||||
</div>
|
||||
) : null}
|
||||
{player.devSpeed === 0 ? <div>Game Paused</div> : null}
|
||||
{player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? (
|
||||
<div>
|
||||
Dev Speed: {format(player.devSpeed)}x
|
||||
<Node id="devspeed" />
|
||||
</div>
|
||||
<div>Dev Speed: {format(player.devSpeed)}x</div>
|
||||
) : null}
|
||||
{player.offlineTime != null && player.offlineTime !== 0 ? (
|
||||
<div>
|
||||
Offline Time: {formatTime(player.offlineTime)}
|
||||
<Node id="offline" />
|
||||
</div>
|
||||
<div>Offline Time: {formatTime(player.offlineTime)}</div>
|
||||
) : null}
|
||||
<div>
|
||||
{Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null}
|
||||
<h2>{format(points.value)}</h2>
|
||||
{Decimal.lt(points.value, "1e1e6") ? <span> points</span> : null}
|
||||
</div>
|
||||
{Decimal.gt(pointGain.value, 0) ? (
|
||||
<div>
|
||||
({oomps.value})
|
||||
<Node id="oomps" />
|
||||
</div>
|
||||
) : null}
|
||||
{Decimal.gt(pointGain.value, 0) ? <div>({oomps.value})</div> : null}
|
||||
<Spacer />
|
||||
{render(tree)}
|
||||
</>
|
||||
|
|
|
@ -88,10 +88,6 @@
|
|||
"type": "string",
|
||||
"enum": ["base64", "lz", "plain"],
|
||||
"description": "The encoding to use when exporting to the clipboard. Plain-text is fast to generate but is easiest for the player to manipulate and cheat with. Base 64 is slightly slower and the string will be longer but will offer a small barrier to people trying to cheat. LZ-String is the slowest method, but produces the smallest strings and still offers a small barrier to those trying to cheat. Some sharing platforms like pastebin may automatically delete base64 encoded text, and some sites might not support all the characters used in lz-string exports."
|
||||
},
|
||||
"disableHealthWarning": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to disable the health warning that appears to the player after excessive playtime (activity during 6 of the last 8 hours). If left enabled, the player will still be able to individually turn off the health warning in settings or by clicking \"Do not show again\" in the warning itself."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,5 @@
|
|||
"maxTickLength": 3600,
|
||||
"offlineLimit": 1,
|
||||
"enablePausing": true,
|
||||
"exportEncoding": "base64",
|
||||
"disableHealthWarning": false
|
||||
"exportEncoding": "base64"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { computed } from "@vue/reactivity";
|
||||
import { isArray } from "@vue/shared";
|
||||
import { computed } from "vue";
|
||||
import Select from "components/fields/Select.vue";
|
||||
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
|
@ -160,9 +159,6 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
|
||||
achievement.earned = earned;
|
||||
achievement.complete = function () {
|
||||
if (earned.value) {
|
||||
return;
|
||||
}
|
||||
earned.value = true;
|
||||
const genericAchievement = achievement as GenericAchievement;
|
||||
genericAchievement.onComplete?.();
|
||||
|
@ -208,7 +204,7 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
unref(achievement.earned) &&
|
||||
!(
|
||||
display != null &&
|
||||
typeof display === "object" &&
|
||||
typeof display == "object" &&
|
||||
"optionsDisplay" in (display as Record<string, unknown>)
|
||||
)
|
||||
) {
|
||||
|
@ -275,7 +271,7 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
const requirements = [
|
||||
createVisibilityRequirement(genericAchievement),
|
||||
createBooleanRequirement(() => !genericAchievement.earned.value),
|
||||
...(isArray(achievement.requirements)
|
||||
...(Array.isArray(achievement.requirements)
|
||||
? achievement.requirements
|
||||
: [achievement.requirements])
|
||||
];
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import {
|
||||
Component,
|
||||
|
@ -157,7 +156,7 @@ export function createAction<T extends ActionOptions>(
|
|||
}
|
||||
];
|
||||
const originalStyle = unref(style);
|
||||
if (isArray(originalStyle)) {
|
||||
if (Array.isArray(originalStyle)) {
|
||||
currStyle.push(...originalStyle);
|
||||
} else if (originalStyle != null) {
|
||||
currStyle.push(originalStyle);
|
||||
|
|
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||
break;
|
||||
case Direction.Left:
|
||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
|
||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
|
||||
break;
|
||||
case Direction.Default:
|
||||
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||
|
|
|
@ -28,16 +28,7 @@
|
|||
v-for="link in unref(links) || []"
|
||||
:key="`${link.startNode.id}-${link.endNode.id}`"
|
||||
>
|
||||
<BoardLinkVue
|
||||
:link="link"
|
||||
:dragging="unref(draggingNode)"
|
||||
:dragged="
|
||||
link.startNode === unref(draggingNode) ||
|
||||
link.endNode === unref(draggingNode)
|
||||
? dragged
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
<BoardLinkVue :link="link" />
|
||||
</g>
|
||||
</transition-group>
|
||||
<transition-group name="grow" :duration="500" appear>
|
||||
|
@ -47,12 +38,10 @@
|
|||
:nodeType="types[node.type]"
|
||||
:dragging="unref(draggingNode)"
|
||||
:dragged="unref(draggingNode) === node ? dragged : undefined"
|
||||
:hasDragged="unref(draggingNode) == null ? false : hasDragged"
|
||||
:receivingNode="unref(receivingNode) === node"
|
||||
:isSelected="unref(selectedNode) === node"
|
||||
:selectedAction="
|
||||
unref(selectedNode) === node ? unref(selectedAction) : null
|
||||
"
|
||||
:hasDragged="hasDragged"
|
||||
:receivingNode="unref(receivingNode)?.id === node.id"
|
||||
:selectedNode="unref(selectedNode)"
|
||||
:selectedAction="unref(selectedAction)"
|
||||
@mouseDown="mouseDown"
|
||||
@endDragging="endDragging"
|
||||
@clickAction="(actionId: string) => clickAction(node, actionId)"
|
||||
|
@ -108,10 +97,6 @@ const stage = ref<any>(null);
|
|||
|
||||
const sortedNodes = computed(() => {
|
||||
const nodes = props.nodes.value.slice();
|
||||
if (props.selectedNode.value) {
|
||||
const node = nodes.splice(nodes.indexOf(props.selectedNode.value), 1)[0];
|
||||
nodes.push(node);
|
||||
}
|
||||
if (props.draggingNode.value) {
|
||||
const node = nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0];
|
||||
nodes.push(node);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<line
|
||||
class="link"
|
||||
v-bind="linkProps"
|
||||
v-bind="link"
|
||||
:class="{ pulsing: link.pulsing }"
|
||||
:x1="startPosition.x"
|
||||
:y1="startPosition.y"
|
||||
|
@ -11,53 +11,36 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BoardNode, BoardNodeLink } from "features/boards/board";
|
||||
import { kebabifyObject } from "util/vue";
|
||||
import type { BoardNodeLink } from "features/boards/board";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
link: BoardNodeLink;
|
||||
dragging: BoardNode | null;
|
||||
dragged?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}>();
|
||||
const props = toRefs(_props);
|
||||
|
||||
const startPosition = computed(() => {
|
||||
const position = { ...props.link.value.startNode.position };
|
||||
const position = props.link.value.startNode.position;
|
||||
if (props.link.value.offsetStart) {
|
||||
position.x += unref(props.link.value.offsetStart).x;
|
||||
position.y += unref(props.link.value.offsetStart).y;
|
||||
}
|
||||
if (props.dragging?.value === props.link.value.startNode) {
|
||||
position.x += props.dragged?.value?.x ?? 0;
|
||||
position.y += props.dragged?.value?.y ?? 0;
|
||||
}
|
||||
return position;
|
||||
});
|
||||
|
||||
const endPosition = computed(() => {
|
||||
const position = { ...props.link.value.endNode.position };
|
||||
const position = props.link.value.endNode.position;
|
||||
if (props.link.value.offsetEnd) {
|
||||
position.x += unref(props.link.value.offsetEnd).x;
|
||||
position.y += unref(props.link.value.offsetEnd).y;
|
||||
}
|
||||
if (props.dragging?.value === props.link.value.endNode) {
|
||||
position.x += props.dragged?.value?.x ?? 0;
|
||||
position.y += props.dragged?.value?.y ?? 0;
|
||||
}
|
||||
return position;
|
||||
});
|
||||
|
||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
transition-duration: 0s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link.pulsing {
|
||||
|
|
|
@ -160,7 +160,7 @@ const _props = defineProps<{
|
|||
};
|
||||
hasDragged?: boolean;
|
||||
receivingNode?: boolean;
|
||||
isSelected: boolean;
|
||||
selectedNode: BoardNode | null;
|
||||
selectedAction: GenericBoardNodeAction | null;
|
||||
}>();
|
||||
const props = toRefs(_props);
|
||||
|
@ -170,6 +170,7 @@ const emit = defineEmits<{
|
|||
(e: "clickAction", actionId: string): void;
|
||||
}>();
|
||||
|
||||
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
|
||||
const isDraggable = computed(() =>
|
||||
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
||||
);
|
||||
|
@ -210,7 +211,7 @@ const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(p
|
|||
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
|
||||
const label = computed(
|
||||
() =>
|
||||
(props.isSelected.value
|
||||
(isSelected.value
|
||||
? unref(props.selectedAction) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
getNodeProperty(unref(props.selectedAction)!.tooltip, unref(props.node))
|
||||
|
|
|
@ -104,7 +104,7 @@ export interface NodeTypeOptions {
|
|||
shape: NodeComputable<Shape>;
|
||||
/** Whether the node can accept another node being dropped upon it. */
|
||||
canAccept?: NodeComputable<boolean, [BoardNode]>;
|
||||
/** The progress value of the node, from 0 to 1. */
|
||||
/** The progress value of the node. */
|
||||
progress?: NodeComputable<number>;
|
||||
/** How the progress should be displayed on the node. */
|
||||
progressDisplay?: NodeComputable<ProgressDisplay>;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import Toggle from "components/fields/Toggle.vue";
|
||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
|
@ -52,6 +51,8 @@ export interface ChallengeOptions {
|
|||
reset?: GenericReset;
|
||||
/** The requirement(s) to complete this challenge. */
|
||||
requirements: Requirements;
|
||||
/** Whether or not completing this challenge should grant multiple completions if requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
|
||||
maximize?: Computable<boolean>;
|
||||
/** The maximum number of times the challenge can be completed. */
|
||||
completionLimit?: Computable<DecimalSource>;
|
||||
/** Shows a marker on the corner of the feature. */
|
||||
|
@ -122,6 +123,7 @@ export type Challenge<T extends ChallengeOptions> = Replace<
|
|||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
|
||||
requirements: GetComputableType<T["requirements"]>;
|
||||
maximize: GetComputableType<T["maximize"]>;
|
||||
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
||||
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
|
@ -207,7 +209,10 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
}
|
||||
};
|
||||
challenge.canComplete = computed(() =>
|
||||
maxRequirementsMet((challenge as GenericChallenge).requirements)
|
||||
Decimal.max(
|
||||
maxRequirementsMet((challenge as GenericChallenge).requirements),
|
||||
unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1
|
||||
)
|
||||
);
|
||||
challenge.complete = function (remainInChallenge?: boolean) {
|
||||
const genericChallenge = challenge as GenericChallenge;
|
||||
|
@ -248,6 +253,7 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
|
||||
processComputable(challenge as T, "canStart");
|
||||
setDefault(challenge, "canStart", true);
|
||||
processComputable(challenge as T, "maximize");
|
||||
processComputable(challenge as T, "completionLimit");
|
||||
setDefault(challenge, "completionLimit", 1);
|
||||
processComputable(challenge as T, "mark");
|
||||
|
@ -348,7 +354,7 @@ export function createActiveChallenge(
|
|||
export function isAnyChallengeActive(
|
||||
challenges: GenericChallenge[] | Ref<GenericChallenge | null>
|
||||
): Ref<boolean> {
|
||||
if (isArray(challenges)) {
|
||||
if (Array.isArray(challenges)) {
|
||||
challenges = createActiveChallenge(challenges);
|
||||
}
|
||||
return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CoercableComponent, OptionsFunc, Replace } from "features/feature";
|
||||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { setDefault } from "features/feature";
|
||||
import type { Resource } from "features/resources/resource";
|
||||
import Formula from "game/formulas/formulas";
|
||||
|
@ -12,7 +12,6 @@ import { createLazyProxy } from "util/proxies";
|
|||
import type { Ref } from "vue";
|
||||
import { computed, unref } from "vue";
|
||||
import { GenericDecorator } from "./decorators/common";
|
||||
import { createBooleanRequirement } from "game/requirements";
|
||||
|
||||
/** An object that configures a {@link Conversion}. */
|
||||
export interface ConversionOptions {
|
||||
|
@ -293,20 +292,3 @@ export function setupPassiveGeneration(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates requirement that is met when the conversion hits a specified gain amount
|
||||
* @param conversion The conversion to check the gain amount of
|
||||
* @param minGainAmount The minimum gain amount that must be met for the requirement to be met
|
||||
*/
|
||||
export function createCanConvertRequirement(
|
||||
conversion: GenericConversion,
|
||||
minGainAmount: Computable<DecimalSource> = 1,
|
||||
display?: CoercableComponent
|
||||
) {
|
||||
const computedMinGainAmount = convertComputable(minGainAmount);
|
||||
return createBooleanRequirement(
|
||||
() => Decimal.gte(unref(conversion.actualGain), unref(computedMinGainAmount)),
|
||||
display
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,12 +69,14 @@ export const bonusAmountDecorator: Decorator<
|
|||
BaseBonusAmountFeature,
|
||||
GenericBonusAmountFeature
|
||||
> = {
|
||||
postConstruct(feature) {
|
||||
preConstruct(feature) {
|
||||
if (feature.amount === undefined) {
|
||||
console.error(
|
||||
`Decorated feature ${feature.id} does not contain the required 'amount' property"`
|
||||
);
|
||||
}
|
||||
},
|
||||
postConstruct(feature) {
|
||||
processComputable(feature, "bonusAmount");
|
||||
if (feature.totalAmount === undefined) {
|
||||
feature.totalAmount = computed(() =>
|
||||
|
|
|
@ -27,8 +27,8 @@ export type Decorator<
|
|||
|
||||
export type GenericDecorator = Decorator<unknown>;
|
||||
|
||||
export interface EffectFeatureOptions<T = unknown> {
|
||||
effect: Computable<T>;
|
||||
export interface EffectFeatureOptions {
|
||||
effect: Computable<unknown>;
|
||||
}
|
||||
|
||||
export type EffectFeature<T extends EffectFeatureOptions> = Replace<
|
||||
|
@ -36,9 +36,9 @@ export type EffectFeature<T extends EffectFeatureOptions> = Replace<
|
|||
{ effect: GetComputableType<T["effect"]> }
|
||||
>;
|
||||
|
||||
export type GenericEffectFeature<T = unknown> = Replace<
|
||||
export type GenericEffectFeature = Replace<
|
||||
EffectFeature<EffectFeatureOptions>,
|
||||
{ effect: ProcessedComputable<T> }
|
||||
{ effect: ProcessedComputable<unknown> }
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -92,7 +92,7 @@ export function setDefault<T, K extends keyof T>(
|
|||
key: K,
|
||||
value: T[K]
|
||||
): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
|
||||
if (object[key] == null && value != null) {
|
||||
if (object[key] === undefined && value != undefined) {
|
||||
object[key] = value;
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]
|
|||
if (value != null && typeof value === "object") {
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typeof (value as Record<string, any>).type === "symbol" &&
|
||||
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)
|
||||
) {
|
||||
|
|
|
@ -128,7 +128,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
}
|
||||
if (prop != null || typeof key === "symbol") {
|
||||
if (prop != undefined || typeof key === "symbol") {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
cache[key] = computed(() => prop.call(receiver, id, target.getState(id)));
|
||||
}
|
||||
return cache[key].value;
|
||||
} else if (prop != null) {
|
||||
} else if (prop != undefined) {
|
||||
return unref(prop);
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,7 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
|||
prop = (target as any)[`on${key}`];
|
||||
if (isFunction(prop)) {
|
||||
return () => prop.call(receiver, id, target.getState(id));
|
||||
} else if (prop != null) {
|
||||
} else if (prop != undefined) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ export function createGrid<T extends GridOptions>(
|
|||
return grid.id + "-" + cell;
|
||||
};
|
||||
grid.getState = function (this: GenericGrid, cell: string | number) {
|
||||
if (this.cellState.value[cell] != null) {
|
||||
if (this.cellState.value[cell] != undefined) {
|
||||
return cellState.value[cell];
|
||||
}
|
||||
return this.cells[cell].startState;
|
||||
|
|
|
@ -99,30 +99,16 @@ document.onkeydown = function (e) {
|
|||
if (hasWon.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
const keysToCheck: string[] = [e.key];
|
||||
if (e.shiftKey && e.ctrlKey) {
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toUpperCase());
|
||||
if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("ctrl+shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
keysToCheck.push("shift+ctrl+" + uppercaseNumbers.indexOf(e.key));
|
||||
} else {
|
||||
keysToCheck.push("ctrl+shift+" + e.key.toLowerCase());
|
||||
keysToCheck.push("shift+ctrl+" + e.key.toLowerCase());
|
||||
}
|
||||
} else if (uppercaseNumbers.includes(e.key)) {
|
||||
keysToCheck.push("shift+" + e.key);
|
||||
keysToCheck.push("shift+" + uppercaseNumbers.indexOf(e.key));
|
||||
let key = e.key;
|
||||
if (uppercaseNumbers.includes(key)) {
|
||||
key = "shift+" + uppercaseNumbers.indexOf(key);
|
||||
} else if (e.shiftKey) {
|
||||
keysToCheck.push("shift+" + e.key.toUpperCase());
|
||||
keysToCheck.push("shift+" + e.key.toLowerCase());
|
||||
} else if (e.ctrlKey) {
|
||||
// remove e.key since the key doesn't change based on ctrl being held or not
|
||||
keysToCheck.splice(0, 1);
|
||||
keysToCheck.push("ctrl+" + e.key);
|
||||
key = "shift+" + key;
|
||||
}
|
||||
const hotkey = hotkeys[keysToCheck.find(key => key in hotkeys) ?? ""];
|
||||
if (e.ctrlKey) {
|
||||
key = "ctrl+" + key;
|
||||
}
|
||||
const hotkey = hotkeys[key];
|
||||
if (hotkey && unref(hotkey.enabled)) {
|
||||
e.preventDefault();
|
||||
hotkey.onPress();
|
||||
|
@ -142,7 +128,7 @@ registerInfoComponent(
|
|||
<div style="column-count: 2">
|
||||
{keys.map(hotkey => (
|
||||
<div>
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} /> {unref(hotkey?.description)}
|
||||
<Hotkey hotkey={hotkey as GenericHotkey} /> {hotkey?.description}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<line
|
||||
stroke-width="15px"
|
||||
stroke="white"
|
||||
v-bind="linkProps"
|
||||
v-bind="link"
|
||||
:x1="startPosition.x"
|
||||
:y1="startPosition.y"
|
||||
:x2="endPosition.x"
|
||||
|
@ -13,7 +13,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { Link } from "features/links/links";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { kebabifyObject } from "util/vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
|
@ -55,6 +54,4 @@ const endPosition = computed(() => {
|
|||
}
|
||||
return position;
|
||||
});
|
||||
|
||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||
</script>
|
||||
|
|
|
@ -36,7 +36,7 @@ onMounted(() => (boundingRect.value = resizeListener.value?.getBoundingClientRec
|
|||
const validLinks = computed(() => {
|
||||
const n = nodes.value;
|
||||
return (
|
||||
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.endNode.id]?.rect) ?? []
|
||||
links.value?.filter(link => n[link.startNode.id]?.rect && n[link.startNode.id]?.rect) ?? []
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -47,7 +47,8 @@ export default defineComponent({
|
|||
resizeTo: resListener,
|
||||
backgroundAlpha: 0
|
||||
});
|
||||
resizeListener.value?.appendChild(app.value.view);
|
||||
// I think it's supporsed to be a canvas element
|
||||
resizeListener.value?.appendChild(app.value.view as HTMLCanvasElement);
|
||||
props.onInit?.(app.value as Application);
|
||||
}
|
||||
updateBounds();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
|
@ -67,6 +66,8 @@ export interface RepeatableOptions {
|
|||
mark?: Computable<boolean | string>;
|
||||
/** Toggles a smaller design for the feature. */
|
||||
small?: Computable<boolean>;
|
||||
/** Whether or not clicking this repeatable should attempt to maximize amount based on the requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
|
||||
maximize?: Computable<boolean>;
|
||||
/** The display to use for this repeatable. */
|
||||
display?: Computable<RepeatableDisplay>;
|
||||
}
|
||||
|
@ -85,6 +86,7 @@ export interface BaseRepeatable {
|
|||
canClick: ProcessedComputable<boolean>;
|
||||
/**
|
||||
* How much amount can be increased by, or 1 if unclickable.
|
||||
* Capped at 1 if {@link RepeatableOptions.maximize} is false.
|
||||
**/
|
||||
amountToIncrease: Ref<DecimalSource>;
|
||||
/** A function that gets called when this repeatable is clicked. */
|
||||
|
@ -108,6 +110,7 @@ export type Repeatable<T extends RepeatableOptions> = Replace<
|
|||
style: GetComputableType<T["style"]>;
|
||||
mark: GetComputableType<T["mark"]>;
|
||||
small: GetComputableType<T["small"]>;
|
||||
maximize: GetComputableType<T["maximize"]>;
|
||||
display: Ref<CoercableComponent>;
|
||||
}
|
||||
>;
|
||||
|
@ -158,11 +161,10 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
)
|
||||
),
|
||||
requiresPay: false,
|
||||
visibility: Visibility.None,
|
||||
canMaximize: true
|
||||
visibility: Visibility.None
|
||||
} as const;
|
||||
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
||||
if (isArray(repeatable.requirements)) {
|
||||
if (Array.isArray(repeatable.requirements)) {
|
||||
repeatable.requirements.unshift(visibilityRequirement);
|
||||
repeatable.requirements.push(limitRequirement);
|
||||
} else {
|
||||
|
@ -191,7 +193,9 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
return currClasses;
|
||||
});
|
||||
repeatable.amountToIncrease = computed(() =>
|
||||
Decimal.clampMin(maxRequirementsMet(repeatable.requirements), 1)
|
||||
unref((repeatable as GenericRepeatable).maximize)
|
||||
? maxRequirementsMet(repeatable.requirements)
|
||||
: 1
|
||||
);
|
||||
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
||||
const onClick = repeatable.onClick;
|
||||
|
@ -200,12 +204,8 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
if (!unref(genericRepeatable.canClick)) {
|
||||
return;
|
||||
}
|
||||
const amountToIncrease = unref(repeatable.amountToIncrease) ?? 1;
|
||||
payRequirements(repeatable.requirements, amountToIncrease);
|
||||
genericRepeatable.amount.value = Decimal.add(
|
||||
genericRepeatable.amount.value,
|
||||
amountToIncrease
|
||||
);
|
||||
payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease));
|
||||
genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
|
||||
onClick?.(event);
|
||||
};
|
||||
processComputable(repeatable as T, "display");
|
||||
|
@ -268,6 +268,7 @@ export function createRepeatable<T extends RepeatableOptions>(
|
|||
processComputable(repeatable as T, "style");
|
||||
processComputable(repeatable as T, "mark");
|
||||
processComputable(repeatable as T, "small");
|
||||
processComputable(repeatable as T, "maximize");
|
||||
|
||||
for (const decorator of decorators) {
|
||||
decorator.postConstruct?.(repeatable);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { OptionsFunc, Replace } from "features/feature";
|
||||
import { getUniqueID } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import type { BaseLayer } from "game/layers";
|
||||
import { NonPersistent, Persistent, SkipPersistence } from "game/persistence";
|
||||
import type { NonPersistent, Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
import type { Unsubscribe } from "nanoevents";
|
||||
import Decimal from "util/bignum";
|
||||
|
@ -20,7 +19,7 @@ export const ResetType = Symbol("Reset");
|
|||
*/
|
||||
export interface ResetOptions {
|
||||
/** List of things to reset. Can include objects which will be recursed over for persistent values. */
|
||||
thingsToReset: Computable<unknown[]>;
|
||||
thingsToReset: Computable<Record<string, unknown>[]>;
|
||||
/** A function that is called when the reset is performed. */
|
||||
onReset?: VoidFunction;
|
||||
}
|
||||
|
@ -62,15 +61,7 @@ export function createReset<T extends ResetOptions>(
|
|||
|
||||
reset.reset = function () {
|
||||
const handleObject = (obj: unknown) => {
|
||||
if (
|
||||
obj != null &&
|
||||
typeof obj === "object" &&
|
||||
!(obj instanceof Decimal) &&
|
||||
!(obj instanceof Formula)
|
||||
) {
|
||||
if (SkipPersistence in obj && obj[SkipPersistence] === true) {
|
||||
return;
|
||||
}
|
||||
if (obj != null && typeof obj === "object") {
|
||||
if (DefaultValue in obj) {
|
||||
const persistent = obj as NonPersistent;
|
||||
persistent.value = persistent[DefaultValue];
|
||||
|
|
|
@ -151,7 +151,8 @@ export function createTabFamily<T extends TabFamilyOptions>(
|
|||
optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
|
||||
): TabFamily<T> {
|
||||
if (Object.keys(tabs).length === 0) {
|
||||
console.error("Cannot create tab family with 0 tabs");
|
||||
console.warn("Cannot create tab family with 0 tabs");
|
||||
throw new Error("Cannot create tab family with 0 tabs");
|
||||
}
|
||||
|
||||
const selected = persistent(Object.keys(tabs)[0], false);
|
||||
|
|
|
@ -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,
|
||||
|
@ -95,6 +95,18 @@ export function addTooltip<T extends TooltipOptions>(
|
|||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (options.pinnable) {
|
||||
if ("pinned" in element) {
|
||||
console.error(
|
||||
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
|
||||
);
|
||||
options.pinnable = false;
|
||||
deletePersistent(options.pinned as Persistent<boolean>);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(element as any).pinned = options.pinned;
|
||||
}
|
||||
}
|
||||
const elementComponent = element[Component];
|
||||
element[Component] = TooltipComponent as GenericComponent;
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
|
|
|
@ -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<Link[]>;
|
||||
/** 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<boolean>;
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import { GenericDecorator } from "features/decorators/common";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
|
@ -151,7 +150,7 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
};
|
||||
|
||||
const visibilityRequirement = createVisibilityRequirement(upgrade as GenericUpgrade);
|
||||
if (isArray(upgrade.requirements)) {
|
||||
if (Array.isArray(upgrade.requirements)) {
|
||||
upgrade.requirements.unshift(visibilityRequirement);
|
||||
} else {
|
||||
upgrade.requirements = [visibilityRequirement, upgrade.requirements];
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { Settings } from "game/settings";
|
|||
import { createNanoEvents } from "nanoevents";
|
||||
import type { App } from "vue";
|
||||
import type { GenericLayer } from "./layers";
|
||||
import state from "./state";
|
||||
|
||||
/** All types of events able to be sent or emitted from the global event bus. */
|
||||
export interface GlobalEvents {
|
||||
|
@ -60,7 +59,3 @@ if ("fonts" in document) {
|
|||
// JSDom doesn't add document.fonts, and Object.defineProperty doesn't seem to work on document
|
||||
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
||||
}
|
||||
|
||||
document.onmousemove = function () {
|
||||
state.mouseActivity[state.mouseActivity.length - 1] = true;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Resource } from "features/resources/resource";
|
|||
import { NonPersistent } from "game/persistence";
|
||||
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||
import { Computable, ProcessedComputable, convertComputable } from "util/computed";
|
||||
import { Ref, computed, ref, unref } from "vue";
|
||||
import { ComputedRef, Ref, computed, ref, unref } from "vue";
|
||||
import * as ops from "./operations";
|
||||
import type {
|
||||
EvaluateFunction,
|
||||
|
@ -56,7 +56,6 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
protected readonly internalIntegrate: IntegrateFunction<T> | undefined;
|
||||
protected readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
|
||||
protected readonly applySubstitution: SubstitutionFunction<T> | undefined;
|
||||
protected readonly description: string | undefined;
|
||||
protected readonly internalVariables: number;
|
||||
|
||||
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
|
||||
|
@ -86,7 +85,6 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
this.internalIntegrate = readonlyProperties.internalIntegrate;
|
||||
this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
|
||||
this.applySubstitution = readonlyProperties.applySubstitution;
|
||||
this.description = options.description;
|
||||
}
|
||||
|
||||
private setupVariable({
|
||||
|
@ -106,7 +104,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
|
||||
private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties<T> {
|
||||
if (inputs.length !== 1) {
|
||||
console.error("Evaluate function is required if inputs is not length 1");
|
||||
throw new Error("Evaluate function is required if inputs is not length 1");
|
||||
}
|
||||
return {
|
||||
inputs: inputs as T,
|
||||
|
@ -218,25 +216,6 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({ variable: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringifies the formula so it's more easy to read in the console
|
||||
* @param formula The formula source to print, used for mapping inputs
|
||||
*/
|
||||
public static stringify(formula: FormulaSource): string {
|
||||
if (formula instanceof InternalFormula) {
|
||||
if (formula.description != null) {
|
||||
return formula.description;
|
||||
}
|
||||
if (formula.internalEvaluate == null) {
|
||||
return formula.hasVariable() ? "x" : format(formula.inputs[0] ?? 0);
|
||||
}
|
||||
return `${formula.internalEvaluate.name}(${formula.inputs
|
||||
.map(Formula.stringify)
|
||||
.join(", ")})`;
|
||||
}
|
||||
return format(unref(formula));
|
||||
}
|
||||
|
||||
// TODO add integration support to step-wise functions
|
||||
/**
|
||||
* Creates a step-wise formula. After {@link start} the formula will have an additional modifier.
|
||||
|
@ -250,16 +229,15 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
start: Computable<DecimalSource>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
) {
|
||||
const formula = formulaModifier(Formula.variable(0));
|
||||
const lhsRef = ref<DecimalSource>(0);
|
||||
const formula = formulaModifier(Formula.variable(lhsRef));
|
||||
const processedStart = convertComputable(start);
|
||||
function evalStep(lhs: DecimalSource) {
|
||||
if (Decimal.lt(lhs, unref(processedStart))) {
|
||||
return lhs;
|
||||
}
|
||||
return Decimal.add(
|
||||
formula.evaluate(Decimal.sub(lhs, unref(processedStart))),
|
||||
unref(processedStart)
|
||||
);
|
||||
lhsRef.value = Decimal.sub(lhs, unref(processedStart));
|
||||
return Decimal.add(formula.evaluate(), unref(processedStart));
|
||||
}
|
||||
function invertStep(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs) && formula.isInvertible()) {
|
||||
|
@ -271,15 +249,12 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
}
|
||||
return lhs.invert(value);
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: evalStep,
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||
description: "indeterminate"
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -318,8 +293,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
!formula.isInvertible() ||
|
||||
(elseFormula != null && !elseFormula.isInvertible())
|
||||
) {
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
if (unref(processedCondition)) {
|
||||
return lhs.invert(formula.invert(value));
|
||||
|
@ -332,9 +306,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: evalStep,
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined,
|
||||
// Can't do anything more descriptive, due to formula's input always being a variable
|
||||
description: "indeterminate"
|
||||
invert: formula.isInvertible() && formula.hasVariable() ? invertStep : undefined
|
||||
});
|
||||
}
|
||||
public static conditional(
|
||||
|
@ -370,35 +342,19 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
public static sgn = InternalFormula.sign;
|
||||
|
||||
public static round(value: FormulaSource) {
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.round,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.round });
|
||||
}
|
||||
|
||||
public static floor(value: FormulaSource) {
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.floor,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.floor });
|
||||
}
|
||||
|
||||
public static ceil(value: FormulaSource) {
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.ceil,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.ceil });
|
||||
}
|
||||
|
||||
public static trunc(value: FormulaSource) {
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.trunc,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.trunc });
|
||||
}
|
||||
|
||||
public static add<T extends GenericFormula>(value: T, other: FormulaSource): T;
|
||||
|
@ -500,7 +456,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({
|
||||
inputs: [value, min, max],
|
||||
evaluate: Decimal.clamp,
|
||||
invert: ops.invertPassthrough
|
||||
invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]>
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -903,10 +859,6 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
});
|
||||
}
|
||||
|
||||
public stringify() {
|
||||
return Formula.stringify(this);
|
||||
}
|
||||
|
||||
public step(
|
||||
start: Computable<DecimalSource>,
|
||||
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
|
||||
|
@ -1307,8 +1259,7 @@ export default class Formula<
|
|||
} else if (this.inputs.length === 1 && this.hasVariable()) {
|
||||
return value;
|
||||
}
|
||||
console.error("Cannot invert non-invertible formula");
|
||||
return 0;
|
||||
throw new Error("Cannot invert non-invertible formula");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1318,8 +1269,7 @@ export default class Formula<
|
|||
*/
|
||||
evaluateIntegral(variable?: DecimalSource): DecimalSource {
|
||||
if (!this.isIntegrable()) {
|
||||
console.error("Cannot evaluate integral of formula without integral");
|
||||
return 0;
|
||||
throw new Error("Cannot evaluate integral of formula without integral");
|
||||
}
|
||||
return this.getIntegralFormula().evaluate(variable);
|
||||
}
|
||||
|
@ -1331,8 +1281,7 @@ export default class Formula<
|
|||
*/
|
||||
invertIntegral(value: DecimalSource): DecimalSource {
|
||||
if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
|
||||
console.error("Cannot invert integral of formula without invertible integral");
|
||||
return 0;
|
||||
throw new Error("Cannot invert integral of formula without invertible integral");
|
||||
}
|
||||
return (this.getIntegralFormula() as InvertibleFormula).invert(value);
|
||||
}
|
||||
|
@ -1359,8 +1308,7 @@ export default class Formula<
|
|||
// We're the complex operation of this formula
|
||||
stack = [];
|
||||
if (this.internalIntegrate == null) {
|
||||
console.error("Cannot integrate formula with non-integrable operation");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Cannot integrate formula with non-integrable operation");
|
||||
}
|
||||
let value = this.internalIntegrate.call(this, stack, ...this.inputs);
|
||||
stack.forEach(func => (value = func(value)));
|
||||
|
@ -1380,16 +1328,14 @@ export default class Formula<
|
|||
) {
|
||||
this.integralFormula = this;
|
||||
} else {
|
||||
console.error("Cannot integrate formula without variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Cannot integrate formula without variable");
|
||||
}
|
||||
}
|
||||
return this.integralFormula;
|
||||
} else {
|
||||
// "Inner" part of the formula
|
||||
if (this.applySubstitution == null) {
|
||||
console.error("Cannot have two complex operations in an integrable formula");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Cannot have two complex operations in an integrable formula");
|
||||
}
|
||||
stack.push((variable: GenericFormula) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
@ -1406,8 +1352,7 @@ export default class Formula<
|
|||
) {
|
||||
return this;
|
||||
} else {
|
||||
console.error("Cannot integrate formula without variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Cannot integrate formula without variable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1432,70 +1377,80 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
|
|||
}
|
||||
|
||||
/**
|
||||
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
|
||||
* Stringifies a formula so it's more easy to read in the console
|
||||
* @param formula The formula to print
|
||||
*/
|
||||
export function printFormula(formula: FormulaSource): string {
|
||||
if (formula instanceof InternalFormula) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return formula.internalEvaluate == null
|
||||
? formula.hasVariable()
|
||||
? "x"
|
||||
: formula.inputs[0] ?? 0
|
||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
formula.internalEvaluate.name +
|
||||
"(" +
|
||||
formula.inputs.map(printFormula).join(", ") +
|
||||
")";
|
||||
}
|
||||
return format(unref(formula));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link spendResources} is changed to false, the calculation will be much faster with higher numbers.
|
||||
* @param formula The formula to use for calculating buy max from
|
||||
* @param resource The resource used when purchasing (is only read from)
|
||||
* @param cumulativeCost Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
|
||||
* @param directSum How many of the most expensive purchases should be manually summed for better accuracy. If unspecified uses 10 when spending resources and 0 when not
|
||||
* @param maxBulkAmount Cap on how many can be purchased at once. If equal to 1 or lte to {@link directSum} then the formula does not need to be invertible. Defaults to Infinity.
|
||||
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
|
||||
* @param summedPurchases How many of the most expensive purchases should be manually summed for better accuracy. If unspecified uses 10 when spending resources and 0 when not
|
||||
*/
|
||||
export function calculateMaxAffordable(
|
||||
formula: GenericFormula,
|
||||
formula: InvertibleFormula,
|
||||
resource: Resource,
|
||||
cumulativeCost: Computable<boolean> = true,
|
||||
directSum?: Computable<number>,
|
||||
maxBulkAmount: Computable<DecimalSource> = Decimal.dInf
|
||||
spendResources?: true,
|
||||
summedPurchases?: number
|
||||
): ComputedRef<DecimalSource>;
|
||||
export function calculateMaxAffordable(
|
||||
formula: InvertibleIntegralFormula,
|
||||
resource: Resource,
|
||||
spendResources: Computable<boolean>,
|
||||
summedPurchases?: number
|
||||
): ComputedRef<DecimalSource>;
|
||||
export function calculateMaxAffordable(
|
||||
formula: InvertibleFormula,
|
||||
resource: Resource,
|
||||
spendResources: Computable<boolean> = true,
|
||||
summedPurchases?: number
|
||||
) {
|
||||
const computedCumulativeCost = convertComputable(cumulativeCost);
|
||||
const computedDirectSum = convertComputable(directSum);
|
||||
const computedmaxBulkAmount = convertComputable(maxBulkAmount);
|
||||
const computedSpendResources = convertComputable(spendResources);
|
||||
return computed(() => {
|
||||
const maxBulkAmount = unref(computedmaxBulkAmount);
|
||||
if (Decimal.eq(maxBulkAmount, 1)) {
|
||||
return Decimal.gte(resource.value, formula.evaluate()) ? Decimal.dOne : Decimal.dZero;
|
||||
}
|
||||
|
||||
const cumulativeCost = unref(computedCumulativeCost);
|
||||
const directSum = unref(computedDirectSum) ?? (cumulativeCost ? 10 : 0);
|
||||
let affordable: DecimalSource = 0;
|
||||
if (Decimal.gt(maxBulkAmount, directSum)) {
|
||||
let affordable;
|
||||
if (unref(computedSpendResources)) {
|
||||
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
|
||||
throw new Error(
|
||||
"Cannot calculate max affordable of formula with non-invertible integral"
|
||||
);
|
||||
}
|
||||
affordable = Decimal.floor(
|
||||
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
|
||||
).sub(unref(formula.innermostVariable) ?? 0);
|
||||
if (summedPurchases == null) {
|
||||
summedPurchases = 10;
|
||||
}
|
||||
} else {
|
||||
if (!formula.isInvertible()) {
|
||||
console.error(
|
||||
"Cannot calculate max affordable of non-invertible formula with more maxBulkAmount than directSum"
|
||||
);
|
||||
return 0;
|
||||
throw new Error("Cannot calculate max affordable of non-invertible formula");
|
||||
}
|
||||
if (cumulativeCost) {
|
||||
if (!formula.isIntegralInvertible()) {
|
||||
console.error(
|
||||
"Cannot calculate max affordable of formula with non-invertible integral"
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
affordable = Decimal.floor(
|
||||
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
|
||||
).sub(unref(formula.innermostVariable) ?? 0);
|
||||
} else {
|
||||
affordable = Decimal.floor(formula.invert(resource.value));
|
||||
affordable = Decimal.floor(formula.invert(resource.value));
|
||||
if (summedPurchases == null) {
|
||||
summedPurchases = 0;
|
||||
}
|
||||
}
|
||||
affordable = Decimal.clampMax(affordable, maxBulkAmount);
|
||||
if (directSum > 0) {
|
||||
const preSumAffordable = affordable;
|
||||
affordable = Decimal.sub(affordable, directSum).clampMin(0);
|
||||
let summedCost;
|
||||
if (cumulativeCost) {
|
||||
summedCost = calculateCost(formula as InvertibleFormula, affordable, true, 0);
|
||||
} else {
|
||||
summedCost = formula.evaluate(
|
||||
Decimal.add(unref(formula.innermostVariable) ?? 0, affordable)
|
||||
);
|
||||
}
|
||||
while (
|
||||
Decimal.lt(affordable, maxBulkAmount) &&
|
||||
Decimal.lt(affordable, Number.MAX_SAFE_INTEGER) &&
|
||||
Decimal.add(preSumAffordable, 1).gte(affordable)
|
||||
) {
|
||||
if (summedPurchases > 0 && Decimal.lt(calculateCost(formula, affordable, true, 0), 1e308)) {
|
||||
affordable = affordable.sub(summedPurchases).clampMin(0);
|
||||
let summedCost = calculateCost(formula, affordable, true, 0);
|
||||
while (true) {
|
||||
const nextCost = formula.evaluate(
|
||||
affordable.add(unref(formula.innermostVariable) ?? 0)
|
||||
);
|
||||
|
@ -1512,78 +1467,65 @@ export function calculateMaxAffordable(
|
|||
}
|
||||
|
||||
/**
|
||||
* Utility for calculating the cost of a formula for a given amount of purchases. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
|
||||
* Utility for calculating the cost of a formula for a given amount of purchases. If {@link spendResources} is changed to false, the calculation will be much faster with higher numbers.
|
||||
* @param formula The formula to use for calculating buy max from
|
||||
* @param amountToBuy The amount of purchases to calculate the cost for
|
||||
* @param cumulativeCost Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
|
||||
* @param directSum How many purchases to manually sum for improved accuracy. If not specified, defaults to 10 when cost is cumulative and 0 when not
|
||||
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
|
||||
* @param summedPurchases How many purchases to manually sum for improved accuracy. If not specified, defaults to 10 when spending resources and 0 when not
|
||||
*/
|
||||
export function calculateCost(
|
||||
formula: InvertibleFormula,
|
||||
amountToBuy: DecimalSource,
|
||||
cumulativeCost?: true,
|
||||
directSum?: number
|
||||
spendResources?: true,
|
||||
summedPurchases?: number
|
||||
): DecimalSource;
|
||||
export function calculateCost(
|
||||
formula: InvertibleIntegralFormula,
|
||||
amountToBuy: DecimalSource,
|
||||
cumulativeCost: boolean,
|
||||
directSum?: number
|
||||
spendResources: boolean,
|
||||
summedPurchases?: number
|
||||
): DecimalSource;
|
||||
export function calculateCost(
|
||||
formula: InvertibleFormula,
|
||||
amountToBuy: DecimalSource,
|
||||
cumulativeCost = true,
|
||||
directSum?: number
|
||||
spendResources = true,
|
||||
summedPurchases?: number
|
||||
) {
|
||||
// Single purchase
|
||||
if (Decimal.eq(amountToBuy, 1)) {
|
||||
return formula.evaluate();
|
||||
}
|
||||
|
||||
const origValue = unref(formula.innermostVariable) ?? 0;
|
||||
let newValue = Decimal.add(amountToBuy, origValue);
|
||||
const targetValue = newValue;
|
||||
directSum ??= cumulativeCost ? 10 : 0;
|
||||
newValue = newValue.sub(directSum).clampMin(origValue);
|
||||
let cost: DecimalSource = 0;
|
||||
|
||||
// Indirect sum
|
||||
if (Decimal.gt(amountToBuy, directSum)) {
|
||||
if (!formula.isInvertible()) {
|
||||
console.error("Cannot calculate cost with indirect sum of non-invertible formula");
|
||||
return 0;
|
||||
let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
|
||||
if (spendResources) {
|
||||
if (!formula.isIntegrable()) {
|
||||
throw new Error(
|
||||
"Cannot calculate cost with spending resources of non-integrable formula"
|
||||
);
|
||||
}
|
||||
if (cumulativeCost) {
|
||||
if (!formula.isIntegrable()) {
|
||||
console.error(
|
||||
"Cannot calculate cost with cumulative cost of non-integrable formula"
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
|
||||
if (targetValue.gt(1e308)) {
|
||||
// Too large of a number for directSum to make a difference,
|
||||
// just get the cost and multiply by summed purchases
|
||||
return Decimal.add(
|
||||
cost,
|
||||
Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cost = formula.evaluate(newValue);
|
||||
newValue = newValue.add(1);
|
||||
if (targetValue.gt(1e308)) {
|
||||
// Too large of a number for directSum to make a difference,
|
||||
// just get the cost and multiply by summed purchases
|
||||
return Decimal.sub(targetValue, newValue).add(1).times(cost);
|
||||
}
|
||||
const targetValue = newValue;
|
||||
newValue = newValue
|
||||
.sub(summedPurchases ?? 10)
|
||||
.clampMin(unref(formula.innermostVariable) ?? 0);
|
||||
let cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
|
||||
if (targetValue.gt(1e308)) {
|
||||
// Too large of a number for summedPurchases to make a difference,
|
||||
// just get the cost and multiply by summed purchases
|
||||
return cost.add(Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue)));
|
||||
}
|
||||
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
|
||||
cost = cost.add(formula.evaluate(i));
|
||||
}
|
||||
return cost;
|
||||
} else {
|
||||
const targetValue = newValue;
|
||||
newValue = newValue
|
||||
.sub(summedPurchases ?? 0)
|
||||
.clampMin(unref(formula.innermostVariable) ?? 0);
|
||||
let cost = formula.evaluate(newValue);
|
||||
if (targetValue.gt(1e308)) {
|
||||
// Too large of a number for summedPurchases to make a difference,
|
||||
// just get the cost and multiply by summed purchases
|
||||
return Decimal.sub(targetValue, newValue).add(1).times(cost);
|
||||
}
|
||||
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
|
||||
cost = Decimal.add(cost, formula.evaluate(i));
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
// Direct sum
|
||||
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
|
||||
cost = Decimal.add(cost, formula.evaluate(i));
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
|
||||
import {
|
||||
FormulaSource,
|
||||
GenericFormula,
|
||||
InvertFunction,
|
||||
InvertibleFormula,
|
||||
SubstitutionStack
|
||||
} from "./types";
|
||||
import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
|
||||
|
||||
const ln10 = Decimal.ln(10);
|
||||
|
||||
|
@ -14,33 +8,21 @@ export function passthrough<T extends GenericFormula | DecimalSource>(value: T):
|
|||
return value;
|
||||
}
|
||||
|
||||
export function invertPassthrough(value: DecimalSource, ...inputs: FormulaSource[]) {
|
||||
const variable = inputs.find(input => hasVariable(input)) as InvertibleFormula | undefined;
|
||||
if (variable == null) {
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
}
|
||||
return variable.invert(value);
|
||||
}
|
||||
|
||||
export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.neg(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
return Formula.neg(lhs.getIntegralFormula(stack));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionNeg(value: GenericFormula) {
|
||||
|
@ -53,28 +35,24 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateInnerAdd(
|
||||
|
@ -84,21 +62,18 @@ export function integrateInnerAdd(
|
|||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.add(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.add(x, lhs);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -107,28 +82,24 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateInnerSub(
|
||||
|
@ -138,21 +109,18 @@ export function integrateInnerSub(
|
|||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.sub(x, lhs);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -161,28 +129,24 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.times(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.times(x, lhs);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionMul(
|
||||
|
@ -195,8 +159,7 @@ export function applySubstitutionMul(
|
|||
} else if (hasVariable(rhs)) {
|
||||
return Formula.div(value, lhs);
|
||||
}
|
||||
console.error("Could not apply substitution due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not apply substitution due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -205,28 +168,24 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.div(x, rhs);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.div(lhs, x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function applySubstitutionDiv(
|
||||
|
@ -239,37 +198,32 @@ export function applySubstitutionDiv(
|
|||
} else if (hasVariable(rhs)) {
|
||||
return Formula.mul(value, lhs);
|
||||
}
|
||||
console.error("Could not apply substitution due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not apply substitution due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.recip(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.ln(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow10(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog10(lhs: DecimalSource) {
|
||||
|
@ -281,15 +235,13 @@ function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
|
|||
const numerator = ln10.times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
|
@ -297,8 +249,7 @@ export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
|
|||
invert: internalInvertIntegralLog10
|
||||
});
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -307,8 +258,7 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
|
||||
|
@ -320,15 +270,13 @@ function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs
|
|||
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack), rhs],
|
||||
|
@ -336,16 +284,14 @@ export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs:
|
|||
invert: internalInvertIntegralLog
|
||||
});
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow(2, value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLog2(lhs: DecimalSource) {
|
||||
|
@ -357,15 +303,13 @@ function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
|
|||
const numerator = Decimal.ln(2).times(value);
|
||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
|
@ -373,16 +317,14 @@ export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
|
|||
invert: internalInvertIntegralLog2
|
||||
});
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLn(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.exp(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
function internalIntegrateLn(lhs: DecimalSource) {
|
||||
|
@ -393,15 +335,13 @@ function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
|
|||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw()));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
return new Formula({
|
||||
inputs: [lhs.getIntegralFormula(stack)],
|
||||
|
@ -409,8 +349,7 @@ export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
|
|||
invert: internalInvertIntegralLn
|
||||
});
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -419,81 +358,70 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
const pow = Formula.add(rhs, 1);
|
||||
return Formula.pow(x, pow).div(pow);
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
return Formula.pow(lhs, x).div(Formula.ln(lhs));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.root(value, 10));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow10(x).div(Formula.ln(10));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(rhs))));
|
||||
return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
|
||||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow(rhs, x).div(Formula.ln(rhs));
|
||||
} else if (hasVariable(rhs)) {
|
||||
if (!rhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = rhs.getIntegralFormula(stack);
|
||||
const denominator = Formula.add(lhs, 1);
|
||||
return Formula.pow(x, denominator).div(denominator);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
|
@ -502,42 +430,36 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
|
|||
} else if (hasVariable(rhs)) {
|
||||
return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertExp(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.ln(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.exp(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function tetrate(
|
||||
|
@ -552,17 +474,14 @@ 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)) {
|
||||
return base.invert(Decimal.ssqrt(value));
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function iteratedexp(
|
||||
|
@ -578,7 +497,6 @@ export function invertIteratedExp(
|
|||
value: DecimalSource,
|
||||
lhs: FormulaSource,
|
||||
height: FormulaSource,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
payload: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
|
@ -591,8 +509,7 @@ export function invertIteratedExp(
|
|||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function iteratedLog(
|
||||
|
@ -616,8 +533,7 @@ export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: Formul
|
|||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
|
||||
|
@ -629,7 +545,6 @@ export function invertLayeradd(
|
|||
value: DecimalSource,
|
||||
lhs: FormulaSource,
|
||||
diff: FormulaSource,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
base: FormulaSource
|
||||
) {
|
||||
if (hasVariable(lhs)) {
|
||||
|
@ -641,24 +556,21 @@ export function invertLayeradd(
|
|||
);
|
||||
}
|
||||
// Other params can't be inverted ATM
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.pow(Math.E, value).times(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tetrate(value, 2));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
|
||||
|
@ -670,262 +582,226 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
|
|||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.asin(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cos(x).neg();
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertCos(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.acos(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sin(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertTan(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.atan(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cos(x).ln().neg();
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.sin(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.asin(x)
|
||||
.times(x)
|
||||
.add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.cos(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.acos(x)
|
||||
.times(x)
|
||||
.sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tan(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.atan(x)
|
||||
.times(x)
|
||||
.sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.asinh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cosh(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.acosh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.sinh(x);
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.atanh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.cosh(x).ln();
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.sinh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt());
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.cosh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.acosh(x)
|
||||
.times(x)
|
||||
.sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.tanh(value));
|
||||
}
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
throw new Error("Could not invert due to no input being a variable");
|
||||
}
|
||||
|
||||
export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
if (!lhs.isIntegrable()) {
|
||||
console.error("Could not integrate due to variable not being integrable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to variable not being integrable");
|
||||
}
|
||||
const x = lhs.getIntegralFormula(stack);
|
||||
return Formula.atanh(x)
|
||||
.times(x)
|
||||
.add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
|
||||
}
|
||||
console.error("Could not integrate due to no input being a variable");
|
||||
return Formula.constant(0);
|
||||
throw new Error("Could not integrate due to no input being a variable");
|
||||
}
|
||||
|
||||
export function createPassthroughBinaryFormula(
|
||||
|
|
8
src/game/formulas/types.d.ts
vendored
8
src/game/formulas/types.d.ts
vendored
|
@ -37,13 +37,9 @@ type SubstitutionFunction<T> = (
|
|||
...inputs: T
|
||||
) => GenericFormula;
|
||||
|
||||
type VariableFormulaOptions = {
|
||||
variable: ProcessedComputable<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
|
||||
type ConstantFormulaOptions = {
|
||||
inputs: [FormulaSource];
|
||||
description?: string;
|
||||
};
|
||||
type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
||||
inputs: T;
|
||||
|
@ -52,7 +48,6 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
|
|||
integrate?: IntegrateFunction<T>;
|
||||
integrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
description?: string;
|
||||
};
|
||||
type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
|
||||
| VariableFormulaOptions
|
||||
|
@ -68,7 +63,6 @@ type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
|
|||
internalIntegrateInner?: IntegrateFunction<T>;
|
||||
applySubstitution?: SubstitutionFunction<T>;
|
||||
innermostVariable?: ProcessedComputable<DecimalSource>;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
|
||||
|
|
|
@ -43,7 +43,7 @@ function update() {
|
|||
loadingSave.value = false;
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != null) {
|
||||
if (player.offlineTime != undefined) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ function update() {
|
|||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != null) {
|
||||
if (player.devSpeed != undefined) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,3 @@ export async function startGameLoop() {
|
|||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
state.mouseActivity = [...state.mouseActivity.slice(-7), false];
|
||||
}, 1000 * 60 * 60);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Modal from "components/modals/Modal.vue";
|
||||
import Modal from "components/Modal.vue";
|
||||
import type {
|
||||
CoercableComponent,
|
||||
JSXFunction,
|
||||
|
@ -225,9 +225,7 @@ export function createLayer<T extends LayerOptions>(
|
|||
addingLayers[addingLayers.length - 1] == null ||
|
||||
addingLayers[addingLayers.length - 1] !== id
|
||||
) {
|
||||
throw new Error(
|
||||
`Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`
|
||||
);
|
||||
throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`;
|
||||
}
|
||||
addingLayers.pop();
|
||||
|
||||
|
|
|
@ -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<CoercableComponent>;
|
||||
}
|
||||
|
||||
/** Utility type that represents the output of all modifiers that represent a single operation. */
|
||||
export type OperationModifier<T> = WithRequired<
|
||||
Modifier,
|
||||
"invert" | "getFormula" | Extract<RequiredKeys<T>, 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<T, S> = undefined extends T
|
||||
? undefined extends S
|
||||
? Omit<WithRequired<Modifier, "invert" | "getFormula">, "description" | "enabled">
|
||||
: Omit<WithRequired<Modifier, "invert" | "enabled" | "getFormula">, "description">
|
||||
: undefined extends S
|
||||
? Omit<WithRequired<Modifier, "invert" | "description" | "getFormula">, "enabled">
|
||||
: WithRequired<Modifier, "invert" | "enabled" | "description" | "getFormula">;
|
||||
|
||||
/** 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<T extends AdditiveModifierOptions, S = OperationModifier<T>>(
|
||||
export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
) {
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(feature => {
|
||||
const { addend, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||
feature,
|
||||
|
@ -106,7 +111,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as S;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/** 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<T>
|
||||
>(optionsFunc: OptionsFunc<T>) {
|
||||
export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(feature => {
|
||||
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||
feature,
|
||||
|
@ -171,7 +175,7 @@ export function createMultiplicativeModifier<
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as S;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/** 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<T>
|
||||
>(optionsFunc: OptionsFunc<T>) {
|
||||
export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
return createLazyProxy(feature => {
|
||||
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
||||
optionsFunc.call(feature, feature);
|
||||
|
@ -260,7 +263,7 @@ export function createExponentialModifier<
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as S;
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,9 +274,11 @@ export function createExponentialModifier<
|
|||
* @see {@link createModifierSection}.
|
||||
*/
|
||||
export function createSequentialModifier<
|
||||
T extends Modifier,
|
||||
S = WithRequired<Modifier, Extract<RequiredKeys<T>, keyof Modifier>>
|
||||
>(modifiersFunc: () => T[]) {
|
||||
T extends Modifier[],
|
||||
S = T extends WithRequired<Modifier, "invert">[]
|
||||
? WithRequired<Modifier, "description" | "invert">
|
||||
: Omit<WithRequired<Modifier, "description">, "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}. */
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import { globalBus } from "game/events";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
import { addingLayers, persistentRefs } from "game/layers";
|
||||
|
@ -9,7 +8,6 @@ import type { Ref, WritableComputedRef } from "vue";
|
|||
import { computed, isReactive, isRef, ref } from "vue";
|
||||
import player from "./player";
|
||||
import state from "./state";
|
||||
import Formula from "./formulas/formulas";
|
||||
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
|
@ -62,8 +60,6 @@ export type State =
|
|||
| number
|
||||
| boolean
|
||||
| DecimalSource
|
||||
| null
|
||||
| undefined
|
||||
| { [key: string]: State }
|
||||
| { [key: number]: State };
|
||||
|
||||
|
@ -118,7 +114,12 @@ function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T)
|
|||
state.NaNPath = persistent[SaveDataPath];
|
||||
state.NaNPersistent = persistent as Persistent<DecimalSource>;
|
||||
}
|
||||
console.error(`Attempted to save NaN value to ${persistent[SaveDataPath]?.join(".")}`);
|
||||
console.error(
|
||||
`Attempted to save NaN value to`,
|
||||
persistent[SaveDataPath]?.join("."),
|
||||
persistent
|
||||
);
|
||||
throw new Error("Attempted to set NaN value. See above for details");
|
||||
}
|
||||
persistent[PersistentState].value = value;
|
||||
}
|
||||
|
@ -229,7 +230,7 @@ export function noPersist<T extends Persistent<S>, S extends State>(persistent:
|
|||
if (key === PersistentState) {
|
||||
return false;
|
||||
}
|
||||
if (key === SkipPersistence) {
|
||||
if (key == SkipPersistence) {
|
||||
return true;
|
||||
}
|
||||
return Reflect.has(target, key);
|
||||
|
@ -281,7 +282,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
// Handle SaveDataPath
|
||||
const newPath = [layer.id, ...path, key];
|
||||
if (
|
||||
value[SaveDataPath] != null &&
|
||||
value[SaveDataPath] != undefined &&
|
||||
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
||||
) {
|
||||
console.error(
|
||||
|
@ -289,8 +290,8 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
"."
|
||||
)}\` when it's already present at \`${value[SaveDataPath].join(
|
||||
"."
|
||||
)}\`.`,
|
||||
"This can cause unexpected behavior when loading saves between updates."
|
||||
)}\`. This can cause unexpected behavior when loading saves between updates.`,
|
||||
value
|
||||
);
|
||||
}
|
||||
value[SaveDataPath] = newPath;
|
||||
|
@ -323,7 +324,6 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
}
|
||||
} else if (
|
||||
!(value instanceof Decimal) &&
|
||||
!(value instanceof Formula) &&
|
||||
!isRef(value) &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
!features.includes(value as { type: typeof Symbol })
|
||||
|
@ -341,7 +341,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
// Show warning for persistent values inside arrays
|
||||
// TODO handle arrays better
|
||||
if (foundPersistentInChild) {
|
||||
if (isArray(value) && !isArray(obj)) {
|
||||
if (Array.isArray(value) && !Array.isArray(obj)) {
|
||||
console.warn(
|
||||
"Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.",
|
||||
ProxyState in obj
|
||||
|
@ -365,9 +365,9 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
return;
|
||||
}
|
||||
console.error(
|
||||
`Created persistent ref in ${layer.id} without registering it to the layer!`,
|
||||
"Make sure to include everything persistent in the returned object.\n\nCreated at:\n" +
|
||||
persistent[StackTrace]
|
||||
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
|
||||
persistent,
|
||||
"\nCreated at:\n" + persistent[StackTrace]
|
||||
);
|
||||
});
|
||||
persistentRefs[layer.id].clear();
|
||||
|
|
|
@ -64,8 +64,7 @@ export default window.player = player;
|
|||
|
||||
/** Convert a player save data object into a JSON string. Unwraps refs. */
|
||||
export function stringifySave(player: Player): string {
|
||||
// Convert undefineds into nulls for proper parsing
|
||||
return JSON.stringify(player, (key, value) => unref(value) ?? null);
|
||||
return JSON.stringify(player, (key, value) => unref(value));
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { isArray } from "@vue/shared";
|
||||
import {
|
||||
CoercableComponent,
|
||||
isVisible,
|
||||
|
@ -86,15 +85,7 @@ export interface CostRequirementOptions {
|
|||
* When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
|
||||
* @see {Formula}
|
||||
*/
|
||||
cumulativeCost?: Computable<boolean>;
|
||||
/**
|
||||
* Upper limit on levels that can be performed at once. Defaults to 1.
|
||||
*/
|
||||
maxBulkAmount?: Computable<DecimalSource>;
|
||||
/**
|
||||
* When calculating requirement for multiple levels, how many should be directly summed for increase accuracy. High numbers can cause lag. Defaults to 10 if cumulative cost, 0 otherwise.
|
||||
*/
|
||||
directSum?: Computable<number>;
|
||||
spendResources?: Computable<boolean>;
|
||||
/**
|
||||
* Pass-through to {@link Requirement.pay}. May be required for maximizing support.
|
||||
* @see {@link cost} for restrictions on maximizing support.
|
||||
|
@ -108,7 +99,7 @@ export type CostRequirement = Replace<
|
|||
cost: ProcessedComputable<DecimalSource> | GenericFormula;
|
||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
||||
requiresPay: ProcessedComputable<boolean>;
|
||||
cumulativeCost: ProcessedComputable<boolean>;
|
||||
spendResources: ProcessedComputable<boolean>;
|
||||
canMaximize: ProcessedComputable<boolean>;
|
||||
}
|
||||
>;
|
||||
|
@ -134,12 +125,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
|||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost) as boolean,
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
|
@ -151,12 +137,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
|||
{displayResource(
|
||||
req.resource,
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost) as boolean,
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)}{" "}
|
||||
{req.resource.displayName}
|
||||
|
@ -168,64 +149,54 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
|||
processComputable(req as T, "cost");
|
||||
processComputable(req as T, "requiresPay");
|
||||
setDefault(req, "requiresPay", true);
|
||||
processComputable(req as T, "cumulativeCost");
|
||||
setDefault(req, "cumulativeCost", true);
|
||||
processComputable(req as T, "maxBulkAmount");
|
||||
setDefault(req, "maxBulkAmount", 1);
|
||||
processComputable(req as T, "directSum");
|
||||
processComputable(req as T, "spendResources");
|
||||
setDefault(req, "spendResources", true);
|
||||
setDefault(req, "pay", function (amount?: DecimalSource) {
|
||||
const cost =
|
||||
req.cost instanceof Formula
|
||||
? calculateCost(
|
||||
req.cost,
|
||||
amount ?? 1,
|
||||
unref(req.cumulativeCost) as boolean,
|
||||
unref(req.directSum) as number
|
||||
)
|
||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
||||
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
||||
});
|
||||
|
||||
req.canMaximize = computed(() => {
|
||||
if (!(req.cost instanceof Formula)) {
|
||||
return false;
|
||||
}
|
||||
const maxBulkAmount = unref(req.maxBulkAmount as ProcessedComputable<DecimalSource>);
|
||||
if (Decimal.lte(maxBulkAmount, 1)) {
|
||||
return false;
|
||||
}
|
||||
const cumulativeCost = unref(req.cumulativeCost as ProcessedComputable<boolean>);
|
||||
const directSum =
|
||||
unref(req.directSum as ProcessedComputable<number>) ?? (cumulativeCost ? 10 : 0);
|
||||
if (Decimal.lte(maxBulkAmount, directSum)) {
|
||||
return true;
|
||||
}
|
||||
if (!req.cost.isInvertible()) {
|
||||
return false;
|
||||
}
|
||||
if (cumulativeCost === true && !req.cost.isIntegrable()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
req.canMaximize = computed(
|
||||
() =>
|
||||
req.cost instanceof Formula &&
|
||||
req.cost.isInvertible() &&
|
||||
(unref(req.spendResources) === false || req.cost.isIntegrable())
|
||||
);
|
||||
|
||||
if (req.cost instanceof Formula) {
|
||||
req.requirementMet = calculateMaxAffordable(
|
||||
if (req.cost instanceof Formula && req.cost.isInvertible()) {
|
||||
const maxAffordable = calculateMaxAffordable(
|
||||
req.cost,
|
||||
req.resource,
|
||||
req.cumulativeCost ?? true,
|
||||
req.directSum,
|
||||
req.maxBulkAmount
|
||||
unref(req.spendResources) as boolean
|
||||
);
|
||||
req.requirementMet = computed(() => {
|
||||
if (unref(req.canMaximize)) {
|
||||
return maxAffordable.value;
|
||||
} else {
|
||||
if (req.cost instanceof Formula) {
|
||||
return Decimal.gte(req.resource.value, req.cost.evaluate());
|
||||
} else {
|
||||
return Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
req.requirementMet = computed(() =>
|
||||
Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
req.requirementMet = computed(() => {
|
||||
if (req.cost instanceof Formula) {
|
||||
return Decimal.gte(req.resource.value, req.cost.evaluate());
|
||||
} else {
|
||||
return Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return req as CostRequirement;
|
||||
|
@ -269,7 +240,7 @@ export function createBooleanRequirement(
|
|||
* @param requirements The 1+ requirements to check
|
||||
*/
|
||||
export function requirementsMet(requirements: Requirements): boolean {
|
||||
if (isArray(requirements)) {
|
||||
if (Array.isArray(requirements)) {
|
||||
return requirements.every(requirementsMet);
|
||||
}
|
||||
const reqsMet = unref(requirements.requirementMet);
|
||||
|
@ -281,7 +252,7 @@ export function requirementsMet(requirements: Requirements): boolean {
|
|||
* @param requirements The 1+ requirements to check
|
||||
*/
|
||||
export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
||||
if (isArray(requirements)) {
|
||||
if (Array.isArray(requirements)) {
|
||||
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
|
||||
}
|
||||
const reqsMet = unref(requirements.requirementMet);
|
||||
|
@ -299,13 +270,13 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource {
|
|||
* @param amount The amount of levels earned to be displayed
|
||||
*/
|
||||
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||
if (isArray(requirements)) {
|
||||
if (Array.isArray(requirements)) {
|
||||
requirements = requirements.filter(r => isVisible(r.visibility));
|
||||
if (requirements.length === 1) {
|
||||
requirements = requirements[0];
|
||||
}
|
||||
}
|
||||
if (isArray(requirements)) {
|
||||
if (Array.isArray(requirements)) {
|
||||
requirements = requirements.filter(r => "partialDisplay" in r);
|
||||
const withCosts = requirements.filter(r => unref(r.requiresPay));
|
||||
const withoutCosts = requirements.filter(r => !unref(r.requiresPay));
|
||||
|
@ -343,7 +314,7 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS
|
|||
* @param amount How many levels to pay for
|
||||
*/
|
||||
export function payRequirements(requirements: Requirements, amount: DecimalSource = 1) {
|
||||
if (isArray(requirements)) {
|
||||
if (Array.isArray(requirements)) {
|
||||
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
|
||||
} else if (unref(requirements.requiresPay)) {
|
||||
requirements.pay?.(amount);
|
||||
|
@ -356,7 +327,7 @@ export function payByDivision(this: CostRequirement, amount?: DecimalSource) {
|
|||
? calculateCost(
|
||||
this.cost,
|
||||
amount ?? 1,
|
||||
unref(this.cumulativeCost as ProcessedComputable<boolean> | undefined) ?? true
|
||||
unref(this.spendResources as ProcessedComputable<boolean> | undefined) ?? true
|
||||
)
|
||||
: unref(this.cost as ProcessedComputable<DecimalSource>);
|
||||
this.resource.value = Decimal.div(this.resource.value, cost);
|
||||
|
|
|
@ -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. */
|
||||
|
@ -20,8 +20,6 @@ export interface Settings {
|
|||
unthrottled: boolean;
|
||||
/** Whether to align modifiers to the unit. */
|
||||
alignUnits: boolean;
|
||||
/** Whether or not to show a video game health warning after playing excessively. */
|
||||
showHealthWarning: boolean;
|
||||
}
|
||||
|
||||
const state = reactive<Partial<Settings>>({
|
||||
|
@ -30,8 +28,7 @@ const state = reactive<Partial<Settings>>({
|
|||
showTPS: true,
|
||||
theme: Themes.Nordic,
|
||||
unthrottled: false,
|
||||
alignUnits: false,
|
||||
showHealthWarning: true
|
||||
alignUnits: false
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -59,15 +56,12 @@ declare global {
|
|||
export default window.settings = state as Settings;
|
||||
/** A function that erases all player settings, including all saves. */
|
||||
export const hardResetSettings = (window.hardResetSettings = () => {
|
||||
// Only partial because of any properties that are only added during the loadSettings event.
|
||||
const settings: Partial<Settings> = {
|
||||
const settings = {
|
||||
active: "",
|
||||
saves: [],
|
||||
showTPS: true,
|
||||
theme: Themes.Nordic,
|
||||
unthrottled: false,
|
||||
alignUnits: false,
|
||||
showHealthWarning: true
|
||||
alignUnits: false
|
||||
};
|
||||
globalBus.emit("loadSettings", settings);
|
||||
Object.assign(state, settings);
|
||||
|
@ -84,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;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import type { DecimalSource } from "util/bignum";
|
||||
import { reactive, shallowReactive } from "vue";
|
||||
import { shallowReactive } from "vue";
|
||||
import type { Persistent } from "./persistence";
|
||||
|
||||
/** An object of global data that is not persistent. */
|
||||
export interface Transient {
|
||||
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
|
||||
lastTenTicks: number[];
|
||||
/** A list of bools represnting which of the last few hours had mouse activity. */
|
||||
mouseActivity: boolean[];
|
||||
/** Whether or not a NaN value has been detected and undealt with. */
|
||||
hasNaN: boolean;
|
||||
/** The location within the player save data object of the NaN value. */
|
||||
NaNPath?: string[];
|
||||
/** The ref that was being set to NaN. */
|
||||
NaNPersistent?: Persistent<DecimalSource>;
|
||||
/** List of errors that have occurred, to show the user. */
|
||||
errors: Error[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -27,8 +23,6 @@ declare global {
|
|||
/** The global transient state object. */
|
||||
export default window.state = shallowReactive<Transient>({
|
||||
lastTenTicks: [],
|
||||
mouseActivity: [false],
|
||||
hasNaN: false,
|
||||
NaNPath: [],
|
||||
errors: reactive([])
|
||||
NaNPath: []
|
||||
});
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { Application } from "@pixi/app";
|
||||
import { BatchRenderer, Renderer } from "@pixi/core";
|
||||
import { TickerPlugin } from "@pixi/ticker";
|
||||
|
||||
Application.registerPlugin(TickerPlugin);
|
||||
|
||||
Renderer.registerPlugin("batch", BatchRenderer);
|
17
src/lib/pwa-register.d.ts
vendored
17
src/lib/pwa-register.d.ts
vendored
|
@ -1,17 +0,0 @@
|
|||
declare module 'virtual:pwa-register/vue' {
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export interface RegisterSWOptions {
|
||||
immediate?: boolean
|
||||
onNeedRefresh?: () => void
|
||||
onOfflineReady?: () => void
|
||||
onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void
|
||||
onRegisterError?: (error: any) => void
|
||||
}
|
||||
|
||||
export function useRegisterSW(options?: RegisterSWOptions): {
|
||||
needRefresh: Ref<boolean>
|
||||
offlineReady: Ref<boolean>
|
||||
updateServiceWorker: (reloadPage?: boolean) => Promise<void>
|
||||
}
|
||||
}
|
|
@ -66,7 +66,3 @@ ul {
|
|||
.Vue-Toastification__toast {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
30
src/main.ts
30
src/main.ts
|
@ -2,13 +2,11 @@ import "@fontsource/material-icons";
|
|||
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 { 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 {
|
||||
/**
|
||||
|
@ -25,32 +23,11 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
const error = console.error;
|
||||
console.error = function (...args) {
|
||||
if (import.meta.env.DEV) {
|
||||
state.errors.push(new Error(args[0], { cause: args[1] }));
|
||||
}
|
||||
error(...args);
|
||||
};
|
||||
|
||||
window.onerror = function (event, source, lineno, colno, err) {
|
||||
state.errors.push(err instanceof Error ? err : new Error(JSON.stringify(err)));
|
||||
error(err);
|
||||
return true;
|
||||
};
|
||||
window.onunhandledrejection = function (event) {
|
||||
state.errors.push(
|
||||
event.reason instanceof Error ? event.reason : new Error(JSON.stringify(event.reason))
|
||||
);
|
||||
error(event.reason);
|
||||
};
|
||||
|
||||
document.title = projInfo.title;
|
||||
window.projInfo = projInfo;
|
||||
if (projInfo.id === "") {
|
||||
console.error(
|
||||
"Project ID is empty!",
|
||||
"Please select a unique ID for this project in /src/data/projInfo.json"
|
||||
throw new Error(
|
||||
"Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -66,9 +43,6 @@ requestAnimationFrame(async () => {
|
|||
|
||||
// Create Vue
|
||||
const vue = (window.vue = createApp(App));
|
||||
vue.config.errorHandler = function (err, instance, info) {
|
||||
console.error(err, info, instance);
|
||||
};
|
||||
globalBus.emit("setupVue", vue);
|
||||
vue.mount("#app");
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export function exponentialFormat(num: DecimalSource, precision: number, mantiss
|
|||
}
|
||||
|
||||
export function commaFormat(num: DecimalSource, precision: number): string {
|
||||
if (num == null) {
|
||||
if (num === null || num === undefined) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
@ -36,12 +36,12 @@ export function commaFormat(num: DecimalSource, precision: number): string {
|
|||
const init = num.toStringWithDecimalPlaces(precision);
|
||||
const portions = init.split(".");
|
||||
portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
|
||||
if (portions.length === 1) return portions[0];
|
||||
if (portions.length == 1) return portions[0];
|
||||
return portions[0] + "." + portions[1];
|
||||
}
|
||||
|
||||
export function regularFormat(num: DecimalSource, precision: number): string {
|
||||
if (num == null) {
|
||||
if (num === null || num === undefined) {
|
||||
return "NaN";
|
||||
}
|
||||
num = new Decimal(num);
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
export type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: NonNullable<unknown> extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
export type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: NonNullable<unknown> extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type OmitOptional<T> = Pick<T, RequiredKeys<T>>;
|
||||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
export type ArrayElements<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer S>
|
||||
|
@ -20,11 +12,6 @@ export function camelToTitle(camel: string): string {
|
|||
return title;
|
||||
}
|
||||
|
||||
export function camelToKebab(camel: string) {
|
||||
// Split off first character so function works on upper camel (pascal) case
|
||||
return (camel[0] + camel.slice(1).replace(/[A-Z]/g, c => `-${c}`)).toLowerCase();
|
||||
}
|
||||
|
||||
export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
|
||||
functionOrValue: ((...args: S) => T) | R
|
||||
): functionOrValue is (...args: S) => T {
|
||||
|
|
|
@ -1,185 +0,0 @@
|
|||
import { LoadablePlayerData } from "components/modals/SavesManager.vue";
|
||||
import player, { Player, stringifySave } from "game/player";
|
||||
import settings from "game/settings";
|
||||
import LZString from "lz-string";
|
||||
import { GalaxyApi, initGalaxy } from "unofficial-galaxy-sdk";
|
||||
import { ref } from "vue";
|
||||
import { decodeSave, loadSave, save, setupInitialStore } from "./save";
|
||||
|
||||
export const galaxy = ref<GalaxyApi>();
|
||||
export const conflictingSaves = ref<
|
||||
{ id: string; local: LoadablePlayerData; cloud: LoadablePlayerData; slot: number }[]
|
||||
>([]);
|
||||
export const syncedSaves = ref<string[]>([]);
|
||||
|
||||
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<Player> & { 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 == null) {
|
||||
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<Player> | 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;
|
||||
}
|
|
@ -40,7 +40,7 @@ export function createLazyProxy<T extends object, S extends T>(
|
|||
function calculateObj(): T {
|
||||
if (!calculated) {
|
||||
if (calculating) {
|
||||
console.error("Cyclical dependency detected. Cannot evaluate lazy proxy.");
|
||||
throw new Error("Cyclical dependency detected. Cannot evaluate lazy proxy.");
|
||||
}
|
||||
calculating = true;
|
||||
Object.assign(obj, objectFunc.call(obj, obj));
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { LoadablePlayerData } from "components/modals/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 LZString from "lz-string";
|
||||
import { ref, shallowReactive } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
export function setupInitialStore(player: Partial<Player> = {}): Player {
|
||||
return Object.assign(
|
||||
|
@ -43,9 +42,17 @@ export async function load(): Promise<void> {
|
|||
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 +67,6 @@ export async function load(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
|
@ -119,7 +109,7 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
|||
playerObj.time &&
|
||||
playerObj.devSpeed !== 0
|
||||
) {
|
||||
if (playerObj.offlineTime == null) playerObj.offlineTime = 0;
|
||||
if (playerObj.offlineTime == undefined) playerObj.offlineTime = 0;
|
||||
playerObj.offlineTime += Math.min(
|
||||
playerObj.offlineTime + (Date.now() - playerObj.time) / 1000,
|
||||
projInfo.offlineLimit * 3600
|
||||
|
@ -137,40 +127,6 @@ export async function loadSave(playerObj: Partial<Player>): Promise<void> {
|
|||
globalBus.emit("onLoad");
|
||||
}
|
||||
|
||||
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
|
||||
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();
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
unref,
|
||||
watchEffect
|
||||
} from "vue";
|
||||
import { camelToKebab } from "./common";
|
||||
|
||||
export function coerceComponent(
|
||||
component: CoercableComponent,
|
||||
|
@ -126,7 +125,9 @@ export function setupHoldToClick(
|
|||
handleHolding: VoidFunction;
|
||||
} {
|
||||
const interval = ref<NodeJS.Timer | null>(null);
|
||||
const event = ref<MouseEvent | TouchEvent | undefined>(undefined);
|
||||
const event = ref<MouseEvent | TouchEvent | undefined>(undefined) as Ref<
|
||||
MouseEvent | TouchEvent | undefined
|
||||
>;
|
||||
|
||||
function start(e: MouseEvent | TouchEvent) {
|
||||
if (!interval.value) {
|
||||
|
@ -191,7 +192,7 @@ export function computeOptionalComponent(
|
|||
watchEffect(() => {
|
||||
const currComponent = unwrapRef(component);
|
||||
comp.value =
|
||||
currComponent === "" || currComponent == null
|
||||
currComponent == "" || currComponent == null
|
||||
? null
|
||||
: coerceComponent(currComponent, defaultWrapper);
|
||||
});
|
||||
|
@ -242,10 +243,3 @@ export function trackHover(element: VueFeature): Ref<boolean> {
|
|||
|
||||
return isHovered;
|
||||
}
|
||||
|
||||
export function kebabifyObject(object: Record<string, unknown>) {
|
||||
return Object.keys(object).reduce((acc, curr) => {
|
||||
acc[camelToKebab(curr)] = object[curr];
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>);
|
||||
}
|
||||
|
|
|
@ -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(() => ({
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
import { createHotkey, hotkeys } from "features/hotkey";
|
||||
import { afterEach, describe, expect, onTestFailed, test } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
|
||||
function createSuccessHotkey(key: string, triggered: Ref<boolean>) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "",
|
||||
key: key,
|
||||
onPress: () => (triggered.value = true)
|
||||
}));
|
||||
}
|
||||
|
||||
function createFailHotkey(key: string) {
|
||||
hotkeys[key] = createHotkey(() => ({
|
||||
description: "Fail test",
|
||||
key,
|
||||
onPress: () => expect(true).toBe(false)
|
||||
}));
|
||||
}
|
||||
|
||||
function mockKeypress(key: string, shiftKey = false, ctrlKey = false) {
|
||||
const event = new KeyboardEvent("keydown", { key, shiftKey, ctrlKey });
|
||||
expect(document.dispatchEvent(event)).toBe(true);
|
||||
return event;
|
||||
}
|
||||
|
||||
function testHotkey(pass: string, fail: string, key: string, shiftKey = false, ctrlKey = false) {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(pass, triggered);
|
||||
createFailHotkey(fail);
|
||||
mockKeypress(key, shiftKey, ctrlKey);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
|
||||
describe("Hotkeys fire correctly", () => {
|
||||
afterEach(() => {
|
||||
Object.keys(hotkeys).forEach(key => delete hotkeys[key]);
|
||||
});
|
||||
|
||||
test("Lower case letters", () => testHotkey("a", "A", "a"));
|
||||
|
||||
test.each([["A"], ["shift+a"], ["shift+A"]])("Upper case letters using %s as key", key => {
|
||||
testHotkey(key, "a", "A", true);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[0, ")"],
|
||||
[1, "!"],
|
||||
[2, "@"],
|
||||
[3, "#"],
|
||||
[4, "$"],
|
||||
[5, "%"],
|
||||
[6, "^"],
|
||||
[7, "&"],
|
||||
[8, "*"],
|
||||
[9, "("]
|
||||
])("Handle number %i and it's 'capital', %s", (number, symbol) => {
|
||||
test("Triggering number", () =>
|
||||
testHotkey(number.toString(), symbol, number.toString(), true));
|
||||
test.each([symbol, `shift+${number}`, `shift+${symbol}`])(
|
||||
"Triggering symbol using %s as key",
|
||||
key => testHotkey(key, number.toString(), symbol, true)
|
||||
);
|
||||
});
|
||||
|
||||
test("Ctrl modifier", () => testHotkey("ctrl+a", "a", "a", false, true));
|
||||
|
||||
test.each(["shift+ctrl+a", "ctrl+shift+a", "shift+ctrl+A", "ctrl+shift+A"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("a");
|
||||
createFailHotkey("A");
|
||||
createFailHotkey("shift+A");
|
||||
createFailHotkey("shift+a");
|
||||
createFailHotkey("ctrl+a");
|
||||
createFailHotkey("ctrl+A");
|
||||
mockKeypress("a", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
test.each(["shift+ctrl+1", "ctrl+shift+1", "shift+ctrl+!", "ctrl+shift+!"])(
|
||||
"Shift and Ctrl modifiers using %s as key",
|
||||
key => {
|
||||
const triggered = ref(false);
|
||||
createSuccessHotkey(key, triggered);
|
||||
createFailHotkey("1");
|
||||
createFailHotkey("!");
|
||||
createFailHotkey("shift+1");
|
||||
createFailHotkey("shift+!");
|
||||
createFailHotkey("ctrl+1");
|
||||
createFailHotkey("ctrl+!");
|
||||
mockKeypress("!", true, true);
|
||||
expect(triggered.value).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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<boolean>, shouldNotReset: Ref<boolean>;
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -13,13 +13,9 @@ import { InvertibleIntegralFormula } from "game/formulas/types";
|
|||
|
||||
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
|
||||
|
||||
const testValues = [-2, "0", new Decimal(10.5)] as const;
|
||||
const testValues = [-1, "0", Decimal.dOne] as const;
|
||||
|
||||
const invertibleZeroParamFunctionNames = [
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"neg",
|
||||
"recip",
|
||||
"log10",
|
||||
|
@ -52,6 +48,10 @@ const invertibleZeroParamFunctionNames = [
|
|||
const nonInvertibleZeroParamFunctionNames = [
|
||||
"abs",
|
||||
"sign",
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"pLog10",
|
||||
"absLog10",
|
||||
"factorial",
|
||||
|
@ -85,10 +85,6 @@ const integrableZeroParamFunctionNames = [
|
|||
] as const;
|
||||
const nonIntegrableZeroParamFunctionNames = [
|
||||
...nonInvertibleZeroParamFunctionNames,
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"lambertw",
|
||||
"ssqrt"
|
||||
] as const;
|
||||
|
@ -231,15 +227,14 @@ describe("Creating Formulas", () => {
|
|||
expect(formula.evaluate()).compare_tolerance(expectedValue));
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
test("Invert errors", () => expect(() => formula.invert(25)).toLogError());
|
||||
test("Integrate errors", () =>
|
||||
test("Invert throws", () => expect(() => formula.invert(25)).toThrow());
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow());
|
||||
test("Invert integral throws", () =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
expect(() => formula.evaluateIntegral()).toLogError());
|
||||
test("Invert integral errors", () =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
expect(() => formula.invertIntegral(25)).toLogError());
|
||||
expect(() => formula.invertIntegral(25)).toThrow());
|
||||
});
|
||||
}
|
||||
testConstant("number", () => Formula.constant(10));
|
||||
|
@ -261,10 +256,10 @@ describe("Creating Formulas", () => {
|
|||
// None of these formulas have variables, so they should all behave the same
|
||||
test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false));
|
||||
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
|
||||
test(`Formula errors if trying to invert`, () =>
|
||||
test(`Formula throws if trying to invert`, () =>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
expect(() => formula.invert(10)).toLogError());
|
||||
expect(() => formula.invert(10)).toThrow());
|
||||
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
|
||||
test("Has a non-invertible integral", () =>
|
||||
expect(formula.isIntegralInvertible()).toBe(false));
|
||||
|
@ -492,20 +487,20 @@ describe("Inverting", () => {
|
|||
});
|
||||
|
||||
test("Inverting nested formulas", () => {
|
||||
const formula = Formula.add(variable, constant).times(constant).floor();
|
||||
const formula = Formula.add(variable, constant).times(constant);
|
||||
expect(formula.invert(100)).compare_tolerance(0);
|
||||
});
|
||||
|
||||
describe("Inverting with non-invertible sections", () => {
|
||||
test("Non-invertible constant", () => {
|
||||
const formula = Formula.add(variable, constant.sign());
|
||||
const formula = Formula.add(variable, constant.ceil());
|
||||
expect(formula.isInvertible()).toBe(true);
|
||||
expect(() => formula.invert(10)).not.toLogError();
|
||||
expect(() => formula.invert(10)).not.toThrow();
|
||||
});
|
||||
test("Non-invertible variable", () => {
|
||||
const formula = Formula.add(variable.sign(), constant);
|
||||
const formula = Formula.add(variable.ceil(), constant);
|
||||
expect(formula.isInvertible()).toBe(false);
|
||||
expect(() => formula.invert(10)).toLogError();
|
||||
expect(() => formula.invert(10)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -629,19 +624,19 @@ describe("Integrating", () => {
|
|||
|
||||
test("Integrating nested complex formulas", () => {
|
||||
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
||||
expect(() => formula.evaluateIntegral()).toLogError();
|
||||
expect(() => formula.evaluateIntegral()).toThrow();
|
||||
});
|
||||
|
||||
describe("Integrating with non-integrable sections", () => {
|
||||
test("Non-integrable constant", () => {
|
||||
const formula = Formula.add(variable, constant.ceil());
|
||||
expect(formula.isIntegrable()).toBe(true);
|
||||
expect(() => formula.evaluateIntegral()).not.toLogError();
|
||||
expect(() => formula.evaluateIntegral()).not.toThrow();
|
||||
});
|
||||
test("Non-integrable variable", () => {
|
||||
const formula = Formula.add(variable.ceil(), constant);
|
||||
expect(formula.isIntegrable()).toBe(false);
|
||||
expect(() => formula.evaluateIntegral()).toLogError();
|
||||
expect(() => formula.evaluateIntegral()).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -662,7 +657,7 @@ describe("Inverting integrals", () => {
|
|||
describe("Invertible Integral functions marked as such", () => {
|
||||
function checkFormula(formula: InvertibleIntegralFormula) {
|
||||
expect(formula.isIntegralInvertible()).toBe(true);
|
||||
expect(() => formula.invertIntegral(10)).not.toLogError();
|
||||
expect(() => formula.invertIntegral(10)).to.not.throw();
|
||||
}
|
||||
invertibleIntegralZeroPramFunctionNames.forEach(name => {
|
||||
describe(name, () => {
|
||||
|
@ -681,7 +676,7 @@ describe("Inverting integrals", () => {
|
|||
test(`${name}(var, var) is marked as not having an invertible integral`, () => {
|
||||
const formula = Formula[name](variable, variable);
|
||||
expect(formula.isIntegralInvertible()).toBe(false);
|
||||
expect(() => formula.invertIntegral(10)).toLogError();
|
||||
expect(() => formula.invertIntegral(10)).to.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -737,7 +732,7 @@ describe("Inverting integrals", () => {
|
|||
|
||||
test("Inverting integral of nested complex formulas", () => {
|
||||
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
||||
expect(() => formula.invertIntegral(100)).toLogError();
|
||||
expect(() => formula.invertIntegral(100)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -770,7 +765,7 @@ describe("Step-wise", () => {
|
|||
);
|
||||
expect(() =>
|
||||
Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral()
|
||||
).toLogError();
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("Formula never marked as having an invertible integral", () => {
|
||||
|
@ -779,7 +774,7 @@ describe("Step-wise", () => {
|
|||
).toBe(false);
|
||||
expect(() =>
|
||||
Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10)
|
||||
).toLogError();
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
||||
|
@ -871,7 +866,7 @@ describe("Conditionals", () => {
|
|||
);
|
||||
expect(() =>
|
||||
Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral()
|
||||
).toLogError();
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("Formula never marked as having an invertible integral", () => {
|
||||
|
@ -880,7 +875,7 @@ describe("Conditionals", () => {
|
|||
).toBe(false);
|
||||
expect(() =>
|
||||
Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10)
|
||||
).toLogError();
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
||||
|
@ -981,7 +976,7 @@ describe("Custom Formulas", () => {
|
|||
evaluate: () => 6,
|
||||
invert: value => value
|
||||
}).invert(10)
|
||||
).toLogError());
|
||||
).toThrow());
|
||||
test("One input inverts correctly", () =>
|
||||
expect(
|
||||
new Formula({
|
||||
|
@ -1008,7 +1003,7 @@ describe("Custom Formulas", () => {
|
|||
evaluate: () => 0,
|
||||
integrate: stack => variable
|
||||
}).evaluateIntegral()
|
||||
).toLogError());
|
||||
).toThrow());
|
||||
test("One input integrates correctly", () =>
|
||||
expect(
|
||||
new Formula({
|
||||
|
@ -1035,7 +1030,7 @@ describe("Custom Formulas", () => {
|
|||
evaluate: () => 0,
|
||||
integrate: stack => variable
|
||||
}).invertIntegral(20)
|
||||
).toLogError());
|
||||
).toThrow());
|
||||
test("One input inverts integral correctly", () =>
|
||||
expect(
|
||||
new Formula({
|
||||
|
@ -1078,19 +1073,12 @@ describe("Buy Max", () => {
|
|||
beforeAll(() => {
|
||||
resource = createResource(ref(100000));
|
||||
});
|
||||
describe("Without cumulative cost", () => {
|
||||
test("Errors on calculating max affordable of non-invertible formula", () => {
|
||||
const purchases = ref(1);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.abs(variable);
|
||||
const maxAffordable = calculateMaxAffordable(formula, resource, false);
|
||||
expect(() => maxAffordable.value).toLogError();
|
||||
});
|
||||
test("Errors on calculating cost of non-invertible formula", () => {
|
||||
const purchases = ref(1);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.abs(variable);
|
||||
expect(() => calculateCost(formula, 5, false, 0)).toLogError();
|
||||
describe("Without spending", () => {
|
||||
test("Throws on formula with non-invertible integral", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
|
||||
expect(() => maxAffordable.value).toThrow();
|
||||
});
|
||||
test("Calculates max affordable and cost correctly", () => {
|
||||
const variable = Formula.variable(0);
|
||||
|
@ -1101,32 +1089,11 @@ describe("Buy Max", () => {
|
|||
Decimal.pow(1.05, 141).times(100)
|
||||
);
|
||||
});
|
||||
test("Calculates max affordable and cost correctly with direct sum", () => {
|
||||
const variable = Formula.variable(0);
|
||||
const formula = Formula.pow(1.05, variable).times(100);
|
||||
const maxAffordable = calculateMaxAffordable(formula, resource, false, 4);
|
||||
expect(maxAffordable.value).compare_tolerance(141 - 4);
|
||||
|
||||
const actualCost = new Array(4)
|
||||
.fill(null)
|
||||
.reduce((acc, _, i) => acc.add(formula.evaluate(133 + i)), new Decimal(0));
|
||||
const calculatedCost = calculateCost(formula, maxAffordable.value, false, 4);
|
||||
expect(calculatedCost).compare_tolerance(actualCost);
|
||||
});
|
||||
});
|
||||
describe("With cumulative cost", () => {
|
||||
test("Errors on calculating max affordable of non-invertible formula", () => {
|
||||
const purchases = ref(1);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.abs(variable);
|
||||
const maxAffordable = calculateMaxAffordable(formula, resource, true);
|
||||
expect(() => maxAffordable.value).toLogError();
|
||||
});
|
||||
test("Errors on calculating cost of non-invertible formula", () => {
|
||||
const purchases = ref(1);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.abs(variable);
|
||||
expect(() => calculateCost(formula, 5, true, 0)).toLogError();
|
||||
describe("With spending", () => {
|
||||
test("Throws on non-invertible formula", () => {
|
||||
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
|
||||
expect(() => maxAffordable.value).toThrow();
|
||||
});
|
||||
test("Estimates max affordable and cost correctly with 0 purchases", () => {
|
||||
const purchases = ref(0);
|
||||
|
@ -1184,7 +1151,7 @@ describe("Buy Max", () => {
|
|||
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
||||
).toBeLessThan(0.1);
|
||||
});
|
||||
test("Estimates max affordable and cost more accurately with direct sum", () => {
|
||||
test("Estimates max affordable and cost more accurately with summing last purchases", () => {
|
||||
const purchases = ref(1);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.pow(1.05, variable).times(100);
|
||||
|
@ -1211,7 +1178,7 @@ describe("Buy Max", () => {
|
|||
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
||||
).toBeLessThan(0.02);
|
||||
});
|
||||
test("Handles direct sum when making few purchases", () => {
|
||||
test("Handles summing purchases when making few purchases", () => {
|
||||
const purchases = ref(90);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = Formula.pow(1.05, variable).times(100);
|
||||
|
@ -1239,7 +1206,7 @@ describe("Buy Max", () => {
|
|||
// Since we're summing all the purchases this should be equivalent
|
||||
expect(calculatedCost).compare_tolerance(actualCost);
|
||||
});
|
||||
test("Handles direct sum when making very few purchases", () => {
|
||||
test("Handles summing purchases when making very few purchases", () => {
|
||||
const purchases = ref(0);
|
||||
const variable = Formula.variable(purchases);
|
||||
const formula = variable.add(1);
|
||||
|
@ -1253,11 +1220,11 @@ describe("Buy Max", () => {
|
|||
(acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
|
||||
new Decimal(0)
|
||||
);
|
||||
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
||||
const calculatedCost = calculateCost(formula, maxAffordable.value, true);
|
||||
// Since we're summing all the purchases this should be equivalent
|
||||
expect(calculatedCost).compare_tolerance(actualCost);
|
||||
});
|
||||
test("Handles direct sum when over e308 purchases", () => {
|
||||
test("Handles summing purchases when over e308 purchases", () => {
|
||||
resource.value = "1ee308";
|
||||
const purchases = ref(0);
|
||||
const variable = Formula.variable(purchases);
|
||||
|
@ -1268,23 +1235,5 @@ describe("Buy Max", () => {
|
|||
expect(Decimal.isFinite(calculatedCost)).toBe(true);
|
||||
resource.value = 100000;
|
||||
});
|
||||
test("Handles direct sum of non-integrable formula", () => {
|
||||
const purchases = ref(0);
|
||||
const formula = Formula.variable(purchases).abs();
|
||||
expect(() => calculateCost(formula, 10)).not.toLogError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stringifies", () => {
|
||||
test("Nested formula", () => {
|
||||
const variable = Formula.variable(ref(0));
|
||||
expect(variable.add(5).pow(Formula.constant(10)).stringify()).toBe(
|
||||
"pow(add(x, 5.00), 10.00)"
|
||||
);
|
||||
});
|
||||
test("Indeterminate", () => {
|
||||
expect(Formula.if(10, true, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||
expect(Formula.step(10, 5, f => f.add(5)).stringify()).toBe("indeterminate");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CoercableComponent, JSXFunction } from "features/feature";
|
||||
import Formula from "game/formulas/formulas";
|
||||
import Formula, { printFormula } from "game/formulas/formulas";
|
||||
import {
|
||||
createAdditiveModifier,
|
||||
createExponentialModifier,
|
||||
|
@ -52,7 +52,7 @@ function testModifiers<
|
|||
expect(modifier.invert(operation(10, 5))).compare_tolerance(10));
|
||||
test("getFormula returns the right formula", () => {
|
||||
const value = ref(10);
|
||||
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||
`${operation.name}(x, 5.00)`
|
||||
);
|
||||
});
|
||||
|
@ -133,14 +133,14 @@ describe("Exponential Modifiers", () =>
|
|||
testModifiers(createExponentialModifier, "exponent", Decimal.pow));
|
||||
|
||||
describe("Sequential Modifiers", () => {
|
||||
function createModifier<T extends Partial<ModifierConstructorOptions>>(
|
||||
function createModifier(
|
||||
value: Computable<DecimalSource>,
|
||||
options?: T
|
||||
) {
|
||||
options: Partial<ModifierConstructorOptions> = {}
|
||||
): WithRequired<Modifier, "invert" | "getFormula"> {
|
||||
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 }))
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ describe("Sequential Modifiers", () => {
|
|||
expect(modifier.invert(Decimal.add(10, 5).times(5).pow(5))).compare_tolerance(10));
|
||||
test("getFormula returns the right formula", () => {
|
||||
const value = ref(10);
|
||||
expect(modifier.getFormula(Formula.variable(value)).stringify()).toBe(
|
||||
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||
`pow(mul(add(x, 5.00), 5.00), 5.00)`
|
||||
);
|
||||
});
|
||||
|
@ -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", () => {
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
Requirement,
|
||||
requirementsMet
|
||||
} from "game/requirements";
|
||||
import Decimal from "util/bignum";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
import { isRef, ref, unref } from "vue";
|
||||
import "../utils";
|
||||
|
@ -27,7 +26,8 @@ describe("Creating cost requirement", () => {
|
|||
beforeAll(() => {
|
||||
requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10
|
||||
cost: 10,
|
||||
spendResources: false
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe("Creating cost requirement", () => {
|
|||
});
|
||||
test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
|
||||
test("requires pay", () => expect(requirement.requiresPay).toBe(true));
|
||||
test("does not spend resources", () => expect(requirement.cumulativeCost).toBe(true));
|
||||
test("does not spend resources", () => expect(requirement.spendResources).toBe(false));
|
||||
test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
|
||||
});
|
||||
|
||||
|
@ -56,9 +56,8 @@ describe("Creating cost requirement", () => {
|
|||
cost: Formula.variable(resource).times(10),
|
||||
visibility: Visibility.None,
|
||||
requiresPay: false,
|
||||
cumulativeCost: false,
|
||||
maxBulkAmount: Decimal.dInf,
|
||||
directSum: 5,
|
||||
maximize: true,
|
||||
spendResources: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
pay() {}
|
||||
}));
|
||||
|
@ -70,43 +69,30 @@ describe("Creating cost requirement", () => {
|
|||
requirement.pay.length === 1);
|
||||
test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None));
|
||||
test("does not require pay", () => expect(requirement.requiresPay).toBe(false));
|
||||
test("spends resources", () => expect(requirement.cumulativeCost).toBe(false));
|
||||
test("spends resources", () => expect(requirement.spendResources).toBe(true));
|
||||
test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true));
|
||||
test("maxBulkAmount is set", () =>
|
||||
expect(unref(requirement.maxBulkAmount)).compare_tolerance(Decimal.dInf));
|
||||
test("directSum is set", () => expect(unref(requirement.directSum)).toBe(5));
|
||||
});
|
||||
|
||||
test("Requirement met when meeting the cost", () => {
|
||||
const requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
cumulativeCost: false
|
||||
spendResources: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(1);
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when not meeting the cost", () => {
|
||||
const requirement = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 100,
|
||||
cumulativeCost: false
|
||||
spendResources: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(0);
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
|
||||
describe("canMaximize works correctly", () => {
|
||||
test("Cost function cannot maximize", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: () => 10,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(false));
|
||||
test("Integrable formula cannot maximize if maxBulkAmount is left at 1", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
|
@ -115,104 +101,82 @@ describe("Creating cost requirement", () => {
|
|||
})).canMaximize
|
||||
)
|
||||
).toBe(false));
|
||||
test("Non-invertible formula cannot maximize when max bulk amount is above direct sum", () =>
|
||||
test("Non-invertible formula cannot maximize", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).abs(),
|
||||
maxBulkAmount: Decimal.dInf
|
||||
cost: Formula.variable(resource).abs()
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(false));
|
||||
test("Non-invertible formula can maximize when max bulk amount is lte direct sum", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).abs(),
|
||||
maxBulkAmount: 20,
|
||||
directSum: 20
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(true));
|
||||
test("Invertible formula can maximize if cumulativeCost is false", () =>
|
||||
test("Invertible formula can maximize if spendResources is false", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).lambertw(),
|
||||
cumulativeCost: false,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
spendResources: false
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(true));
|
||||
test("Invertible formula cannot maximize if cumulativeCost is true", () =>
|
||||
test("Invertible formula cannot maximize if spendResources is true", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).lambertw(),
|
||||
cumulativeCost: true,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
spendResources: true
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(false));
|
||||
test("Integrable formula can maximize if cumulativeCost is false", () =>
|
||||
test("Integrable formula can maximize if spendResources is false", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).pow(2),
|
||||
cumulativeCost: false,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
spendResources: false
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(true));
|
||||
test("Integrable formula can maximize if cumulativeCost is true", () =>
|
||||
test("Integrable formula can maximize if spendResources is true", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).pow(2),
|
||||
cumulativeCost: true,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
spendResources: true
|
||||
})).canMaximize
|
||||
)
|
||||
).toBe(true));
|
||||
});
|
||||
|
||||
test("Requirements met capped by maxBulkAmount", () =>
|
||||
expect(
|
||||
unref(
|
||||
createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: Formula.variable(resource).times(0.0001),
|
||||
maxBulkAmount: 10,
|
||||
cumulativeCost: false
|
||||
})).requirementMet
|
||||
)
|
||||
).compare_tolerance(10));
|
||||
});
|
||||
|
||||
test("Creating visibility requirement", () => {
|
||||
const visibility = ref<Visibility.None | Visibility.Visible | boolean>(Visibility.Visible);
|
||||
const requirement = createVisibilityRequirement({ visibility });
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
visibility.value = true;
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
visibility.value = Visibility.None;
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
visibility.value = false;
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
describe("Creating visibility requirement", () => {
|
||||
test("Requirement met when visible", () => {
|
||||
const requirement = createVisibilityRequirement({ visibility: Visibility.Visible });
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when not visible", () => {
|
||||
let requirement = createVisibilityRequirement({ visibility: Visibility.None });
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
requirement = createVisibilityRequirement({ visibility: false });
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("Creating boolean requirement", () => {
|
||||
const req = ref(true);
|
||||
const requirement = createBooleanRequirement(req);
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
req.value = false;
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
describe("Creating boolean requirement", () => {
|
||||
test("Requirement met when true", () => {
|
||||
const requirement = createBooleanRequirement(ref(true));
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
});
|
||||
|
||||
test("Requirement not met when false", () => {
|
||||
const requirement = createBooleanRequirement(ref(false));
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checking all requirements met", () => {
|
||||
|
@ -244,7 +208,7 @@ describe("Checking maximum levels of requirements met", () => {
|
|||
createCostRequirement(() => ({
|
||||
resource: createResource(ref(10)),
|
||||
cost: Formula.variable(0),
|
||||
cumulativeCost: false
|
||||
spendResources: false
|
||||
}))
|
||||
];
|
||||
expect(maxRequirementsMet(requirements)).compare_tolerance(0);
|
||||
|
@ -256,8 +220,7 @@ describe("Checking maximum levels of requirements met", () => {
|
|||
createCostRequirement(() => ({
|
||||
resource: createResource(ref(10)),
|
||||
cost: Formula.variable(0),
|
||||
cumulativeCost: false,
|
||||
maxBulkAmount: Decimal.dInf
|
||||
spendResources: false
|
||||
}))
|
||||
];
|
||||
expect(maxRequirementsMet(requirements)).compare_tolerance(10);
|
||||
|
@ -270,12 +233,12 @@ test("Paying requirements", () => {
|
|||
resource,
|
||||
cost: 10,
|
||||
requiresPay: false,
|
||||
cumulativeCost: false
|
||||
spendResources: false
|
||||
}));
|
||||
const payment = createCostRequirement(() => ({
|
||||
resource,
|
||||
cost: 10,
|
||||
cumulativeCost: false
|
||||
spendResources: false
|
||||
}));
|
||||
payRequirements([noPayment, payment]);
|
||||
expect(resource.value).compare_tolerance(90);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||
import { Mock, expect, vi } from "vitest";
|
||||
import { expect } from "vitest";
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
|
||||
toLogError(): R;
|
||||
}
|
||||
|
||||
declare module "vitest" {
|
||||
|
@ -34,25 +33,5 @@ expect.extend({
|
|||
expected: format(expected),
|
||||
actual: format(received)
|
||||
};
|
||||
},
|
||||
toLogError(received: () => unknown) {
|
||||
const { isNot } = this;
|
||||
console.error = vi.fn();
|
||||
received();
|
||||
const calls = (
|
||||
console.error as unknown as Mock<
|
||||
Parameters<typeof console.error>,
|
||||
ReturnType<typeof console.error>
|
||||
>
|
||||
).mock.calls.length;
|
||||
const pass = calls >= 1;
|
||||
vi.restoreAllMocks();
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
`Expected ${received} to ${(isNot as boolean) ? " not" : ""} log an error`,
|
||||
expected: "1+",
|
||||
actual: calls
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,50 +1,34 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noErrorTruncation": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"checkJs": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "src",
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
],
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src"],
|
||||
"entryPointStrategy": "expand",
|
||||
"cleanOutputDir": true,
|
||||
"name": "Profectus",
|
||||
"includeVersion": true,
|
||||
"categorizeByGroup": false,
|
||||
"readme": "none",
|
||||
"out": "../docs/api"
|
||||
}
|
||||
"compilerOptions": {
|
||||
"noErrorTruncation": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"checkJs": false,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "src",
|
||||
"types": ["vite/client", "vite-plugin-pwa/vue"],
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src"],
|
||||
"entryPointStrategy": "expand",
|
||||
"cleanOutputDir": true,
|
||||
"name": "Profectus",
|
||||
"includeVersion": true,
|
||||
"categorizeByGroup": false,
|
||||
"readme": "none",
|
||||
"out": "../docs/api"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export default defineConfig({
|
|||
VitePWA({
|
||||
includeAssets: ["Logo.svg", "favicon.ico", "robots.txt", "apple-touch-icon.png"],
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg}"]
|
||||
},
|
||||
manifest: {
|
||||
name: projInfo.title,
|
||||
|
|
Loading…
Reference in a new issue