Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
7df42f3185
69 changed files with 19730 additions and 8425 deletions
31
.forgejo/workflows/deploy.yaml
Normal file
31
.forgejo/workflows/deploy.yaml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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.
|
21
.forgejo/workflows/test.yaml
Normal file
21
.forgejo/workflows/test.yaml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -1,11 +1,11 @@
|
||||||
name: Build and Deploy
|
name: Run Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -6,17 +6,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
### Added
|
||||||
- **BREAKING** New requirements system
|
- **BREAKING** New requirements system
|
||||||
- Replaces many features' existing requirements with new generic form
|
- Replaces many features' existing requirements with new generic form
|
||||||
- Formulas, which can be used to calculate buy max for you
|
- **BREAKING** Formulas, which can be used to calculate buy max for you
|
||||||
- Action feature
|
- Requirements can use them so repeatables and challenges can be "buy max" without any extra effort
|
||||||
- ETA util
|
- Conversions now use formulas instead of the old scaling functions system, allowing for arbitrary functions that are much easier to follow
|
||||||
- createCollapsibleMilestones util
|
- Modifiers have a new getFormula property
|
||||||
|
- Feature decorators, which simplify the process of adding extra values to features
|
||||||
|
- Action feature, which is a clickable with a cooldown
|
||||||
|
- ETA util (calculates time until a specific amount of a resource, based on its current gain rate)
|
||||||
|
- createCollapsibleAchievements util
|
||||||
- deleteLowerSaves util
|
- deleteLowerSaves util
|
||||||
- Minimized layers can now display a component
|
- Minimized layers can now display a component
|
||||||
- submitOnBlur property to Text fields
|
- submitOnBlur property to Text fields
|
||||||
- showPopups property to Milestones
|
- showPopups property to achievements
|
||||||
- Mouse/touch events to more onClick listeners
|
- Mouse/touch events to more onClick listeners
|
||||||
- Example hotkey to starting layer
|
- Example hotkey to starting layer
|
||||||
- Schema for projInfo.json
|
- Schema for projInfo.json
|
||||||
|
@ -29,12 +70,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()`
|
- Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()`
|
||||||
- **BREAKING** Visibility properties can now take booleans
|
- **BREAKING** Visibility properties can now take booleans
|
||||||
- Removed showIf util
|
- Removed showIf util
|
||||||
|
- **BREAKING** Lazy proxies and options functions now pass the base object in as `this` as well as the first parameter.
|
||||||
- Tweaked settings display
|
- Tweaked settings display
|
||||||
- setupPassiveGeneration will no longer lower the resource
|
- setupPassiveGeneration will no longer lower the resource
|
||||||
- displayResource now floors resource amounts
|
- displayResource now floors resource amounts
|
||||||
- Tweaked modifier displays, incl showing negative modifiers in red
|
- Tweaked modifier displays, incl showing negative modifiers in red
|
||||||
- Hotkeys now appear on key graphic
|
- Hotkeys now appear on key graphic
|
||||||
- Mofifier sections now accept computable strings for title and subtitle
|
- Mofifier sections now accept computable strings for title and subtitle
|
||||||
|
- Every VueFeature's `[Component]` property is now typed as GenericComponent
|
||||||
|
- Make errors throw objects instead of strings
|
||||||
- Updated b_e
|
- Updated b_e
|
||||||
### Fixed
|
### Fixed
|
||||||
- NaN detection stopped working
|
- NaN detection stopped working
|
||||||
|
@ -54,15 +98,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Tabs could sometimes not update correctly
|
- Tabs could sometimes not update correctly
|
||||||
- offlineTime not capping properly
|
- offlineTime not capping properly
|
||||||
- Tooltips being user-selectable
|
- Tooltips being user-selectable
|
||||||
|
- Pinnable tooltips causing stack overflow
|
||||||
- Workflows not working with submodules
|
- Workflows not working with submodules
|
||||||
- Various minor typing issues
|
- Various minor typing issues
|
||||||
|
### Removed
|
||||||
|
- **BREAKING** Removed milestones (achievements now have small and large displays)
|
||||||
### Documented
|
### Documented
|
||||||
- requirements.tsx
|
- every single feature
|
||||||
- formulas.tsx
|
|
||||||
- repeatables.tsx
|
|
||||||
### Tests
|
|
||||||
- requirements
|
|
||||||
- formulas
|
- formulas
|
||||||
|
- requirements
|
||||||
|
### Tests
|
||||||
|
- conversions
|
||||||
|
- formulas
|
||||||
|
- modifiers
|
||||||
|
- requirements
|
||||||
|
|
||||||
Contributors: thepaperpilot, escapee, adsaf, ducdat
|
Contributors: thepaperpilot, escapee, adsaf, ducdat
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,6 @@ npm run build
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Runs the tests using vite-jest
|
### Runs the tests using vite-jest
|
||||||
```
|
```
|
||||||
npm run test
|
npm run test
|
||||||
|
|
8744
package-lock.json
generated
8744
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "profectus",
|
"name": "profectus",
|
||||||
"version": "0.5.2",
|
"version": "0.6.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"eslint": "^8.6.0",
|
"eslint": "^8.6.0",
|
||||||
"jsdom": "^20.0.0",
|
"jsdom": "^20.0.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^5.0.2",
|
||||||
"vitest": "^0.29.3",
|
"vitest": "^0.29.3",
|
||||||
"vue-tsc": "^0.38.1"
|
"vue-tsc": "^0.38.1"
|
||||||
},
|
},
|
||||||
|
|
39
src/App.vue
39
src/App.vue
|
@ -1,18 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="modal-root" :style="theme" />
|
<div v-if="appErrors.length > 0" class="error-container" :style="theme"><Error :errors="appErrors" /></div>
|
||||||
<div class="app" :style="theme" :class="{ useHeader }">
|
<template v-else>
|
||||||
<Nav v-if="useHeader" />
|
<div id="modal-root" :style="theme" />
|
||||||
<Game />
|
<div class="app" :style="theme" :class="{ useHeader }">
|
||||||
<TPS v-if="unref(showTPS)" />
|
<Nav v-if="useHeader" />
|
||||||
<GameOverScreen />
|
<Game />
|
||||||
<NaNScreen />
|
<TPS v-if="unref(showTPS)" />
|
||||||
<component :is="gameComponent" />
|
<GameOverScreen />
|
||||||
</div>
|
<NaNScreen />
|
||||||
|
<component :is="gameComponent" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
|
import "@fontsource/roboto-mono";
|
||||||
|
import Error from "components/Error.vue";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
|
import state from "game/state";
|
||||||
import { coerceComponent, render } from "util/vue";
|
import { coerceComponent, render } from "util/vue";
|
||||||
|
import { CSSProperties, watch } from "vue";
|
||||||
import { computed, toRef, unref } from "vue";
|
import { computed, toRef, unref } from "vue";
|
||||||
import Game from "./components/Game.vue";
|
import Game from "./components/Game.vue";
|
||||||
import GameOverScreen from "./components/GameOverScreen.vue";
|
import GameOverScreen from "./components/GameOverScreen.vue";
|
||||||
|
@ -23,12 +30,11 @@ import projInfo from "./data/projInfo.json";
|
||||||
import themes from "./data/themes";
|
import themes from "./data/themes";
|
||||||
import settings, { gameComponents } from "./game/settings";
|
import settings, { gameComponents } from "./game/settings";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
import "@fontsource/roboto-mono";
|
|
||||||
import type { CSSProperties } from "vue";
|
|
||||||
|
|
||||||
const useHeader = projInfo.useHeader;
|
const useHeader = projInfo.useHeader;
|
||||||
const theme = computed(() => themes[settings.theme].variables as CSSProperties);
|
const theme = computed(() => themes[settings.theme].variables as CSSProperties);
|
||||||
const showTPS = toRef(settings, "showTPS");
|
const showTPS = toRef(settings, "showTPS");
|
||||||
|
const appErrors = toRef(state, "errors");
|
||||||
|
|
||||||
const gameComponent = computed(() => {
|
const gameComponent = computed(() => {
|
||||||
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
|
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
|
||||||
|
@ -51,4 +57,15 @@ const gameComponent = computed(() => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
background: var(--background);
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container > .error {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
135
src/components/Error.vue
Normal file
135
src/components/Error.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<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>
|
|
@ -13,7 +13,7 @@
|
||||||
v-if="layerKeys.includes(tab)"
|
v-if="layerKeys.includes(tab)"
|
||||||
v-bind="gatherLayerProps(layers[tab]!)"
|
v-bind="gatherLayerProps(layers[tab]!)"
|
||||||
:index="index"
|
:index="index"
|
||||||
@set-minimized="value => (layers[tab]!.minimized.value = value)"
|
@set-minimized="(value: boolean) => (layers[tab]!.minimized.value = value)"
|
||||||
/>
|
/>
|
||||||
<component :is="tab" :index="index" v-else />
|
<component :is="tab" :index="index" v-else />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,12 +33,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/WzejVAx"
|
href="https://discord.gg/yJ4fjnjU54"
|
||||||
class="info-modal-discord-link"
|
class="info-modal-discord-link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span class="material-icons info-modal-discord">discord</span>
|
<span class="material-icons info-modal-discord">discord</span>
|
||||||
The Paper Pilot Community
|
Profectus & Friends
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="layer-container" :style="{ '--layer-color': unref(color) }">
|
<ErrorVue v-if="errors.length > 0" :errors="errors" />
|
||||||
|
<div class="layer-container" :style="{ '--layer-color': unref(color) }" v-bind="$attrs" v-else>
|
||||||
<button v-if="showGoBack" class="goBack" @click="goBack">❌</button>
|
<button v-if="showGoBack" class="goBack" @click="goBack">❌</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -28,12 +29,12 @@ import type { CoercableComponent } from "features/feature";
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
|
||||||
import type { PropType, Ref } from "vue";
|
import { PropType, Ref, computed, defineComponent, onErrorCaptured, ref, toRefs, unref } from "vue";
|
||||||
import { computed, defineComponent, toRefs, unref } from "vue";
|
|
||||||
import Context from "./Context.vue";
|
import Context from "./Context.vue";
|
||||||
|
import ErrorVue from "./Error.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { Context },
|
components: { Context, ErrorVue },
|
||||||
props: {
|
props: {
|
||||||
index: {
|
index: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -73,21 +74,27 @@ export default defineComponent({
|
||||||
player.tabs.splice(unref(props.index), Infinity);
|
player.tabs.splice(unref(props.index), Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMinimized(min: boolean) {
|
|
||||||
minimized.value = min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
|
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
|
||||||
props.nodes.value = nodes;
|
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 {
|
return {
|
||||||
component,
|
component,
|
||||||
minimizedComponent,
|
minimizedComponent,
|
||||||
showGoBack,
|
showGoBack,
|
||||||
updateNodes,
|
updateNodes,
|
||||||
unref,
|
unref,
|
||||||
goBack
|
goBack,
|
||||||
|
errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
:href="discordLink || 'https://discord.gg/WzejVAx'"
|
:href="discordLink || 'https://discord.gg/yJ4fjnjU54'"
|
||||||
class="nan-modal-discord-link"
|
class="nan-modal-discord-link"
|
||||||
>
|
>
|
||||||
<span class="material-icons nan-modal-discord">discord</span>
|
<span class="material-icons nan-modal-discord">discord</span>
|
||||||
{{ discordName || "The Paper Pilot Community" }}
|
{{ discordName || "Profectus & Friends" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|
|
@ -15,9 +15,7 @@
|
||||||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/WzejVAx" target="_blank"
|
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||||
>The Paper Pilot Community</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||||
|
@ -82,9 +80,7 @@
|
||||||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/WzejVAx" target="_blank"
|
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||||
>The Paper Pilot Community</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<DangerButton
|
<DangerButton
|
||||||
:disabled="isActive"
|
:disabled="isActive"
|
||||||
@click="emit('delete')"
|
@click="emit('delete')"
|
||||||
@confirmingChanged="value => (isConfirming = value)"
|
@confirmingChanged="(value: boolean) => (isConfirming = value)"
|
||||||
>
|
>
|
||||||
<Tooltip display="Delete" :direction="Direction.Left" class="info">
|
<Tooltip display="Delete" :direction="Direction.Left" class="info">
|
||||||
<span class="material-icons" style="margin: -2px">delete</span>
|
<span class="material-icons" style="margin: -2px">delete</span>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
:save="saves[element]"
|
:save="saves[element]"
|
||||||
@open="openSave(element)"
|
@open="openSave(element)"
|
||||||
@export="exportSave(element)"
|
@export="exportSave(element)"
|
||||||
@editName="name => editSave(element, name)"
|
@editName="(name: string) => editSave(element, name)"
|
||||||
@duplicate="duplicateSave(element)"
|
@duplicate="duplicateSave(element)"
|
||||||
@delete="deleteSave(element)"
|
@delete="deleteSave(element)"
|
||||||
/>
|
/>
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
v-if="Object.keys(bank).length > 0"
|
v-if="Object.keys(bank).length > 0"
|
||||||
:options="bank"
|
:options="bank"
|
||||||
:modelValue="selectedPreset"
|
:modelValue="selectedPreset"
|
||||||
@update:modelValue="preset => newFromPreset(preset as string)"
|
@update:modelValue="(preset: unknown) => newFromPreset(preset as string)"
|
||||||
closeOnSelect
|
closeOnSelect
|
||||||
placeholder="Select preset"
|
placeholder="Select preset"
|
||||||
class="presets"
|
class="presets"
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="tpsDisplay" v-if="!tps.isNan()">
|
<div class="tpsDisplay" v-if="!tps.isNan()">TPS: {{ formatWhole(tps) }}</div>
|
||||||
TPS: {{ formatWhole(tps) }}
|
|
||||||
<transition name="fade"
|
|
||||||
><span v-if="showLow" class="low">{{ formatWhole(low) }}</span></transition
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import state from "game/state";
|
import state from "game/state";
|
||||||
import type { DecimalSource } from "util/bignum";
|
|
||||||
import Decimal, { formatWhole } from "util/bignum";
|
import Decimal, { formatWhole } from "util/bignum";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const tps = computed(() =>
|
const tps = computed(() =>
|
||||||
Decimal.div(
|
Decimal.div(
|
||||||
|
@ -19,20 +13,6 @@ const tps = computed(() =>
|
||||||
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { createClickable } from "features/clickables/clickable";
|
||||||
import type { GenericConversion } from "features/conversion";
|
import type { GenericConversion } from "features/conversion";
|
||||||
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
|
||||||
import { jsx, setDefault } from "features/feature";
|
import { jsx, setDefault } from "features/feature";
|
||||||
import { displayResource, Resource } from "features/resources/resource";
|
import { Resource, displayResource } from "features/resources/resource";
|
||||||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||||
import { createTreeNode } from "features/trees/tree";
|
import { createTreeNode } from "features/trees/tree";
|
||||||
import Formula from "game/formulas/formulas";
|
import type { GenericFormula } from "game/formulas/types";
|
||||||
import type { FormulaSource, GenericFormula } from "game/formulas/types";
|
import { BaseLayer } from "game/layers";
|
||||||
import type { Modifier } from "game/modifiers";
|
import type { Modifier } from "game/modifiers";
|
||||||
import type { Persistent } from "game/persistence";
|
import type { Persistent } from "game/persistence";
|
||||||
import { DefaultValue, persistent } from "game/persistence";
|
import { DefaultValue, persistent } from "game/persistence";
|
||||||
|
@ -17,7 +17,7 @@ import player from "game/player";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal, { format, formatSmall, formatTime } from "util/bignum";
|
import Decimal, { format, formatSmall, formatTime } from "util/bignum";
|
||||||
import type { WithRequired } from "util/common";
|
import { WithRequired, camelToTitle } from "util/common";
|
||||||
import type {
|
import type {
|
||||||
Computable,
|
Computable,
|
||||||
GetComputableType,
|
GetComputableType,
|
||||||
|
@ -99,8 +99,8 @@ export type GenericResetButton = Replace<
|
||||||
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
||||||
optionsFunc: OptionsFunc<T>
|
optionsFunc: OptionsFunc<T>
|
||||||
): ResetButton<T> {
|
): ResetButton<T> {
|
||||||
return createClickable(() => {
|
return createClickable(feature => {
|
||||||
const resetButton = optionsFunc();
|
const resetButton = optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
processComputable(resetButton as T, "showNextAt");
|
processComputable(resetButton as T, "showNextAt");
|
||||||
setDefault(resetButton, "showNextAt", true);
|
setDefault(resetButton, "showNextAt", true);
|
||||||
|
@ -134,10 +134,10 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
||||||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||||
{displayResource(
|
{displayResource(
|
||||||
resetButton.conversion.baseResource,
|
resetButton.conversion.baseResource,
|
||||||
unref(resetButton.conversion.buyMax) ||
|
!unref(resetButton.conversion.buyMax) &&
|
||||||
Decimal.floor(unref(resetButton.conversion.actualGain)).neq(1)
|
Decimal.gte(unref(resetButton.conversion.actualGain), 1)
|
||||||
? unref(resetButton.conversion.nextAt)
|
? unref(resetButton.conversion.currentAt)
|
||||||
: unref(resetButton.conversion.currentAt)
|
: unref(resetButton.conversion.nextAt)
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{resetButton.conversion.baseResource.displayName}
|
{resetButton.conversion.baseResource.displayName}
|
||||||
</div>
|
</div>
|
||||||
|
@ -178,11 +178,6 @@ export interface LayerTreeNodeOptions extends TreeNodeOptions {
|
||||||
layerID: string;
|
layerID: string;
|
||||||
/** The color to display this tree node as */
|
/** The color to display this tree node as */
|
||||||
color: Computable<string>; // marking as required
|
color: Computable<string>; // marking as required
|
||||||
/**
|
|
||||||
* The content to display in the tree node.
|
|
||||||
* Defaults to the layer's ID
|
|
||||||
*/
|
|
||||||
display?: Computable<CoercableComponent>;
|
|
||||||
/** Whether or not to append the layer to the tabs list.
|
/** Whether or not to append the layer to the tabs list.
|
||||||
* If set to false, then the tree node will instead always remove all tabs to its right and then add the layer tab.
|
* If set to false, then the tree node will instead always remove all tabs to its right and then add the layer tab.
|
||||||
* Defaults to true.
|
* Defaults to true.
|
||||||
|
@ -213,14 +208,12 @@ export type GenericLayerTreeNode = Replace<
|
||||||
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
|
||||||
optionsFunc: OptionsFunc<T>
|
optionsFunc: OptionsFunc<T>
|
||||||
): LayerTreeNode<T> {
|
): LayerTreeNode<T> {
|
||||||
return createTreeNode(() => {
|
return createTreeNode(feature => {
|
||||||
const options = optionsFunc();
|
const options = optionsFunc.call(feature, feature);
|
||||||
processComputable(options as T, "display");
|
setDefault(options, "display", camelToTitle(options.layerID));
|
||||||
setDefault(options, "display", options.layerID);
|
|
||||||
processComputable(options as T, "append");
|
processComputable(options as T, "append");
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
display: options.display,
|
|
||||||
onClick: unref((options as unknown as GenericLayerTreeNode).append)
|
onClick: unref((options as unknown as GenericLayerTreeNode).append)
|
||||||
? function () {
|
? function () {
|
||||||
if (player.tabs.includes(options.layerID)) {
|
if (player.tabs.includes(options.layerID)) {
|
||||||
|
@ -253,17 +246,17 @@ export interface Section {
|
||||||
baseText?: Computable<CoercableComponent>;
|
baseText?: Computable<CoercableComponent>;
|
||||||
/** Whether or not this section should be currently visible to the player. **/
|
/** Whether or not this section should be currently visible to the player. **/
|
||||||
visible?: Computable<boolean>;
|
visible?: Computable<boolean>;
|
||||||
|
/** Determines if numbers larger or smaller than the base should be displayed as red. */
|
||||||
|
smallerIsBetter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an array of modifier "sections", and creates a JSXFunction that can render all those sections, and allow each section to be collapsed.
|
* Takes an array of modifier "sections", and creates a JSXFunction that can render all those sections, and allow each section to be collapsed.
|
||||||
* Also returns a list of persistent refs that are used to control which sections are currently collapsed.
|
* Also returns a list of persistent refs that are used to control which sections are currently collapsed.
|
||||||
* @param sectionsFunc A function that returns the sections to display.
|
* @param sectionsFunc A function that returns the sections to display.
|
||||||
* @param smallerIsBetter Determines whether numbers larger or smaller than the base should be displayed as red.
|
|
||||||
*/
|
*/
|
||||||
export function createCollapsibleModifierSections(
|
export function createCollapsibleModifierSections(
|
||||||
sectionsFunc: () => Section[],
|
sectionsFunc: () => Section[]
|
||||||
smallerIsBetter = false
|
|
||||||
): [JSXFunction, Persistent<Record<number, boolean>>] {
|
): [JSXFunction, Persistent<Record<number, boolean>>] {
|
||||||
const sections: Section[] = [];
|
const sections: Section[] = [];
|
||||||
const processed:
|
const processed:
|
||||||
|
@ -324,7 +317,9 @@ export function createCollapsibleModifierSections(
|
||||||
{s.unit}
|
{s.unit}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{renderJSX(unref(s.modifier.description))}
|
{s.modifier.description == null
|
||||||
|
? null
|
||||||
|
: renderJSX(unref(s.modifier.description))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -353,7 +348,7 @@ export function createCollapsibleModifierSections(
|
||||||
class="modifier-amount"
|
class="modifier-amount"
|
||||||
style={
|
style={
|
||||||
(
|
(
|
||||||
smallerIsBetter === true
|
s.smallerIsBetter === true
|
||||||
? Decimal.gt(total, base ?? 1)
|
? Decimal.gt(total, base ?? 1)
|
||||||
: Decimal.lt(total, base ?? 1)
|
: Decimal.lt(total, base ?? 1)
|
||||||
)
|
)
|
||||||
|
@ -443,7 +438,7 @@ export function estimateTime(
|
||||||
const currTarget = unref(processedTarget);
|
const currTarget = unref(processedTarget);
|
||||||
if (Decimal.gte(resource.value, currTarget)) {
|
if (Decimal.gte(resource.value, currTarget)) {
|
||||||
return "Now";
|
return "Now";
|
||||||
} else if (Decimal.lt(currRate, 0)) {
|
} else if (Decimal.lte(currRate, 0)) {
|
||||||
return "Never";
|
return "Never";
|
||||||
}
|
}
|
||||||
return formatTime(Decimal.sub(currTarget, resource.value).div(currRate));
|
return formatTime(Decimal.sub(currTarget, resource.value).div(currRate));
|
||||||
|
@ -461,13 +456,13 @@ export function createFormulaPreview(
|
||||||
formula: GenericFormula,
|
formula: GenericFormula,
|
||||||
showPreview: Computable<boolean>,
|
showPreview: Computable<boolean>,
|
||||||
previewAmount: Computable<DecimalSource> = 1
|
previewAmount: Computable<DecimalSource> = 1
|
||||||
): ComputedRef<CoercableComponent> {
|
) {
|
||||||
const processedShowPreview = convertComputable(showPreview);
|
const processedShowPreview = convertComputable(showPreview);
|
||||||
const processedPreviewAmount = convertComputable(previewAmount);
|
const processedPreviewAmount = convertComputable(previewAmount);
|
||||||
if (!formula.hasVariable()) {
|
if (!formula.hasVariable()) {
|
||||||
throw new Error("Cannot create formula preview if the formula does not have a variable");
|
console.error("Cannot create formula preview if the formula does not have a variable");
|
||||||
}
|
}
|
||||||
return computed(() => {
|
return jsx(() => {
|
||||||
if (unref(processedShowPreview)) {
|
if (unref(processedShowPreview)) {
|
||||||
const curr = formatSmall(formula.evaluate());
|
const curr = formatSmall(formula.evaluate());
|
||||||
const preview = formatSmall(
|
const preview = formatSmall(
|
||||||
|
@ -478,37 +473,35 @@ export function createFormulaPreview(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return jsx(() => (
|
return (
|
||||||
<>
|
<>
|
||||||
<b>
|
<b>
|
||||||
<i>
|
<i>
|
||||||
{curr}→{preview}
|
{curr} → {preview}
|
||||||
</i>
|
</i>
|
||||||
</b>
|
</b>
|
||||||
</>
|
</>
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
return formatSmall(formula.evaluate());
|
return <>{formatSmall(formula.evaluate())}</>;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modifierToFormula<T extends GenericFormula>(
|
/**
|
||||||
modifier: WithRequired<Modifier, "revert">,
|
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||||
base: T
|
* Note it will have a true value even if the feature is off screen.
|
||||||
): T;
|
* @param layer The layer the feature appears within
|
||||||
export function modifierToFormula(modifier: Modifier, base: FormulaSource): GenericFormula;
|
* @param id The ID of the feature
|
||||||
export function modifierToFormula(modifier: Modifier, base: FormulaSource) {
|
*/
|
||||||
return new Formula({
|
export function isRendered(layer: BaseLayer, id: string): ComputedRef<boolean>;
|
||||||
inputs: [base],
|
/**
|
||||||
evaluate: val => modifier.apply(val),
|
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||||
invert:
|
* Note it will have a true value even if the feature is off screen.
|
||||||
"revert" in modifier && modifier.revert != null
|
* @param layer The layer the feature appears within
|
||||||
? (val, lhs) => {
|
* @param feature The feature that may be rendered
|
||||||
if (lhs instanceof Formula && lhs.hasVariable()) {
|
*/
|
||||||
return lhs.invert(modifier.revert!(val));
|
export function isRendered(layer: BaseLayer, feature: { id: string }): ComputedRef<boolean>;
|
||||||
}
|
export function isRendered(layer: BaseLayer, idOrFeature: string | { id: string }) {
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
|
||||||
}
|
return computed(() => id in layer.nodes.value);
|
||||||
: undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,18 +2,19 @@ import { computed } from "@vue/reactivity";
|
||||||
import { isArray } from "@vue/shared";
|
import { isArray } from "@vue/shared";
|
||||||
import Select from "components/fields/Select.vue";
|
import Select from "components/fields/Select.vue";
|
||||||
import AchievementComponent from "features/achievements/Achievement.vue";
|
import AchievementComponent from "features/achievements/Achievement.vue";
|
||||||
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import {
|
import {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
Component,
|
Component,
|
||||||
GatherProps,
|
GatherProps,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
getUniqueID,
|
|
||||||
jsx,
|
|
||||||
OptionsFunc,
|
OptionsFunc,
|
||||||
Replace,
|
Replace,
|
||||||
setDefault,
|
|
||||||
StyleValue,
|
StyleValue,
|
||||||
Visibility
|
Visibility,
|
||||||
|
getUniqueID,
|
||||||
|
jsx,
|
||||||
|
setDefault
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import "game/notifications";
|
import "game/notifications";
|
||||||
|
@ -21,10 +22,10 @@ import type { Persistent } from "game/persistence";
|
||||||
import { persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
import player from "game/player";
|
import player from "game/player";
|
||||||
import {
|
import {
|
||||||
|
Requirements,
|
||||||
createBooleanRequirement,
|
createBooleanRequirement,
|
||||||
createVisibilityRequirement,
|
createVisibilityRequirement,
|
||||||
displayRequirements,
|
displayRequirements,
|
||||||
Requirements,
|
|
||||||
requirementsMet
|
requirementsMet
|
||||||
} from "game/requirements";
|
} from "game/requirements";
|
||||||
import settings, { registerSettingField } from "game/settings";
|
import settings, { registerSettingField } from "game/settings";
|
||||||
|
@ -95,7 +96,7 @@ export interface AchievementOptions {
|
||||||
* The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
|
* The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
|
||||||
*/
|
*/
|
||||||
export interface BaseAchievement {
|
export interface BaseAchievement {
|
||||||
/** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
/** Whether or not this achievement has been earned. */
|
/** Whether or not this achievement has been earned. */
|
||||||
earned: Persistent<boolean>;
|
earned: Persistent<boolean>;
|
||||||
|
@ -109,7 +110,7 @@ export interface BaseAchievement {
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An object that represents a feature with that is passively earned upon meeting certain requirements. */
|
/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */
|
||||||
export type Achievement<T extends AchievementOptions> = Replace<
|
export type Achievement<T extends AchievementOptions> = Replace<
|
||||||
T & BaseAchievement,
|
T & BaseAchievement,
|
||||||
{
|
{
|
||||||
|
@ -133,21 +134,35 @@ export type GenericAchievement = Replace<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazily creates a achievement with the given options.
|
* Lazily creates an achievement with the given options.
|
||||||
* @param optionsFunc Achievement options.
|
* @param optionsFunc Achievement options.
|
||||||
*/
|
*/
|
||||||
export function createAchievement<T extends AchievementOptions>(
|
export function createAchievement<T extends AchievementOptions>(
|
||||||
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
|
optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Achievement<T> {
|
): Achievement<T> {
|
||||||
const earned = persistent<boolean>(false, false);
|
const earned = persistent<boolean>(false, false);
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const achievement =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
achievement.id = getUniqueID("achievement-");
|
achievement.id = getUniqueID("achievement-");
|
||||||
achievement.type = AchievementType;
|
achievement.type = AchievementType;
|
||||||
achievement[Component] = AchievementComponent as GenericComponent;
|
achievement[Component] = AchievementComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(achievement);
|
||||||
|
}
|
||||||
|
|
||||||
achievement.earned = earned;
|
achievement.earned = earned;
|
||||||
achievement.complete = function () {
|
achievement.complete = function () {
|
||||||
|
if (earned.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
earned.value = true;
|
earned.value = true;
|
||||||
const genericAchievement = achievement as GenericAchievement;
|
const genericAchievement = achievement as GenericAchievement;
|
||||||
genericAchievement.onComplete?.();
|
genericAchievement.onComplete?.();
|
||||||
|
@ -177,6 +192,8 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Object.assign(achievement, decoratedData);
|
||||||
|
|
||||||
processComputable(achievement as T, "visibility");
|
processComputable(achievement as T, "visibility");
|
||||||
setDefault(achievement, "visibility", Visibility.Visible);
|
setDefault(achievement, "visibility", Visibility.Visible);
|
||||||
const visibility = achievement.visibility as ProcessedComputable<Visibility | boolean>;
|
const visibility = achievement.visibility as ProcessedComputable<Visibility | boolean>;
|
||||||
|
@ -217,6 +234,14 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
processComputable(achievement as T, "showPopups");
|
processComputable(achievement as T, "showPopups");
|
||||||
setDefault(achievement, "showPopups", true);
|
setDefault(achievement, "showPopups", true);
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(achievement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(achievement)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
achievement[GatherProps] = function (this: GenericAchievement) {
|
achievement[GatherProps] = function (this: GenericAchievement) {
|
||||||
const {
|
const {
|
||||||
visibility,
|
visibility,
|
||||||
|
@ -240,7 +265,8 @@ export function createAchievement<T extends AchievementOptions>(
|
||||||
classes,
|
classes,
|
||||||
mark,
|
mark,
|
||||||
small,
|
small,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -31,27 +31,48 @@ import { coerceComponent, isCoercableComponent, render } from "util/vue";
|
||||||
import { computed, Ref, ref, unref } from "vue";
|
import { computed, Ref, ref, unref } from "vue";
|
||||||
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
import { BarOptions, createBar, GenericBar } from "./bars/bar";
|
||||||
import { ClickableOptions } from "./clickables/clickable";
|
import { ClickableOptions } from "./clickables/clickable";
|
||||||
|
import { Decorator, GenericDecorator } from "./decorators/common";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Action} features. */
|
||||||
export const ActionType = Symbol("Action");
|
export const ActionType = Symbol("Action");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures an {@link Action}.
|
||||||
|
*/
|
||||||
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
|
export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
|
||||||
|
/** The cooldown during which the action cannot be performed again, in seconds. */
|
||||||
duration: Computable<DecimalSource>;
|
duration: Computable<DecimalSource>;
|
||||||
|
/** Whether or not the action should perform automatically when the cooldown is finished. */
|
||||||
autoStart?: Computable<boolean>;
|
autoStart?: Computable<boolean>;
|
||||||
|
/** A function that is called when the action is clicked. */
|
||||||
onClick: (amount: DecimalSource) => void;
|
onClick: (amount: DecimalSource) => void;
|
||||||
|
/** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
|
||||||
barOptions?: Partial<BarOptions>;
|
barOptions?: Partial<BarOptions>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ActionOptions} to create an {@link Action}.
|
||||||
|
*/
|
||||||
export interface BaseAction {
|
export interface BaseAction {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof ActionType;
|
type: typeof ActionType;
|
||||||
|
/** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
|
||||||
isHolding: Ref<boolean>;
|
isHolding: Ref<boolean>;
|
||||||
|
/** The current amount of progress through the cooldown. */
|
||||||
progress: Ref<DecimalSource>;
|
progress: Ref<DecimalSource>;
|
||||||
|
/** The bar used to display the current cooldown progress. */
|
||||||
progressBar: GenericBar;
|
progressBar: GenericBar;
|
||||||
|
/** Update the cooldown the specified number of seconds */
|
||||||
update: (diff: number) => void;
|
update: (diff: number) => void;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that can be clicked upon, and then has a cooldown before it can be clicked again. */
|
||||||
export type Action<T extends ActionOptions> = Replace<
|
export type Action<T extends ActionOptions> = Replace<
|
||||||
T & BaseAction,
|
T & BaseAction,
|
||||||
{
|
{
|
||||||
|
@ -67,6 +88,7 @@ export type Action<T extends ActionOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Action} object. */
|
||||||
export type GenericAction = Replace<
|
export type GenericAction = Replace<
|
||||||
Action<ActionOptions>,
|
Action<ActionOptions>,
|
||||||
{
|
{
|
||||||
|
@ -76,12 +98,23 @@ export type GenericAction = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates an action with the given options.
|
||||||
|
* @param optionsFunc Action options.
|
||||||
|
*/
|
||||||
export function createAction<T extends ActionOptions>(
|
export function createAction<T extends ActionOptions>(
|
||||||
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>
|
optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Action<T> {
|
): Action<T> {
|
||||||
const progress = persistent<DecimalSource>(0);
|
const progress = persistent<DecimalSource>(0);
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const action = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const action =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
action.id = getUniqueID("action-");
|
action.id = getUniqueID("action-");
|
||||||
action.type = ActionType;
|
action.type = ActionType;
|
||||||
action[Component] = ClickableComponent as GenericComponent;
|
action[Component] = ClickableComponent as GenericComponent;
|
||||||
|
@ -89,8 +122,13 @@ export function createAction<T extends ActionOptions>(
|
||||||
// Required because of display changing types
|
// Required because of display changing types
|
||||||
const genericAction = action as unknown as GenericAction;
|
const genericAction = action as unknown as GenericAction;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(action);
|
||||||
|
}
|
||||||
|
|
||||||
action.isHolding = ref(false);
|
action.isHolding = ref(false);
|
||||||
action.progress = progress;
|
action.progress = progress;
|
||||||
|
Object.assign(action, decoratedData);
|
||||||
|
|
||||||
processComputable(action as T, "visibility");
|
processComputable(action as T, "visibility");
|
||||||
setDefault(action, "visibility", Visibility.Visible);
|
setDefault(action, "visibility", Visibility.Visible);
|
||||||
|
@ -131,7 +169,6 @@ export function createAction<T extends ActionOptions>(
|
||||||
direction: Direction.Right,
|
direction: Direction.Right,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 10,
|
height: 10,
|
||||||
style: "margin-top: 8px",
|
|
||||||
borderStyle: "border-color: black",
|
borderStyle: "border-color: black",
|
||||||
baseStyle: "margin-top: -1px",
|
baseStyle: "margin-top: -1px",
|
||||||
progress: () => Decimal.div(progress.value, unref(genericAction.duration)),
|
progress: () => Decimal.div(progress.value, unref(genericAction.duration)),
|
||||||
|
@ -202,6 +239,14 @@ export function createAction<T extends ActionOptions>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(action)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
action[GatherProps] = function (this: GenericAction) {
|
action[GatherProps] = function (this: GenericAction) {
|
||||||
const {
|
const {
|
||||||
display,
|
display,
|
||||||
|
@ -225,7 +270,8 @@ export function createAction<T extends ActionOptions>(
|
||||||
canClick,
|
canClick,
|
||||||
small,
|
small,
|
||||||
mark,
|
mark,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default defineComponent({
|
||||||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||||
break;
|
break;
|
||||||
case Direction.Left:
|
case Direction.Left:
|
||||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
|
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
|
||||||
break;
|
break;
|
||||||
case Direction.Default:
|
case Direction.Default:
|
||||||
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||||
|
@ -179,5 +179,6 @@ export default defineComponent({
|
||||||
margin-left: -0.5px;
|
margin-left: -0.5px;
|
||||||
transition-duration: 0.2s;
|
transition-duration: 0.2s;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
transition-duration: 0.05s;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import BarComponent from "features/bars/Bar.vue";
|
import BarComponent from "features/bars/Bar.vue";
|
||||||
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -6,7 +7,7 @@ import type {
|
||||||
Replace,
|
Replace,
|
||||||
StyleValue
|
StyleValue
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
import { Component, GatherProps, Visibility, getUniqueID, setDefault } from "features/feature";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
import type {
|
import type {
|
||||||
|
@ -19,31 +20,56 @@ import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { unref } from "vue";
|
import { unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Bar} features. */
|
||||||
export const BarType = Symbol("Bar");
|
export const BarType = Symbol("Bar");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Bar}.
|
||||||
|
*/
|
||||||
export interface BarOptions {
|
export interface BarOptions {
|
||||||
|
/** Whether this bar should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The width of the bar. */
|
||||||
width: Computable<number>;
|
width: Computable<number>;
|
||||||
|
/** The height of the bar. */
|
||||||
height: Computable<number>;
|
height: Computable<number>;
|
||||||
|
/** The direction in which the bar progresses. */
|
||||||
direction: Computable<Direction>;
|
direction: Computable<Direction>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to the bar's border. */
|
||||||
borderStyle?: Computable<StyleValue>;
|
borderStyle?: Computable<StyleValue>;
|
||||||
|
/** CSS to apply to the bar's base. */
|
||||||
baseStyle?: Computable<StyleValue>;
|
baseStyle?: Computable<StyleValue>;
|
||||||
|
/** CSS to apply to the bar's text. */
|
||||||
textStyle?: Computable<StyleValue>;
|
textStyle?: Computable<StyleValue>;
|
||||||
|
/** CSS to apply to the bar's fill. */
|
||||||
fillStyle?: Computable<StyleValue>;
|
fillStyle?: Computable<StyleValue>;
|
||||||
|
/** The progress value of the bar, from 0 to 1. */
|
||||||
progress: Computable<DecimalSource>;
|
progress: Computable<DecimalSource>;
|
||||||
|
/** The display to use for this bar. */
|
||||||
display?: Computable<CoercableComponent>;
|
display?: Computable<CoercableComponent>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link BarOptions} to create a {@link Bar}.
|
||||||
|
*/
|
||||||
export interface BaseBar {
|
export interface BaseBar {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof BarType;
|
type: typeof BarType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that displays some sort of progress or completion or resource with a cap. */
|
||||||
export type Bar<T extends BarOptions> = Replace<
|
export type Bar<T extends BarOptions> = Replace<
|
||||||
T & BaseBar,
|
T & BaseBar,
|
||||||
{
|
{
|
||||||
|
@ -63,6 +89,7 @@ export type Bar<T extends BarOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Bar} object. */
|
||||||
export type GenericBar = Replace<
|
export type GenericBar = Replace<
|
||||||
Bar<BarOptions>,
|
Bar<BarOptions>,
|
||||||
{
|
{
|
||||||
|
@ -70,15 +97,30 @@ export type GenericBar = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a bar with the given options.
|
||||||
|
* @param optionsFunc Bar options.
|
||||||
|
*/
|
||||||
export function createBar<T extends BarOptions>(
|
export function createBar<T extends BarOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseBar, GenericBar>
|
optionsFunc: OptionsFunc<T, BaseBar, GenericBar>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Bar<T> {
|
): Bar<T> {
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const bar = optionsFunc();
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const bar = optionsFunc.call(feature, feature);
|
||||||
bar.id = getUniqueID("bar-");
|
bar.id = getUniqueID("bar-");
|
||||||
bar.type = BarType;
|
bar.type = BarType;
|
||||||
bar[Component] = BarComponent as GenericComponent;
|
bar[Component] = BarComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(bar, decoratedData);
|
||||||
|
|
||||||
processComputable(bar as T, "visibility");
|
processComputable(bar as T, "visibility");
|
||||||
setDefault(bar, "visibility", Visibility.Visible);
|
setDefault(bar, "visibility", Visibility.Visible);
|
||||||
processComputable(bar as T, "width");
|
processComputable(bar as T, "width");
|
||||||
|
@ -94,6 +136,14 @@ export function createBar<T extends BarOptions>(
|
||||||
processComputable(bar as T, "display");
|
processComputable(bar as T, "display");
|
||||||
processComputable(bar as T, "mark");
|
processComputable(bar as T, "mark");
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(bar)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
bar[GatherProps] = function (this: GenericBar) {
|
bar[GatherProps] = function (this: GenericBar) {
|
||||||
const {
|
const {
|
||||||
progress,
|
progress,
|
||||||
|
@ -125,7 +175,8 @@ export function createBar<T extends BarOptions>(
|
||||||
baseStyle,
|
baseStyle,
|
||||||
fillStyle,
|
fillStyle,
|
||||||
mark,
|
mark,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<panZoom
|
<panZoom
|
||||||
v-if="isVisible(visibility)"
|
v-if="isVisible(visibility)"
|
||||||
v-show="isHidden(visibility)"
|
|
||||||
:style="[
|
:style="[
|
||||||
{
|
{
|
||||||
width,
|
width,
|
||||||
|
@ -18,15 +17,27 @@
|
||||||
@touchmove="drag"
|
@touchmove="drag"
|
||||||
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
@mousedown="(e: MouseEvent) => mouseDown(e)"
|
||||||
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
@touchstart="(e: TouchEvent) => mouseDown(e)"
|
||||||
@mouseup="() => endDragging(dragging)"
|
@mouseup="() => endDragging(unref(draggingNode))"
|
||||||
@touchend.passive="() => endDragging(dragging)"
|
@touchend.passive="() => endDragging(unref(draggingNode))"
|
||||||
@mouseleave="() => endDragging(dragging)"
|
@mouseleave="() => endDragging(unref(draggingNode), true)"
|
||||||
>
|
>
|
||||||
<svg class="stage" width="100%" height="100%">
|
<svg class="stage" width="100%" height="100%">
|
||||||
<g class="g1">
|
<g class="g1">
|
||||||
<transition-group name="link" appear>
|
<transition-group name="link" appear>
|
||||||
<g v-for="(link, i) in unref(links) || []" :key="i">
|
<g
|
||||||
<BoardLinkVue :link="link" />
|
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
|
||||||
|
"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
<transition-group name="grow" :duration="500" appear>
|
<transition-group name="grow" :duration="500" appear>
|
||||||
|
@ -34,14 +45,17 @@
|
||||||
<BoardNodeVue
|
<BoardNodeVue
|
||||||
:node="node"
|
:node="node"
|
||||||
:nodeType="types[node.type]"
|
:nodeType="types[node.type]"
|
||||||
:dragging="draggingNode"
|
:dragging="unref(draggingNode)"
|
||||||
:dragged="dragged"
|
:dragged="unref(draggingNode) === node ? dragged : undefined"
|
||||||
:hasDragged="hasDragged"
|
:hasDragged="unref(draggingNode) == null ? false : hasDragged"
|
||||||
:receivingNode="receivingNode?.id === node.id"
|
:receivingNode="unref(receivingNode) === node"
|
||||||
:selectedNode="unref(selectedNode)"
|
:isSelected="unref(selectedNode) === node"
|
||||||
:selectedAction="unref(selectedAction)"
|
:selectedAction="
|
||||||
|
unref(selectedNode) === node ? unref(selectedAction) : null
|
||||||
|
"
|
||||||
@mouseDown="mouseDown"
|
@mouseDown="mouseDown"
|
||||||
@endDragging="endDragging"
|
@endDragging="endDragging"
|
||||||
|
@clickAction="(actionId: string) => clickAction(node, actionId)"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
@ -60,9 +74,9 @@ import type {
|
||||||
} from "features/boards/board";
|
} from "features/boards/board";
|
||||||
import { getNodeProperty } from "features/boards/board";
|
import { getNodeProperty } from "features/boards/board";
|
||||||
import type { StyleValue } from "features/feature";
|
import type { StyleValue } from "features/feature";
|
||||||
import { isHidden, isVisible, Visibility } from "features/feature";
|
import { Visibility, isVisible } from "features/feature";
|
||||||
import type { ProcessedComputable } from "util/computed";
|
import type { ProcessedComputable } from "util/computed";
|
||||||
import { computed, ref, Ref, toRefs, unref } from "vue";
|
import { Ref, computed, ref, toRefs, unref, watchEffect } from "vue";
|
||||||
import BoardLinkVue from "./BoardLink.vue";
|
import BoardLinkVue from "./BoardLink.vue";
|
||||||
import BoardNodeVue from "./BoardNode.vue";
|
import BoardNodeVue from "./BoardNode.vue";
|
||||||
|
|
||||||
|
@ -78,32 +92,35 @@ const _props = defineProps<{
|
||||||
links: Ref<BoardNodeLink[] | null>;
|
links: Ref<BoardNodeLink[] | null>;
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
selectedNode: Ref<BoardNode | null>;
|
selectedNode: Ref<BoardNode | null>;
|
||||||
|
draggingNode: Ref<BoardNode | null>;
|
||||||
|
receivingNode: Ref<BoardNode | null>;
|
||||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||||
|
setReceivingNode: (node: BoardNode | null) => void;
|
||||||
|
setDraggingNode: (node: BoardNode | null) => void;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
|
|
||||||
const lastMousePosition = ref({ x: 0, y: 0 });
|
const lastMousePosition = ref({ x: 0, y: 0 });
|
||||||
const dragged = ref({ x: 0, y: 0 });
|
const dragged = ref({ x: 0, y: 0 });
|
||||||
const dragging = ref<number | null>(null);
|
|
||||||
const hasDragged = ref(false);
|
const hasDragged = ref(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const stage = ref<any>(null);
|
const stage = ref<any>(null);
|
||||||
|
|
||||||
const draggingNode = computed(() =>
|
|
||||||
dragging.value == null ? undefined : props.nodes.value.find(node => node.id === dragging.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedNodes = computed(() => {
|
const sortedNodes = computed(() => {
|
||||||
const nodes = props.nodes.value.slice();
|
const nodes = props.nodes.value.slice();
|
||||||
if (draggingNode.value) {
|
if (props.selectedNode.value) {
|
||||||
const node = nodes.splice(nodes.indexOf(draggingNode.value), 1)[0];
|
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);
|
nodes.push(node);
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
});
|
});
|
||||||
|
|
||||||
const receivingNode = computed(() => {
|
watchEffect(() => {
|
||||||
const node = draggingNode.value;
|
const node = props.draggingNode.value;
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -113,35 +130,40 @@ const receivingNode = computed(() => {
|
||||||
y: node.position.y + dragged.value.y
|
y: node.position.y + dragged.value.y
|
||||||
};
|
};
|
||||||
let smallestDistance = Number.MAX_VALUE;
|
let smallestDistance = Number.MAX_VALUE;
|
||||||
return props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
|
|
||||||
if (curr.id === node.id) {
|
|
||||||
return smallest;
|
|
||||||
}
|
|
||||||
const nodeType = props.types.value[curr.type];
|
|
||||||
const canAccept = getNodeProperty(nodeType.canAccept, curr);
|
|
||||||
if (!canAccept) {
|
|
||||||
return smallest;
|
|
||||||
}
|
|
||||||
|
|
||||||
const distanceSquared =
|
props.setReceivingNode.value(
|
||||||
Math.pow(position.x - curr.position.x, 2) + Math.pow(position.y - curr.position.y, 2);
|
props.nodes.value.reduce((smallest: BoardNode | null, curr: BoardNode) => {
|
||||||
let size = getNodeProperty(nodeType.size, curr);
|
if (curr.id === node.id) {
|
||||||
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
return smallest;
|
||||||
return smallest;
|
}
|
||||||
}
|
const nodeType = props.types.value[curr.type];
|
||||||
|
const canAccept = getNodeProperty(nodeType.canAccept, curr, node);
|
||||||
|
if (!canAccept) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
smallestDistance = distanceSquared;
|
const distanceSquared =
|
||||||
return curr;
|
Math.pow(position.x - curr.position.x, 2) +
|
||||||
}, null);
|
Math.pow(position.y - curr.position.y, 2);
|
||||||
|
let size = getNodeProperty(nodeType.size, curr);
|
||||||
|
if (distanceSquared > smallestDistance || distanceSquared > size * size) {
|
||||||
|
return smallest;
|
||||||
|
}
|
||||||
|
|
||||||
|
smallestDistance = distanceSquared;
|
||||||
|
return curr;
|
||||||
|
}, null)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function onInit(panzoomInstance: any) {
|
function onInit(panzoomInstance: any) {
|
||||||
panzoomInstance.setTransformOrigin(null);
|
panzoomInstance.setTransformOrigin(null);
|
||||||
|
panzoomInstance.moveTo(stage.value.$el.clientWidth / 2, stage.value.$el.clientHeight / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, draggable = false) {
|
function mouseDown(e: MouseEvent | TouchEvent, node: BoardNode | null = null, draggable = false) {
|
||||||
if (dragging.value == null) {
|
if (props.draggingNode.value == null) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -165,10 +187,10 @@ function mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, dra
|
||||||
hasDragged.value = false;
|
hasDragged.value = false;
|
||||||
|
|
||||||
if (draggable) {
|
if (draggable) {
|
||||||
dragging.value = nodeID;
|
props.setDraggingNode.value(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nodeID != null) {
|
if (node != null) {
|
||||||
props.state.value.selectedNode = null;
|
props.state.value.selectedNode = null;
|
||||||
props.state.value.selectedAction = null;
|
props.state.value.selectedAction = null;
|
||||||
}
|
}
|
||||||
|
@ -183,7 +205,7 @@ function drag(e: MouseEvent | TouchEvent) {
|
||||||
clientX = e.touches[0].clientX;
|
clientX = e.touches[0].clientX;
|
||||||
clientY = e.touches[0].clientY;
|
clientY = e.touches[0].clientY;
|
||||||
} else {
|
} else {
|
||||||
endDragging(dragging.value);
|
endDragging(props.draggingNode.value);
|
||||||
props.mousePosition.value = null;
|
props.mousePosition.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -210,34 +232,44 @@ function drag(e: MouseEvent | TouchEvent) {
|
||||||
hasDragged.value = true;
|
hasDragged.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dragging.value != null) {
|
if (props.draggingNode.value != null) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function endDragging(nodeID: number | null) {
|
function endDragging(node: BoardNode | null, mouseLeave = false) {
|
||||||
if (dragging.value != null && dragging.value === nodeID && draggingNode.value != null) {
|
if (props.draggingNode.value != null && props.draggingNode.value === node) {
|
||||||
draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
|
if (props.receivingNode.value == null) {
|
||||||
draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
|
props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
|
||||||
|
props.draggingNode.value.position.y += Math.round(dragged.value.y / 25) * 25;
|
||||||
|
}
|
||||||
|
|
||||||
const nodes = props.nodes.value;
|
const nodes = props.nodes.value;
|
||||||
nodes.splice(nodes.indexOf(draggingNode.value), 1);
|
nodes.push(nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0]);
|
||||||
nodes.push(draggingNode.value);
|
|
||||||
|
|
||||||
if (receivingNode.value) {
|
if (props.receivingNode.value) {
|
||||||
props.types.value[receivingNode.value.type].onDrop?.(
|
props.types.value[props.receivingNode.value.type].onDrop?.(
|
||||||
receivingNode.value,
|
props.receivingNode.value,
|
||||||
draggingNode.value
|
props.draggingNode.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dragging.value = null;
|
props.setDraggingNode.value(null);
|
||||||
} else if (!hasDragged.value) {
|
} else if (!hasDragged.value && !mouseLeave) {
|
||||||
props.state.value.selectedNode = null;
|
props.state.value.selectedNode = null;
|
||||||
props.state.value.selectedAction = null;
|
props.state.value.selectedAction = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clickAction(node: BoardNode, actionId: string) {
|
||||||
|
if (props.state.value.selectedAction === actionId) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
unref(props.selectedAction)!.onClick(unref(props.selectedNode)!);
|
||||||
|
} else {
|
||||||
|
props.state.value = { ...props.state.value, selectedAction: actionId };
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<line
|
<line
|
||||||
class="link"
|
class="link"
|
||||||
v-bind="link"
|
v-bind="linkProps"
|
||||||
:class="{ pulsing: link.pulsing }"
|
:class="{ pulsing: link.pulsing }"
|
||||||
:x1="startPosition.x"
|
:x1="startPosition.x"
|
||||||
:y1="startPosition.y"
|
:y1="startPosition.y"
|
||||||
|
@ -11,34 +11,55 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BoardNodeLink } from "features/boards/board";
|
import type { BoardNode, BoardNodeLink } from "features/boards/board";
|
||||||
|
import { kebabifyObject } from "util/vue";
|
||||||
import { computed, toRefs, unref } from "vue";
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
link: BoardNodeLink;
|
link: BoardNodeLink;
|
||||||
|
dragging: BoardNode | null;
|
||||||
|
dragged?: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
|
|
||||||
const startPosition = computed(() => {
|
const startPosition = computed(() => {
|
||||||
const position = props.link.value.startNode.position;
|
const position = { ...props.link.value.startNode.position };
|
||||||
if (props.link.value.offsetStart) {
|
if (props.link.value.offsetStart) {
|
||||||
position.x += unref(props.link.value.offsetStart).x;
|
position.x += unref(props.link.value.offsetStart).x;
|
||||||
position.y += unref(props.link.value.offsetStart).y;
|
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;
|
return position;
|
||||||
});
|
});
|
||||||
|
|
||||||
const endPosition = computed(() => {
|
const endPosition = computed(() => {
|
||||||
const position = props.link.value.endNode.position;
|
const position = { ...props.link.value.endNode.position };
|
||||||
if (props.link.value.offsetEnd) {
|
if (props.link.value.offsetEnd) {
|
||||||
position.x += unref(props.link.value.offsetEnd).x;
|
position.x += unref(props.link.value.offsetEnd).x;
|
||||||
position.y += unref(props.link.value.offsetEnd).y;
|
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;
|
return position;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.link {
|
||||||
|
transition-duration: 0s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.link.pulsing {
|
.link.pulsing {
|
||||||
animation: pulsing 2s ease-in infinite;
|
animation: pulsing 2s ease-in infinite;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Ugly casting to prevent TS compiler error about style because vue doesn't think it supports arrays when it does -->
|
||||||
<g
|
<g
|
||||||
class="boardnode"
|
class="boardnode"
|
||||||
:class="node.type"
|
:class="{ [node.type]: true, isSelected, isDraggable, ...classes }"
|
||||||
:style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
|
:style="[{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }, style ?? []] as unknown as (string | CSSProperties)"
|
||||||
:transform="`translate(${position.x},${position.y})`"
|
:transform="`translate(${position.x},${position.y})${isSelected ? ' scale(1.2)' : ''}`"
|
||||||
>
|
>
|
||||||
<transition name="actions" appear>
|
<BoardNodeAction
|
||||||
<g v-if="isSelected && actions">
|
:actions="actions ?? []"
|
||||||
<!-- TODO move to separate file -->
|
:is-selected="isSelected"
|
||||||
<g
|
:node="node"
|
||||||
v-for="(action, index) in actions"
|
:node-type="nodeType"
|
||||||
:key="action.id"
|
:selected-action="selectedAction"
|
||||||
class="action"
|
@click-action="(actionId: string) => emit('clickAction', actionId)"
|
||||||
:class="{ selected: selectedAction?.id === action.id }"
|
/>
|
||||||
:transform="`translate(
|
|
||||||
${
|
|
||||||
(-size - 30) *
|
|
||||||
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)
|
|
||||||
},
|
|
||||||
${
|
|
||||||
(size + 30) *
|
|
||||||
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)
|
|
||||||
}
|
|
||||||
)`"
|
|
||||||
@mousedown="e => performAction(e, action)"
|
|
||||||
@touchstart="e => performAction(e, action)"
|
|
||||||
@mouseup="e => actionMouseUp(e, action)"
|
|
||||||
@touchend.stop="e => actionMouseUp(e, action)"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
:fill="getNodeProperty(action.fillColor, node)"
|
|
||||||
r="20"
|
|
||||||
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
|
||||||
:stroke="outlineColor"
|
|
||||||
/>
|
|
||||||
<text :fill="titleColor" class="material-icons">{{
|
|
||||||
getNodeProperty(action.icon, node)
|
|
||||||
}}</text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<g
|
<g
|
||||||
class="node-container"
|
class="node-container"
|
||||||
@mouseenter="isHovering = true"
|
|
||||||
@mouseleave="isHovering = false"
|
|
||||||
@mousedown="mouseDown"
|
@mousedown="mouseDown"
|
||||||
@touchstart.passive="mouseDown"
|
@touchstart.passive="mouseDown"
|
||||||
@mouseup="mouseUp"
|
@mouseup="mouseUp"
|
||||||
|
@ -69,7 +41,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<circle
|
<circle
|
||||||
class="progressFill"
|
class="progress progressFill"
|
||||||
v-if="progressDisplay === ProgressDisplay.Fill"
|
v-if="progressDisplay === ProgressDisplay.Fill"
|
||||||
:r="Math.max(size * progress - 2, 0)"
|
:r="Math.max(size * progress - 2, 0)"
|
||||||
:fill="progressColor"
|
:fill="progressColor"
|
||||||
|
@ -77,7 +49,7 @@
|
||||||
<circle
|
<circle
|
||||||
v-else
|
v-else
|
||||||
:r="size + 4.5"
|
:r="size + 4.5"
|
||||||
class="progressRing"
|
class="progress progressRing"
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
:stroke-dasharray="(size + 4.5) * 2 * Math.PI"
|
:stroke-dasharray="(size + 4.5) * 2 * Math.PI"
|
||||||
:stroke-width="5"
|
:stroke-width="5"
|
||||||
|
@ -113,7 +85,7 @@
|
||||||
|
|
||||||
<rect
|
<rect
|
||||||
v-if="progressDisplay === ProgressDisplay.Fill"
|
v-if="progressDisplay === ProgressDisplay.Fill"
|
||||||
class="progressFill"
|
class="progress progressFill"
|
||||||
:width="Math.max(size * sqrtTwo * progress - 2, 0)"
|
:width="Math.max(size * sqrtTwo * progress - 2, 0)"
|
||||||
:height="Math.max(size * sqrtTwo * progress - 2, 0)"
|
:height="Math.max(size * sqrtTwo * progress - 2, 0)"
|
||||||
:transform="`translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${
|
:transform="`translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${
|
||||||
|
@ -123,7 +95,7 @@
|
||||||
/>
|
/>
|
||||||
<rect
|
<rect
|
||||||
v-else
|
v-else
|
||||||
class="progressDiamond"
|
class="progress progressDiamond"
|
||||||
:width="size * sqrtTwo + 9"
|
:width="size * sqrtTwo + 9"
|
||||||
:height="size * sqrtTwo + 9"
|
:height="size * sqrtTwo + 9"
|
||||||
:transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${
|
:transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${
|
||||||
|
@ -145,7 +117,7 @@
|
||||||
<transition name="fade" appear>
|
<transition name="fade" appear>
|
||||||
<g v-if="label">
|
<g v-if="label">
|
||||||
<text
|
<text
|
||||||
:fill="label.color || titleColor"
|
:fill="label.color ?? titleColor"
|
||||||
class="node-title"
|
class="node-title"
|
||||||
:class="{ pulsing: label.pulsing }"
|
:class="{ pulsing: label.pulsing }"
|
||||||
:y="-size - 20"
|
:y="-size - 20"
|
||||||
|
@ -157,10 +129,11 @@
|
||||||
<transition name="fade" appear>
|
<transition name="fade" appear>
|
||||||
<text
|
<text
|
||||||
v-if="isSelected && selectedAction"
|
v-if="isSelected && selectedAction"
|
||||||
:fill="titleColor"
|
:fill="confirmationLabel.color ?? titleColor"
|
||||||
class="node-title"
|
class="node-title"
|
||||||
|
:class="{ pulsing: confirmationLabel.pulsing }"
|
||||||
:y="size + 75"
|
:y="size + 75"
|
||||||
>Tap again to confirm</text
|
>{{ confirmationLabel.text }}</text
|
||||||
>
|
>
|
||||||
</transition>
|
</transition>
|
||||||
</g>
|
</g>
|
||||||
|
@ -169,34 +142,34 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import themes from "data/themes";
|
import themes from "data/themes";
|
||||||
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
|
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
|
||||||
import { ProgressDisplay, getNodeProperty, Shape } from "features/boards/board";
|
import { ProgressDisplay, Shape, getNodeProperty } from "features/boards/board";
|
||||||
import { Visibility } from "features/feature";
|
import { isVisible } from "features/feature";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import { computed, ref, toRefs, unref, watch } from "vue";
|
import { CSSProperties, computed, toRefs, unref, watch } from "vue";
|
||||||
|
import BoardNodeAction from "./BoardNodeAction.vue";
|
||||||
|
|
||||||
const sqrtTwo = Math.sqrt(2);
|
const sqrtTwo = Math.sqrt(2);
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
node: BoardNode;
|
node: BoardNode;
|
||||||
nodeType: GenericNodeType;
|
nodeType: GenericNodeType;
|
||||||
dragging?: BoardNode;
|
dragging: BoardNode | null;
|
||||||
dragged?: {
|
dragged?: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
hasDragged?: boolean;
|
hasDragged?: boolean;
|
||||||
receivingNode?: boolean;
|
receivingNode?: boolean;
|
||||||
selectedNode?: BoardNode | null;
|
isSelected: boolean;
|
||||||
selectedAction?: GenericBoardNodeAction | null;
|
selectedAction: GenericBoardNodeAction | null;
|
||||||
}>();
|
}>();
|
||||||
const props = toRefs(_props);
|
const props = toRefs(_props);
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "mouseDown", event: MouseEvent | TouchEvent, node: number, isDraggable: boolean): void;
|
(e: "mouseDown", event: MouseEvent | TouchEvent, node: BoardNode, isDraggable: boolean): void;
|
||||||
(e: "endDragging", node: number): void;
|
(e: "endDragging", node: BoardNode): void;
|
||||||
|
(e: "clickAction", actionId: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isHovering = ref(false);
|
|
||||||
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
|
|
||||||
const isDraggable = computed(() =>
|
const isDraggable = computed(() =>
|
||||||
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
|
||||||
);
|
);
|
||||||
|
@ -204,34 +177,53 @@ const isDraggable = computed(() =>
|
||||||
watch(isDraggable, value => {
|
watch(isDraggable, value => {
|
||||||
const node = unref(props.node);
|
const node = unref(props.node);
|
||||||
if (unref(props.dragging) === node && !value) {
|
if (unref(props.dragging) === node && !value) {
|
||||||
emit("endDragging", node.id);
|
emit("endDragging", node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = computed(() => {
|
const actions = computed(() => {
|
||||||
const node = unref(props.node);
|
const node = unref(props.node);
|
||||||
return getNodeProperty(props.nodeType.value.actions, node)?.filter(
|
return getNodeProperty(props.nodeType.value.actions, node)?.filter(action =>
|
||||||
action => getNodeProperty(action.visibility, node) !== Visibility.None
|
isVisible(getNodeProperty(action.visibility, node))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const position = computed(() => {
|
const position = computed(() => {
|
||||||
const node = unref(props.node);
|
const node = unref(props.node);
|
||||||
const dragged = unref(props.dragged);
|
|
||||||
|
|
||||||
return getNodeProperty(props.nodeType.value.draggable, node) &&
|
if (
|
||||||
|
getNodeProperty(props.nodeType.value.draggable, node) &&
|
||||||
unref(props.dragging)?.id === node.id &&
|
unref(props.dragging)?.id === node.id &&
|
||||||
dragged
|
unref(props.dragged) != null
|
||||||
? {
|
) {
|
||||||
x: node.position.x + Math.round(dragged.x / 25) * 25,
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
y: node.position.y + Math.round(dragged.y / 25) * 25
|
const { x, y } = unref(props.dragged)!;
|
||||||
}
|
return {
|
||||||
: node.position;
|
x: node.position.x + Math.round(x / 25) * 25,
|
||||||
|
y: node.position.y + Math.round(y / 25) * 25
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node.position;
|
||||||
});
|
});
|
||||||
|
|
||||||
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
|
const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(props.node)));
|
||||||
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
|
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
|
||||||
const label = computed(() => getNodeProperty(props.nodeType.value.label, unref(props.node)));
|
const label = computed(
|
||||||
|
() =>
|
||||||
|
(props.isSelected.value
|
||||||
|
? unref(props.selectedAction) &&
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
getNodeProperty(unref(props.selectedAction)!.tooltip, unref(props.node))
|
||||||
|
: null) ?? getNodeProperty(props.nodeType.value.label, unref(props.node))
|
||||||
|
);
|
||||||
|
const confirmationLabel = computed(() =>
|
||||||
|
getNodeProperty(
|
||||||
|
unref(props.selectedAction)?.confirmationLabel ?? {
|
||||||
|
text: "Tap again to confirm"
|
||||||
|
},
|
||||||
|
unref(props.node)
|
||||||
|
)
|
||||||
|
);
|
||||||
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
||||||
const progress = computed(
|
const progress = computed(
|
||||||
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) ?? 0
|
() => getNodeProperty(props.nodeType.value.progress, unref(props.node)) ?? 0
|
||||||
|
@ -260,36 +252,21 @@ const progressDisplay = computed(() =>
|
||||||
);
|
);
|
||||||
const canAccept = computed(
|
const canAccept = computed(
|
||||||
() =>
|
() =>
|
||||||
unref(props.dragging) != null &&
|
props.dragging.value != null &&
|
||||||
unref(props.hasDragged) &&
|
unref(props.hasDragged) &&
|
||||||
getNodeProperty(props.nodeType.value.canAccept, unref(props.node))
|
getNodeProperty(props.nodeType.value.canAccept, unref(props.node), props.dragging.value)
|
||||||
);
|
|
||||||
const actionDistance = computed(() =>
|
|
||||||
getNodeProperty(props.nodeType.value.actionDistance, unref(props.node))
|
|
||||||
);
|
);
|
||||||
|
const style = computed(() => getNodeProperty(props.nodeType.value.style, unref(props.node)));
|
||||||
|
const classes = computed(() => getNodeProperty(props.nodeType.value.classes, unref(props.node)));
|
||||||
|
|
||||||
function mouseDown(e: MouseEvent | TouchEvent) {
|
function mouseDown(e: MouseEvent | TouchEvent) {
|
||||||
emit("mouseDown", e, props.node.value.id, isDraggable.value);
|
emit("mouseDown", e, props.node.value, isDraggable.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseUp() {
|
function mouseUp(e: MouseEvent | TouchEvent) {
|
||||||
if (!props.hasDragged?.value) {
|
if (!props.hasDragged?.value) {
|
||||||
|
emit("endDragging", props.node.value);
|
||||||
props.nodeType.value.onClick?.(props.node.value);
|
props.nodeType.value.onClick?.(props.node.value);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
|
||||||
// If the onClick function made this action selected,
|
|
||||||
// don't propagate the event (which will deselect everything)
|
|
||||||
if (action.onClick(unref(props.node)) || unref(props.selectedAction)?.id === action.id) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
|
||||||
if (unref(props.selectedAction)?.id === action.id) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,33 +278,35 @@ function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeActio
|
||||||
transition-duration: 0s;
|
transition-duration: 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.boardnode:hover .body {
|
||||||
|
fill: var(--highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardnode.isSelected .body {
|
||||||
|
fill: var(--accent1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardnode:not(.isDraggable) .body {
|
||||||
|
fill: var(--locked);
|
||||||
|
}
|
||||||
|
|
||||||
.node-title {
|
.node-title {
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
dominant-baseline: middle;
|
dominant-baseline: middle;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
filter: drop-shadow(3px 3px 2px var(--tooltip-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
transition-duration: 0.05s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressRing {
|
.progressRing {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action:not(.boardnode):hover circle,
|
|
||||||
.action:not(.boardnode).selected circle {
|
|
||||||
r: 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:not(.boardnode):hover text,
|
|
||||||
.action:not(.boardnode).selected text {
|
|
||||||
font-size: 187.5%; /* 150% * 1.25 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.action:not(.boardnode) text {
|
|
||||||
text-anchor: middle;
|
|
||||||
dominant-baseline: central;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -353,11 +332,6 @@ function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeActio
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.actions-enter-from .action,
|
|
||||||
.actions-leave-to .action {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grow-enter-from .node-container,
|
.grow-enter-from .node-container,
|
||||||
.grow-leave-to .node-container {
|
.grow-leave-to .node-container {
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
|
|
109
src/features/boards/BoardNodeAction.vue
Normal file
109
src/features/boards/BoardNodeAction.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<transition name="actions" appear>
|
||||||
|
<g v-if="isSelected && actions">
|
||||||
|
<g
|
||||||
|
v-for="(action, index) in actions"
|
||||||
|
:key="action.id"
|
||||||
|
class="action"
|
||||||
|
:class="{ selected: selectedAction?.id === action.id }"
|
||||||
|
:transform="`translate(
|
||||||
|
${
|
||||||
|
(-size - 30) *
|
||||||
|
Math.sin(((actions.length - 1) / 2 - index) * actionDistance)
|
||||||
|
},
|
||||||
|
${
|
||||||
|
(size + 30) *
|
||||||
|
Math.cos(((actions.length - 1) / 2 - index) * actionDistance)
|
||||||
|
}
|
||||||
|
)`"
|
||||||
|
@mousedown="e => performAction(e, action)"
|
||||||
|
@touchstart="e => performAction(e, action)"
|
||||||
|
@mouseup="e => actionMouseUp(e, action)"
|
||||||
|
@touchend.stop="e => actionMouseUp(e, action)"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
:fill="getNodeProperty(action.fillColor, node)"
|
||||||
|
r="20"
|
||||||
|
:stroke-width="selectedAction?.id === action.id ? 4 : 0"
|
||||||
|
:stroke="outlineColor"
|
||||||
|
/>
|
||||||
|
<text :fill="titleColor" class="material-icons">{{
|
||||||
|
getNodeProperty(action.icon, node)
|
||||||
|
}}</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import themes from "data/themes";
|
||||||
|
import type { BoardNode, GenericBoardNodeAction, GenericNodeType } from "features/boards/board";
|
||||||
|
import { getNodeProperty } from "features/boards/board";
|
||||||
|
import settings from "game/settings";
|
||||||
|
import { computed, toRefs, unref } from "vue";
|
||||||
|
|
||||||
|
const _props = defineProps<{
|
||||||
|
node: BoardNode;
|
||||||
|
nodeType: GenericNodeType;
|
||||||
|
actions?: GenericBoardNodeAction[];
|
||||||
|
isSelected: boolean;
|
||||||
|
selectedAction: GenericBoardNodeAction | null;
|
||||||
|
}>();
|
||||||
|
const props = toRefs(_props);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "clickAction", actionId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const size = computed(() => getNodeProperty(props.nodeType.value.size, unref(props.node)));
|
||||||
|
const outlineColor = computed(
|
||||||
|
() =>
|
||||||
|
getNodeProperty(props.nodeType.value.outlineColor, unref(props.node)) ??
|
||||||
|
themes[settings.theme].variables["--outline"]
|
||||||
|
);
|
||||||
|
const titleColor = computed(
|
||||||
|
() =>
|
||||||
|
getNodeProperty(props.nodeType.value.titleColor, unref(props.node)) ??
|
||||||
|
themes[settings.theme].variables["--foreground"]
|
||||||
|
);
|
||||||
|
const actionDistance = computed(() =>
|
||||||
|
getNodeProperty(props.nodeType.value.actionDistance, unref(props.node))
|
||||||
|
);
|
||||||
|
|
||||||
|
function performAction(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
||||||
|
emit("clickAction", action.id);
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionMouseUp(e: MouseEvent | TouchEvent, action: GenericBoardNodeAction) {
|
||||||
|
if (unref(props.selectedAction)?.id === action.id) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action:not(.boardnode):hover circle,
|
||||||
|
.action:not(.boardnode).selected circle {
|
||||||
|
r: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:not(.boardnode):hover text,
|
||||||
|
.action:not(.boardnode).selected text {
|
||||||
|
font-size: 187.5%; /* 150% * 1.25 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:not(.boardnode) text {
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: central;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.actions-enter-from .action,
|
||||||
|
.actions-leave-to .action {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,7 +12,7 @@ import { globalBus } from "game/events";
|
||||||
import { DefaultValue, deletePersistent, Persistent, State } from "game/persistence";
|
import { DefaultValue, deletePersistent, Persistent, State } from "game/persistence";
|
||||||
import { persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
import type { Unsubscribe } from "nanoevents";
|
import type { Unsubscribe } from "nanoevents";
|
||||||
import { isFunction } from "util/common";
|
import { Direction, isFunction } from "util/common";
|
||||||
import type {
|
import type {
|
||||||
Computable,
|
Computable,
|
||||||
GetComputableType,
|
GetComputableType,
|
||||||
|
@ -21,26 +21,35 @@ import type {
|
||||||
} from "util/computed";
|
} from "util/computed";
|
||||||
import { processComputable } from "util/computed";
|
import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { computed, ref, Ref, unref } from "vue";
|
import { computed, isRef, ref, Ref, unref } from "vue";
|
||||||
import panZoom from "vue-panzoom";
|
import panZoom from "vue-panzoom";
|
||||||
import type { Link } from "../links/links";
|
import type { Link } from "../links/links";
|
||||||
|
|
||||||
globalBus.on("setupVue", app => panZoom.install(app));
|
globalBus.on("setupVue", app => panZoom.install(app));
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Board} features. */
|
||||||
export const BoardType = Symbol("Board");
|
export const BoardType = Symbol("Board");
|
||||||
|
|
||||||
export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
|
/**
|
||||||
|
* A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
|
||||||
|
*/
|
||||||
|
export type NodeComputable<T, S extends unknown[] = []> =
|
||||||
|
| Computable<T>
|
||||||
|
| ((node: BoardNode, ...args: S) => T);
|
||||||
|
|
||||||
|
/** Ways to display progress of an action with a duration. */
|
||||||
export enum ProgressDisplay {
|
export enum ProgressDisplay {
|
||||||
Outline = "Outline",
|
Outline = "Outline",
|
||||||
Fill = "Fill"
|
Fill = "Fill"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Node shapes. */
|
||||||
export enum Shape {
|
export enum Shape {
|
||||||
Circle = "Circle",
|
Circle = "Circle",
|
||||||
Diamond = "Triangle"
|
Diamond = "Triangle"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object representing a node on the board. */
|
||||||
export interface BoardNode {
|
export interface BoardNode {
|
||||||
id: number;
|
id: number;
|
||||||
position: {
|
position: {
|
||||||
|
@ -52,54 +61,90 @@ export interface BoardNode {
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object representing a link between two nodes on the board. */
|
||||||
export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> {
|
export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> {
|
||||||
startNode: BoardNode;
|
startNode: BoardNode;
|
||||||
endNode: BoardNode;
|
endNode: BoardNode;
|
||||||
|
stroke: string;
|
||||||
|
strokeWidth: number;
|
||||||
pulsing?: boolean;
|
pulsing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object representing a label for a node. */
|
||||||
export interface NodeLabel {
|
export interface NodeLabel {
|
||||||
text: string;
|
text: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
pulsing?: boolean;
|
pulsing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The persistent data for a board. */
|
||||||
export type BoardData = {
|
export type BoardData = {
|
||||||
nodes: BoardNode[];
|
nodes: BoardNode[];
|
||||||
selectedNode: number | null;
|
selectedNode: number | null;
|
||||||
selectedAction: string | null;
|
selectedAction: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link NodeType}.
|
||||||
|
*/
|
||||||
export interface NodeTypeOptions {
|
export interface NodeTypeOptions {
|
||||||
|
/** The title to display for the node. */
|
||||||
title: NodeComputable<string>;
|
title: NodeComputable<string>;
|
||||||
|
/** An optional label for the node. */
|
||||||
label?: NodeComputable<NodeLabel | null>;
|
label?: NodeComputable<NodeLabel | null>;
|
||||||
|
/** The size of the node - diameter for circles, width and height for squares. */
|
||||||
size: NodeComputable<number>;
|
size: NodeComputable<number>;
|
||||||
|
/** CSS to apply to this node. */
|
||||||
|
style?: NodeComputable<StyleValue>;
|
||||||
|
/** Dictionary of CSS classes to apply to this node. */
|
||||||
|
classes?: NodeComputable<Record<string, boolean>>;
|
||||||
|
/** Whether the node is draggable or not. */
|
||||||
draggable?: NodeComputable<boolean>;
|
draggable?: NodeComputable<boolean>;
|
||||||
|
/** The shape of the node. */
|
||||||
shape: NodeComputable<Shape>;
|
shape: NodeComputable<Shape>;
|
||||||
canAccept?: boolean | Ref<boolean> | ((node: BoardNode, otherNode: BoardNode) => boolean);
|
/** 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. */
|
||||||
progress?: NodeComputable<number>;
|
progress?: NodeComputable<number>;
|
||||||
|
/** How the progress should be displayed on the node. */
|
||||||
progressDisplay?: NodeComputable<ProgressDisplay>;
|
progressDisplay?: NodeComputable<ProgressDisplay>;
|
||||||
|
/** The color of the progress indicator. */
|
||||||
progressColor?: NodeComputable<string>;
|
progressColor?: NodeComputable<string>;
|
||||||
|
/** The fill color of the node. */
|
||||||
fillColor?: NodeComputable<string>;
|
fillColor?: NodeComputable<string>;
|
||||||
|
/** The outline color of the node. */
|
||||||
outlineColor?: NodeComputable<string>;
|
outlineColor?: NodeComputable<string>;
|
||||||
|
/** The color of the title text. */
|
||||||
titleColor?: NodeComputable<string>;
|
titleColor?: NodeComputable<string>;
|
||||||
|
/** The list of action options for the node. */
|
||||||
actions?: BoardNodeActionOptions[];
|
actions?: BoardNodeActionOptions[];
|
||||||
|
/** The arc between each action, in radians. */
|
||||||
actionDistance?: NodeComputable<number>;
|
actionDistance?: NodeComputable<number>;
|
||||||
|
/** A function that is called when the node is clicked. */
|
||||||
onClick?: (node: BoardNode) => void;
|
onClick?: (node: BoardNode) => void;
|
||||||
|
/** A function that is called when a node is dropped onto this node. */
|
||||||
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
|
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
|
||||||
|
/** A function that is called for each node of this type every tick. */
|
||||||
update?: (node: BoardNode, diff: number) => void;
|
update?: (node: BoardNode, diff: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link NodeTypeOptions} to create a {@link NodeType}.
|
||||||
|
*/
|
||||||
export interface BaseNodeType {
|
export interface BaseNodeType {
|
||||||
|
/** The nodes currently on the board of this type. */
|
||||||
nodes: Ref<BoardNode[]>;
|
nodes: Ref<BoardNode[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a type of node that can appear on a board. It will handle getting properties and callbacks for every node of that type. */
|
||||||
export type NodeType<T extends NodeTypeOptions> = Replace<
|
export type NodeType<T extends NodeTypeOptions> = Replace<
|
||||||
T & BaseNodeType,
|
T & BaseNodeType,
|
||||||
{
|
{
|
||||||
title: GetComputableType<T["title"]>;
|
title: GetComputableType<T["title"]>;
|
||||||
label: GetComputableType<T["label"]>;
|
label: GetComputableType<T["label"]>;
|
||||||
size: GetComputableTypeWithDefault<T["size"], 50>;
|
size: GetComputableTypeWithDefault<T["size"], 50>;
|
||||||
|
style: GetComputableType<T["style"]>;
|
||||||
|
classes: GetComputableType<T["classes"]>;
|
||||||
draggable: GetComputableTypeWithDefault<T["draggable"], false>;
|
draggable: GetComputableTypeWithDefault<T["draggable"], false>;
|
||||||
shape: GetComputableTypeWithDefault<T["shape"], Shape.Circle>;
|
shape: GetComputableTypeWithDefault<T["shape"], Shape.Circle>;
|
||||||
canAccept: GetComputableTypeWithDefault<T["canAccept"], false>;
|
canAccept: GetComputableTypeWithDefault<T["canAccept"], false>;
|
||||||
|
@ -114,33 +159,50 @@ export type NodeType<T extends NodeTypeOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link NodeType} object. */
|
||||||
export type GenericNodeType = Replace<
|
export type GenericNodeType = Replace<
|
||||||
NodeType<NodeTypeOptions>,
|
NodeType<NodeTypeOptions>,
|
||||||
{
|
{
|
||||||
size: NodeComputable<number>;
|
size: NodeComputable<number>;
|
||||||
draggable: NodeComputable<boolean>;
|
draggable: NodeComputable<boolean>;
|
||||||
shape: NodeComputable<Shape>;
|
shape: NodeComputable<Shape>;
|
||||||
canAccept: NodeComputable<boolean>;
|
canAccept: NodeComputable<boolean, [BoardNode]>;
|
||||||
progressDisplay: NodeComputable<ProgressDisplay>;
|
progressDisplay: NodeComputable<ProgressDisplay>;
|
||||||
progressColor: NodeComputable<string>;
|
progressColor: NodeComputable<string>;
|
||||||
actionDistance: NodeComputable<number>;
|
actionDistance: NodeComputable<number>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link BoardNodeAction}.
|
||||||
|
*/
|
||||||
export interface BoardNodeActionOptions {
|
export interface BoardNodeActionOptions {
|
||||||
|
/** A unique identifier for the action. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Whether this action should be visible. */
|
||||||
visibility?: NodeComputable<Visibility | boolean>;
|
visibility?: NodeComputable<Visibility | boolean>;
|
||||||
|
/** The icon to display for the action. */
|
||||||
icon: NodeComputable<string>;
|
icon: NodeComputable<string>;
|
||||||
|
/** The fill color of the action. */
|
||||||
fillColor?: NodeComputable<string>;
|
fillColor?: NodeComputable<string>;
|
||||||
tooltip: NodeComputable<string>;
|
/** The tooltip text to display for the action. */
|
||||||
|
tooltip: NodeComputable<NodeLabel>;
|
||||||
|
/** The confirmation label that appears under the action. */
|
||||||
|
confirmationLabel?: NodeComputable<NodeLabel>;
|
||||||
|
/** An array of board node links associated with the action. They appear when the action is focused. */
|
||||||
links?: NodeComputable<BoardNodeLink[]>;
|
links?: NodeComputable<BoardNodeLink[]>;
|
||||||
onClick: (node: BoardNode) => boolean | undefined;
|
/** A function that is called when the action is clicked. */
|
||||||
|
onClick: (node: BoardNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link BoardNodeActionOptions} to create an {@link BoardNodeAction}.
|
||||||
|
*/
|
||||||
export interface BaseBoardNodeAction {
|
export interface BaseBoardNodeAction {
|
||||||
links?: Ref<BoardNodeLink[]>;
|
links?: Ref<BoardNodeLink[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents an action that can be taken upon a node. */
|
||||||
export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
||||||
T & BaseBoardNodeAction,
|
T & BaseBoardNodeAction,
|
||||||
{
|
{
|
||||||
|
@ -148,40 +210,73 @@ export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
|
||||||
icon: GetComputableType<T["icon"]>;
|
icon: GetComputableType<T["icon"]>;
|
||||||
fillColor: GetComputableType<T["fillColor"]>;
|
fillColor: GetComputableType<T["fillColor"]>;
|
||||||
tooltip: GetComputableType<T["tooltip"]>;
|
tooltip: GetComputableType<T["tooltip"]>;
|
||||||
|
confirmationLabel: GetComputableTypeWithDefault<T["confirmationLabel"], NodeLabel>;
|
||||||
links: GetComputableType<T["links"]>;
|
links: GetComputableType<T["links"]>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link BoardNodeAction} object. */
|
||||||
export type GenericBoardNodeAction = Replace<
|
export type GenericBoardNodeAction = Replace<
|
||||||
BoardNodeAction<BoardNodeActionOptions>,
|
BoardNodeAction<BoardNodeActionOptions>,
|
||||||
{
|
{
|
||||||
visibility: NodeComputable<Visibility | boolean>;
|
visibility: NodeComputable<Visibility | boolean>;
|
||||||
|
confirmationLabel: NodeComputable<NodeLabel>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Board}.
|
||||||
|
*/
|
||||||
export interface BoardOptions {
|
export interface BoardOptions {
|
||||||
|
/** Whether this board should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The height of the board. Defaults to 100% */
|
||||||
height?: Computable<string>;
|
height?: Computable<string>;
|
||||||
|
/** The width of the board. Defaults to 100% */
|
||||||
width?: Computable<string>;
|
width?: Computable<string>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** A function that returns an array of initial board nodes, without IDs. */
|
||||||
startNodes: () => Omit<BoardNode, "id">[];
|
startNodes: () => Omit<BoardNode, "id">[];
|
||||||
|
/** A dictionary of node types that can appear on the board. */
|
||||||
types: Record<string, NodeTypeOptions>;
|
types: Record<string, NodeTypeOptions>;
|
||||||
|
/** The persistent state of the board. */
|
||||||
state?: Computable<BoardData>;
|
state?: Computable<BoardData>;
|
||||||
|
/** An array of board node links to display. */
|
||||||
links?: Computable<BoardNodeLink[] | null>;
|
links?: Computable<BoardNodeLink[] | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
|
||||||
|
*/
|
||||||
export interface BaseBoard {
|
export interface BaseBoard {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** All the nodes currently on the board. */
|
||||||
nodes: Ref<BoardNode[]>;
|
nodes: Ref<BoardNode[]>;
|
||||||
|
/** The currently selected node, if any. */
|
||||||
selectedNode: Ref<BoardNode | null>;
|
selectedNode: Ref<BoardNode | null>;
|
||||||
|
/** The currently selected action, if any. */
|
||||||
selectedAction: Ref<GenericBoardNodeAction | null>;
|
selectedAction: Ref<GenericBoardNodeAction | null>;
|
||||||
|
/** The currently being dragged node, if any. */
|
||||||
|
draggingNode: Ref<BoardNode | null>;
|
||||||
|
/** If dragging a node, the node it's currently being hovered over, if any. */
|
||||||
|
receivingNode: Ref<BoardNode | null>;
|
||||||
|
/** The current mouse position, if over the board. */
|
||||||
mousePosition: Ref<{ x: number; y: number } | null>;
|
mousePosition: Ref<{ x: number; y: number } | null>;
|
||||||
|
/** Places a node in the nearest empty space in the given direction with the specified space around it. */
|
||||||
|
placeInAvailableSpace: (node: BoardNode, radius?: number, direction?: Direction) => void;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof BoardType;
|
type: typeof BoardType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that is a zoomable, pannable board with various nodes upon it. */
|
||||||
export type Board<T extends BoardOptions> = Replace<
|
export type Board<T extends BoardOptions> = Replace<
|
||||||
T & BaseBoard,
|
T & BaseBoard,
|
||||||
{
|
{
|
||||||
|
@ -196,6 +291,7 @@ export type Board<T extends BoardOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Board} object. */
|
||||||
export type GenericBoard = Replace<
|
export type GenericBoard = Replace<
|
||||||
Board<BoardOptions>,
|
Board<BoardOptions>,
|
||||||
{
|
{
|
||||||
|
@ -205,6 +301,10 @@ export type GenericBoard = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a board with the given options.
|
||||||
|
* @param optionsFunc Board options.
|
||||||
|
*/
|
||||||
export function createBoard<T extends BoardOptions>(
|
export function createBoard<T extends BoardOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
|
||||||
): Board<T> {
|
): Board<T> {
|
||||||
|
@ -217,8 +317,8 @@ export function createBoard<T extends BoardOptions>(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const board = optionsFunc();
|
const board = optionsFunc.call(feature, feature);
|
||||||
board.id = getUniqueID("board-");
|
board.id = getUniqueID("board-");
|
||||||
board.type = BoardType;
|
board.type = BoardType;
|
||||||
board[Component] = BoardComponent as GenericComponent;
|
board[Component] = BoardComponent as GenericComponent;
|
||||||
|
@ -239,26 +339,51 @@ export function createBoard<T extends BoardOptions>(
|
||||||
}
|
}
|
||||||
|
|
||||||
board.nodes = computed(() => unref(processedBoard.state).nodes);
|
board.nodes = computed(() => unref(processedBoard.state).nodes);
|
||||||
board.selectedNode = computed(
|
board.selectedNode = computed({
|
||||||
() =>
|
get() {
|
||||||
processedBoard.nodes.value.find(
|
return (
|
||||||
node => node.id === unref(processedBoard.state).selectedNode
|
processedBoard.nodes.value.find(
|
||||||
) || null
|
node => node.id === unref(processedBoard.state).selectedNode
|
||||||
);
|
) || null
|
||||||
board.selectedAction = computed(() => {
|
);
|
||||||
const selectedNode = processedBoard.selectedNode.value;
|
},
|
||||||
if (selectedNode == null) {
|
set(node) {
|
||||||
return null;
|
if (isRef(processedBoard.state)) {
|
||||||
|
processedBoard.state.value = {
|
||||||
|
...processedBoard.state.value,
|
||||||
|
selectedNode: node?.id ?? null
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
processedBoard.state.selectedNode = node?.id ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const type = processedBoard.types[selectedNode.type];
|
});
|
||||||
if (type.actions == null) {
|
board.selectedAction = computed({
|
||||||
return null;
|
get() {
|
||||||
|
const selectedNode = processedBoard.selectedNode.value;
|
||||||
|
if (selectedNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const type = processedBoard.types[selectedNode.type];
|
||||||
|
if (type.actions == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
type.actions.find(
|
||||||
|
action => action.id === unref(processedBoard.state).selectedAction
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set(action) {
|
||||||
|
if (isRef(processedBoard.state)) {
|
||||||
|
processedBoard.state.value = {
|
||||||
|
...processedBoard.state.value,
|
||||||
|
selectedAction: action?.id ?? null
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
processedBoard.state.selectedAction = action?.id ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
type.actions.find(
|
|
||||||
action => action.id === unref(processedBoard.state).selectedAction
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
board.mousePosition = ref(null);
|
board.mousePosition = ref(null);
|
||||||
if (board.links) {
|
if (board.links) {
|
||||||
|
@ -280,12 +405,14 @@ export function createBoard<T extends BoardOptions>(
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
board.draggingNode = ref(null);
|
||||||
|
board.receivingNode = ref(null);
|
||||||
processComputable(board as T, "visibility");
|
processComputable(board as T, "visibility");
|
||||||
setDefault(board, "visibility", Visibility.Visible);
|
setDefault(board, "visibility", Visibility.Visible);
|
||||||
processComputable(board as T, "width");
|
processComputable(board as T, "width");
|
||||||
setDefault(board, "width", "100%");
|
setDefault(board, "width", "100%");
|
||||||
processComputable(board as T, "height");
|
processComputable(board as T, "height");
|
||||||
setDefault(board, "height", "400px");
|
setDefault(board, "height", "100%");
|
||||||
processComputable(board as T, "classes");
|
processComputable(board as T, "classes");
|
||||||
processComputable(board as T, "style");
|
processComputable(board as T, "style");
|
||||||
|
|
||||||
|
@ -296,6 +423,8 @@ export function createBoard<T extends BoardOptions>(
|
||||||
processComputable(nodeType as NodeTypeOptions, "label");
|
processComputable(nodeType as NodeTypeOptions, "label");
|
||||||
processComputable(nodeType as NodeTypeOptions, "size");
|
processComputable(nodeType as NodeTypeOptions, "size");
|
||||||
setDefault(nodeType, "size", 50);
|
setDefault(nodeType, "size", 50);
|
||||||
|
processComputable(nodeType as NodeTypeOptions, "style");
|
||||||
|
processComputable(nodeType as NodeTypeOptions, "classes");
|
||||||
processComputable(nodeType as NodeTypeOptions, "draggable");
|
processComputable(nodeType as NodeTypeOptions, "draggable");
|
||||||
setDefault(nodeType, "draggable", false);
|
setDefault(nodeType, "draggable", false);
|
||||||
processComputable(nodeType as NodeTypeOptions, "shape");
|
processComputable(nodeType as NodeTypeOptions, "shape");
|
||||||
|
@ -326,11 +455,92 @@ export function createBoard<T extends BoardOptions>(
|
||||||
processComputable(action, "icon");
|
processComputable(action, "icon");
|
||||||
processComputable(action, "fillColor");
|
processComputable(action, "fillColor");
|
||||||
processComputable(action, "tooltip");
|
processComputable(action, "tooltip");
|
||||||
|
processComputable(action, "confirmationLabel");
|
||||||
|
setDefault(action, "confirmationLabel", { text: "Tap again to confirm" });
|
||||||
processComputable(action, "links");
|
processComputable(action, "links");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setDraggingNode(node: BoardNode | null) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
board.draggingNode!.value = node;
|
||||||
|
}
|
||||||
|
function setReceivingNode(node: BoardNode | null) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
board.receivingNode!.value = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
board.placeInAvailableSpace = function (
|
||||||
|
node: BoardNode,
|
||||||
|
radius = 100,
|
||||||
|
direction = Direction.Right
|
||||||
|
) {
|
||||||
|
const nodes = processedBoard.nodes.value
|
||||||
|
.slice()
|
||||||
|
.filter(n => {
|
||||||
|
// Exclude self
|
||||||
|
if (n === node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude nodes that aren't within the corridor we'll be moving within
|
||||||
|
if (
|
||||||
|
(direction === Direction.Down || direction === Direction.Up) &&
|
||||||
|
Math.abs(n.position.x - node.position.x) > radius
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(direction === Direction.Left || direction === Direction.Right) &&
|
||||||
|
Math.abs(n.position.y - node.position.y) > radius
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude nodes in the wrong direction
|
||||||
|
return !(
|
||||||
|
(direction === Direction.Right &&
|
||||||
|
n.position.x < node.position.x - radius) ||
|
||||||
|
(direction === Direction.Left && n.position.x > node.position.x + radius) ||
|
||||||
|
(direction === Direction.Up && n.position.y > node.position.y + radius) ||
|
||||||
|
(direction === Direction.Down && n.position.y < node.position.y - radius)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort(
|
||||||
|
direction === Direction.Right
|
||||||
|
? (a, b) => a.position.x - b.position.x
|
||||||
|
: direction === Direction.Left
|
||||||
|
? (a, b) => b.position.x - a.position.x
|
||||||
|
: direction === Direction.Up
|
||||||
|
? (a, b) => b.position.y - a.position.y
|
||||||
|
: (a, b) => a.position.y - b.position.y
|
||||||
|
);
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const nodeToCheck = nodes[i];
|
||||||
|
const distance =
|
||||||
|
direction === Direction.Right || direction === Direction.Left
|
||||||
|
? Math.abs(node.position.x - nodeToCheck.position.x)
|
||||||
|
: Math.abs(node.position.y - nodeToCheck.position.y);
|
||||||
|
|
||||||
|
// If we're too close to this node, move further
|
||||||
|
if (distance < radius) {
|
||||||
|
if (direction === Direction.Right) {
|
||||||
|
node.position.x = nodeToCheck.position.x + radius;
|
||||||
|
} else if (direction === Direction.Left) {
|
||||||
|
node.position.x = nodeToCheck.position.x - radius;
|
||||||
|
} else if (direction === Direction.Up) {
|
||||||
|
node.position.y = nodeToCheck.position.y - radius;
|
||||||
|
} else if (direction === Direction.Down) {
|
||||||
|
node.position.y = nodeToCheck.position.y + radius;
|
||||||
|
}
|
||||||
|
} else if (i > 0 && distance > radius) {
|
||||||
|
// If we're further from this node than the radius, then the nodes are past us and we can early exit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
board[GatherProps] = function (this: GenericBoard) {
|
board[GatherProps] = function (this: GenericBoard) {
|
||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
|
@ -344,7 +554,9 @@ export function createBoard<T extends BoardOptions>(
|
||||||
links,
|
links,
|
||||||
selectedAction,
|
selectedAction,
|
||||||
selectedNode,
|
selectedNode,
|
||||||
mousePosition
|
mousePosition,
|
||||||
|
draggingNode,
|
||||||
|
receivingNode
|
||||||
} = this;
|
} = this;
|
||||||
return {
|
return {
|
||||||
nodes,
|
nodes,
|
||||||
|
@ -358,7 +570,11 @@ export function createBoard<T extends BoardOptions>(
|
||||||
links,
|
links,
|
||||||
selectedAction,
|
selectedAction,
|
||||||
selectedNode,
|
selectedNode,
|
||||||
mousePosition
|
mousePosition,
|
||||||
|
draggingNode,
|
||||||
|
receivingNode,
|
||||||
|
setDraggingNode,
|
||||||
|
setReceivingNode
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -368,10 +584,25 @@ export function createBoard<T extends BoardOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
|
/**
|
||||||
return isFunction<T, [BoardNode], Computable<T>>(property) ? property(node) : unref(property);
|
* Gets the value of a property for a specified node.
|
||||||
|
* @param property The property to find the value of
|
||||||
|
* @param node The node to get the property of
|
||||||
|
*/
|
||||||
|
export function getNodeProperty<T, S extends unknown[]>(
|
||||||
|
property: NodeComputable<T, S>,
|
||||||
|
node: BoardNode,
|
||||||
|
...args: S
|
||||||
|
): T {
|
||||||
|
return isFunction<T, [BoardNode, ...S], Computable<T>>(property)
|
||||||
|
? property(node, ...args)
|
||||||
|
: unref(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to get an ID for a node that is guaranteed unique.
|
||||||
|
* @param board The board feature to generate an ID for
|
||||||
|
*/
|
||||||
export function getUniqueNodeID(board: GenericBoard): number {
|
export function getUniqueNodeID(board: GenericBoard): number {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
board.nodes.value.forEach(node => {
|
board.nodes.value.forEach(node => {
|
||||||
|
|
|
@ -38,6 +38,7 @@ import type { GenericChallenge } from "features/challenges/challenge";
|
||||||
import type { StyleValue } from "features/feature";
|
import type { StyleValue } from "features/feature";
|
||||||
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
import { isHidden, isVisible, jsx, Visibility } from "features/feature";
|
||||||
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
|
||||||
|
import { displayRequirements, Requirements } from "game/requirements";
|
||||||
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
|
||||||
import type { Component, PropType, UnwrapRef } from "vue";
|
import type { Component, PropType, UnwrapRef } from "vue";
|
||||||
import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
|
||||||
|
@ -61,6 +62,7 @@ export default defineComponent({
|
||||||
Object,
|
Object,
|
||||||
Function
|
Function
|
||||||
),
|
),
|
||||||
|
requirements: processedPropType<Requirements>(Object, Array),
|
||||||
visibility: {
|
visibility: {
|
||||||
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
type: processedPropType<Visibility | boolean>(Number, Boolean),
|
||||||
required: true
|
required: true
|
||||||
|
@ -90,7 +92,7 @@ export default defineComponent({
|
||||||
Node
|
Node
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { active, maxed, canComplete, display } = toRefs(props);
|
const { active, maxed, canComplete, display, requirements } = toRefs(props);
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
if (active.value) {
|
if (active.value) {
|
||||||
|
@ -128,7 +130,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
const Title = coerceComponent(currDisplay.title || "", "h3");
|
const Title = coerceComponent(currDisplay.title || "", "h3");
|
||||||
const Description = coerceComponent(currDisplay.description, "div");
|
const Description = coerceComponent(currDisplay.description, "div");
|
||||||
const Goal = coerceComponent(currDisplay.goal || "");
|
const Goal = coerceComponent(currDisplay.goal != null ? currDisplay.goal : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
|
||||||
const Reward = coerceComponent(currDisplay.reward || "");
|
const Reward = coerceComponent(currDisplay.reward || "");
|
||||||
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
|
||||||
comp.value = coerceComponent(
|
comp.value = coerceComponent(
|
||||||
|
@ -140,12 +142,10 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<Description />
|
<Description />
|
||||||
{currDisplay.goal != null ? (
|
<div>
|
||||||
<div>
|
<br />
|
||||||
<br />
|
Goal: <Goal />
|
||||||
Goal: <Goal />
|
</div>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{currDisplay.reward != null ? (
|
{currDisplay.reward != null ? (
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { isArray } from "@vue/shared";
|
import { isArray } from "@vue/shared";
|
||||||
import Toggle from "components/fields/Toggle.vue";
|
import Toggle from "components/fields/Toggle.vue";
|
||||||
import ChallengeComponent from "features/challenges/Challenge.vue";
|
import ChallengeComponent from "features/challenges/Challenge.vue";
|
||||||
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -11,17 +12,17 @@ import type {
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
GatherProps,
|
GatherProps,
|
||||||
|
Visibility,
|
||||||
getUniqueID,
|
getUniqueID,
|
||||||
isVisible,
|
isVisible,
|
||||||
jsx,
|
jsx,
|
||||||
setDefault,
|
setDefault
|
||||||
Visibility
|
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import type { GenericReset } from "features/reset";
|
import type { GenericReset } from "features/reset";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
import type { Persistent } from "game/persistence";
|
import type { Persistent } from "game/persistence";
|
||||||
import { persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
import { maxRequirementsMet, Requirements } from "game/requirements";
|
import { Requirements, maxRequirementsMet } from "game/requirements";
|
||||||
import settings, { registerSettingField } from "game/settings";
|
import settings, { registerSettingField } from "game/settings";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
|
@ -51,8 +52,6 @@ export interface ChallengeOptions {
|
||||||
reset?: GenericReset;
|
reset?: GenericReset;
|
||||||
/** The requirement(s) to complete this challenge. */
|
/** The requirement(s) to complete this challenge. */
|
||||||
requirements: Requirements;
|
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. */
|
/** The maximum number of times the challenge can be completed. */
|
||||||
completionLimit?: Computable<DecimalSource>;
|
completionLimit?: Computable<DecimalSource>;
|
||||||
/** Shows a marker on the corner of the feature. */
|
/** Shows a marker on the corner of the feature. */
|
||||||
|
@ -69,7 +68,7 @@ export interface ChallengeOptions {
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
/** The main text that appears in the display. */
|
/** The main text that appears in the display. */
|
||||||
description: CoercableComponent;
|
description: CoercableComponent;
|
||||||
/** A description of the current goal for this challenge. */
|
/** A description of the current goal for this challenge. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
|
||||||
goal?: CoercableComponent;
|
goal?: CoercableComponent;
|
||||||
/** A description of what will change upon completing this challenge. */
|
/** A description of what will change upon completing this challenge. */
|
||||||
reward?: CoercableComponent;
|
reward?: CoercableComponent;
|
||||||
|
@ -89,7 +88,7 @@ export interface ChallengeOptions {
|
||||||
* The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
|
* The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
|
||||||
*/
|
*/
|
||||||
export interface BaseChallenge {
|
export interface BaseChallenge {
|
||||||
/** An auto-generated ID for identifying challenges that appear in the DOM. Will not persist between refreshes or updates. */
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
/** The current amount of times this challenge can be completed. */
|
/** The current amount of times this challenge can be completed. */
|
||||||
canComplete: Ref<DecimalSource>;
|
canComplete: Ref<DecimalSource>;
|
||||||
|
@ -123,7 +122,6 @@ export type Challenge<T extends ChallengeOptions> = Replace<
|
||||||
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
|
||||||
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
|
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
|
||||||
requirements: GetComputableType<T["requirements"]>;
|
requirements: GetComputableType<T["requirements"]>;
|
||||||
maximize: GetComputableType<T["maximize"]>;
|
|
||||||
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
|
||||||
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
|
||||||
classes: GetComputableType<T["classes"]>;
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
@ -148,19 +146,30 @@ export type GenericChallenge = Replace<
|
||||||
* @param optionsFunc Challenge options.
|
* @param optionsFunc Challenge options.
|
||||||
*/
|
*/
|
||||||
export function createChallenge<T extends ChallengeOptions>(
|
export function createChallenge<T extends ChallengeOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
|
optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Challenge<T> {
|
): Challenge<T> {
|
||||||
const completions = persistent(0);
|
const completions = persistent(0);
|
||||||
const active = persistent(false, false);
|
const active = persistent(false, false);
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const challenge = optionsFunc();
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const challenge = optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
challenge.id = getUniqueID("challenge-");
|
challenge.id = getUniqueID("challenge-");
|
||||||
challenge.type = ChallengeType;
|
challenge.type = ChallengeType;
|
||||||
challenge[Component] = ChallengeComponent as GenericComponent;
|
challenge[Component] = ChallengeComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
challenge.completions = completions;
|
challenge.completions = completions;
|
||||||
challenge.active = active;
|
challenge.active = active;
|
||||||
|
Object.assign(challenge, decoratedData);
|
||||||
|
|
||||||
challenge.completed = computed(() =>
|
challenge.completed = computed(() =>
|
||||||
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
||||||
);
|
);
|
||||||
|
@ -198,10 +207,7 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
challenge.canComplete = computed(() =>
|
challenge.canComplete = computed(() =>
|
||||||
Decimal.max(
|
maxRequirementsMet((challenge as GenericChallenge).requirements)
|
||||||
maxRequirementsMet((challenge as GenericChallenge).requirements),
|
|
||||||
unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
challenge.complete = function (remainInChallenge?: boolean) {
|
challenge.complete = function (remainInChallenge?: boolean) {
|
||||||
const genericChallenge = challenge as GenericChallenge;
|
const genericChallenge = challenge as GenericChallenge;
|
||||||
|
@ -242,7 +248,6 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
|
|
||||||
processComputable(challenge as T, "canStart");
|
processComputable(challenge as T, "canStart");
|
||||||
setDefault(challenge, "canStart", true);
|
setDefault(challenge, "canStart", true);
|
||||||
processComputable(challenge as T, "maximize");
|
|
||||||
processComputable(challenge as T, "completionLimit");
|
processComputable(challenge as T, "completionLimit");
|
||||||
setDefault(challenge, "completionLimit", 1);
|
setDefault(challenge, "completionLimit", 1);
|
||||||
processComputable(challenge as T, "mark");
|
processComputable(challenge as T, "mark");
|
||||||
|
@ -258,6 +263,14 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(challenge)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
challenge[GatherProps] = function (this: GenericChallenge) {
|
challenge[GatherProps] = function (this: GenericChallenge) {
|
||||||
const {
|
const {
|
||||||
active,
|
active,
|
||||||
|
@ -271,7 +284,8 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
canStart,
|
canStart,
|
||||||
mark,
|
mark,
|
||||||
id,
|
id,
|
||||||
toggle
|
toggle,
|
||||||
|
requirements
|
||||||
} = this;
|
} = this;
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
|
@ -285,7 +299,9 @@ export function createChallenge<T extends ChallengeOptions>(
|
||||||
canStart,
|
canStart,
|
||||||
mark,
|
mark,
|
||||||
id,
|
id,
|
||||||
toggle
|
toggle,
|
||||||
|
requirements,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ClickableComponent from "features/clickables/Clickable.vue";
|
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||||
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -6,7 +7,7 @@ import type {
|
||||||
Replace,
|
Replace,
|
||||||
StyleValue
|
StyleValue
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
|
import { Component, GatherProps, Visibility, getUniqueID, setDefault } from "features/feature";
|
||||||
import type { BaseLayer } from "game/layers";
|
import type { BaseLayer } from "game/layers";
|
||||||
import type { Unsubscribe } from "nanoevents";
|
import type { Unsubscribe } from "nanoevents";
|
||||||
import type {
|
import type {
|
||||||
|
@ -19,33 +20,56 @@ import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Clickable} features. */
|
||||||
export const ClickableType = Symbol("Clickable");
|
export const ClickableType = Symbol("Clickable");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Clickable}.
|
||||||
|
*/
|
||||||
export interface ClickableOptions {
|
export interface ClickableOptions {
|
||||||
|
/** Whether this clickable should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** Whether or not the clickable may be clicked. */
|
||||||
canClick?: Computable<boolean>;
|
canClick?: Computable<boolean>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
|
/** The display to use for this clickable. */
|
||||||
display?: Computable<
|
display?: Computable<
|
||||||
| CoercableComponent
|
| CoercableComponent
|
||||||
| {
|
| {
|
||||||
|
/** A header to appear at the top of the display. */
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
|
/** The main text that appears in the display. */
|
||||||
description: CoercableComponent;
|
description: CoercableComponent;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
/** Toggles a smaller design for the feature. */
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
|
/** A function that is called when the clickable is clicked. */
|
||||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
|
/** A function that is called when the clickable is held down. */
|
||||||
onHold?: VoidFunction;
|
onHold?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ClickableOptions} to create an {@link Clickable}.
|
||||||
|
*/
|
||||||
export interface BaseClickable {
|
export interface BaseClickable {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof ClickableType;
|
type: typeof ClickableType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that can be clicked or held down. */
|
||||||
export type Clickable<T extends ClickableOptions> = Replace<
|
export type Clickable<T extends ClickableOptions> = Replace<
|
||||||
T & BaseClickable,
|
T & BaseClickable,
|
||||||
{
|
{
|
||||||
|
@ -58,6 +82,7 @@ export type Clickable<T extends ClickableOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Clickable} object. */
|
||||||
export type GenericClickable = Replace<
|
export type GenericClickable = Replace<
|
||||||
Clickable<ClickableOptions>,
|
Clickable<ClickableOptions>,
|
||||||
{
|
{
|
||||||
|
@ -66,15 +91,32 @@ export type GenericClickable = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a clickable with the given options.
|
||||||
|
* @param optionsFunc Clickable options.
|
||||||
|
*/
|
||||||
export function createClickable<T extends ClickableOptions>(
|
export function createClickable<T extends ClickableOptions>(
|
||||||
optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>
|
optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Clickable<T> {
|
): Clickable<T> {
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const clickable = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const clickable =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
clickable.id = getUniqueID("clickable-");
|
clickable.id = getUniqueID("clickable-");
|
||||||
clickable.type = ClickableType;
|
clickable.type = ClickableType;
|
||||||
clickable[Component] = ClickableComponent as GenericComponent;
|
clickable[Component] = ClickableComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(clickable);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(clickable, decoratedData);
|
||||||
|
|
||||||
processComputable(clickable as T, "visibility");
|
processComputable(clickable as T, "visibility");
|
||||||
setDefault(clickable, "visibility", Visibility.Visible);
|
setDefault(clickable, "visibility", Visibility.Visible);
|
||||||
processComputable(clickable as T, "canClick");
|
processComputable(clickable as T, "canClick");
|
||||||
|
@ -101,6 +143,14 @@ export function createClickable<T extends ClickableOptions>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(clickable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(clickable)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
clickable[GatherProps] = function (this: GenericClickable) {
|
clickable[GatherProps] = function (this: GenericClickable) {
|
||||||
const {
|
const {
|
||||||
display,
|
display,
|
||||||
|
@ -124,7 +174,8 @@ export function createClickable<T extends ClickableOptions>(
|
||||||
canClick,
|
canClick,
|
||||||
small,
|
small,
|
||||||
mark,
|
mark,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -132,6 +183,12 @@ export function createClickable<T extends ClickableOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to auto click a clickable whenever it can be.
|
||||||
|
* @param layer The layer the clickable is apart of
|
||||||
|
* @param clickable The clicker to click automatically
|
||||||
|
* @param autoActive Whether or not the clickable should currently be auto-clicking
|
||||||
|
*/
|
||||||
export function setupAutoClick(
|
export function setupAutoClick(
|
||||||
layer: BaseLayer,
|
layer: BaseLayer,
|
||||||
clickable: GenericClickable,
|
clickable: GenericClickable,
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import type { OptionsFunc, Replace } from "features/feature";
|
import type { CoercableComponent, OptionsFunc, Replace } from "features/feature";
|
||||||
import { setDefault } from "features/feature";
|
import { setDefault } from "features/feature";
|
||||||
import type { Resource } from "features/resources/resource";
|
import type { Resource } from "features/resources/resource";
|
||||||
import Formula from "game/formulas/formulas";
|
import Formula from "game/formulas/formulas";
|
||||||
import {
|
import { InvertibleFormula, InvertibleIntegralFormula } from "game/formulas/types";
|
||||||
IntegrableFormula,
|
|
||||||
InvertibleFormula,
|
|
||||||
InvertibleIntegralFormula
|
|
||||||
} from "game/formulas/types";
|
|
||||||
import type { BaseLayer } from "game/layers";
|
import type { BaseLayer } from "game/layers";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
|
@ -15,6 +11,8 @@ import { convertComputable, processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
import { GenericDecorator } from "./decorators/common";
|
||||||
|
import { createBooleanRequirement } from "game/requirements";
|
||||||
|
|
||||||
/** An object that configures a {@link Conversion}. */
|
/** An object that configures a {@link Conversion}. */
|
||||||
export interface ConversionOptions {
|
export interface ConversionOptions {
|
||||||
|
@ -22,9 +20,7 @@ export interface ConversionOptions {
|
||||||
* The formula used to determine how much {@link gainResource} should be earned by this converting.
|
* The formula used to determine how much {@link gainResource} should be earned by this converting.
|
||||||
* The passed value will be a Formula representing the {@link baseResource} variable.
|
* The passed value will be a Formula representing the {@link baseResource} variable.
|
||||||
*/
|
*/
|
||||||
formula: (
|
formula: (variable: InvertibleIntegralFormula) => InvertibleFormula;
|
||||||
variable: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula
|
|
||||||
) => InvertibleFormula;
|
|
||||||
/**
|
/**
|
||||||
* How much of the output resource the conversion can currently convert for.
|
* How much of the output resource the conversion can currently convert for.
|
||||||
* Typically this will be set for you in a conversion constructor.
|
* Typically this will be set for you in a conversion constructor.
|
||||||
|
@ -123,21 +119,26 @@ export type GenericConversion = Replace<
|
||||||
* @see {@link createIndependentConversion}.
|
* @see {@link createIndependentConversion}.
|
||||||
*/
|
*/
|
||||||
export function createConversion<T extends ConversionOptions>(
|
export function createConversion<T extends ConversionOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseConversion, GenericConversion>
|
optionsFunc: OptionsFunc<T, BaseConversion, GenericConversion>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Conversion<T> {
|
): Conversion<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const conversion = optionsFunc();
|
const conversion = optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(conversion);
|
||||||
|
}
|
||||||
|
|
||||||
(conversion as GenericConversion).formula = conversion.formula(
|
(conversion as GenericConversion).formula = conversion.formula(
|
||||||
Formula.variable(conversion.baseResource)
|
Formula.variable(conversion.baseResource)
|
||||||
);
|
);
|
||||||
if (conversion.currentGain == null) {
|
if (conversion.currentGain == null) {
|
||||||
conversion.currentGain = computed(() => {
|
conversion.currentGain = computed(() => {
|
||||||
let gain = (conversion as GenericConversion).formula.evaluate(
|
let gain = Decimal.floor(
|
||||||
conversion.baseResource.value
|
(conversion as GenericConversion).formula.evaluate(
|
||||||
);
|
conversion.baseResource.value
|
||||||
gain = Decimal.floor(gain).max(0);
|
)
|
||||||
|
).max(0);
|
||||||
if (unref(conversion.buyMax) === false) {
|
if (unref(conversion.buyMax) === false) {
|
||||||
gain = gain.min(1);
|
gain = gain.min(1);
|
||||||
}
|
}
|
||||||
|
@ -187,6 +188,10 @@ export function createConversion<T extends ConversionOptions>(
|
||||||
processComputable(conversion as T, "buyMax");
|
processComputable(conversion as T, "buyMax");
|
||||||
setDefault(conversion, "buyMax", true);
|
setDefault(conversion, "buyMax", true);
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(conversion);
|
||||||
|
}
|
||||||
|
|
||||||
return conversion as unknown as Conversion<T>;
|
return conversion as unknown as Conversion<T>;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -211,17 +216,18 @@ export function createCumulativeConversion<S extends ConversionOptions>(
|
||||||
export function createIndependentConversion<S extends ConversionOptions>(
|
export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
optionsFunc: OptionsFunc<S, BaseConversion, GenericConversion>
|
optionsFunc: OptionsFunc<S, BaseConversion, GenericConversion>
|
||||||
): Conversion<S> {
|
): Conversion<S> {
|
||||||
return createConversion(() => {
|
return createConversion(feature => {
|
||||||
const conversion: S = optionsFunc();
|
const conversion: S = optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
setDefault(conversion, "buyMax", false);
|
setDefault(conversion, "buyMax", false);
|
||||||
|
|
||||||
if (conversion.currentGain == null) {
|
if (conversion.currentGain == null) {
|
||||||
conversion.currentGain = computed(() => {
|
conversion.currentGain = computed(() => {
|
||||||
let gain = (conversion as unknown as GenericConversion).formula.evaluate(
|
let gain = Decimal.floor(
|
||||||
conversion.baseResource.value
|
(conversion as unknown as GenericConversion).formula.evaluate(
|
||||||
);
|
conversion.baseResource.value
|
||||||
gain = Decimal.floor(gain).max(conversion.gainResource.value);
|
)
|
||||||
|
).max(conversion.gainResource.value);
|
||||||
if (unref(conversion.buyMax) === false) {
|
if (unref(conversion.buyMax) === false) {
|
||||||
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
|
||||||
}
|
}
|
||||||
|
@ -235,7 +241,9 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
conversion.baseResource.value
|
conversion.baseResource.value
|
||||||
),
|
),
|
||||||
conversion.gainResource.value
|
conversion.gainResource.value
|
||||||
).max(0);
|
)
|
||||||
|
.floor()
|
||||||
|
.max(0);
|
||||||
|
|
||||||
if (unref(conversion.buyMax) === false) {
|
if (unref(conversion.buyMax) === false) {
|
||||||
gain = gain.min(1);
|
gain = gain.min(1);
|
||||||
|
@ -263,13 +271,13 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
||||||
* @param layer The layer this passive generation will be associated with. Typically `this` when calling this function from inside a layer's options function.
|
* @param layer The layer this passive generation will be associated with. Typically `this` when calling this function from inside a layer's options function.
|
||||||
* @param conversion The conversion that will determine how much generation there is.
|
* @param conversion The conversion that will determine how much generation there is.
|
||||||
* @param rate A multiplier to multiply against the conversion's currentGain.
|
* @param rate A multiplier to multiply against the conversion's currentGain.
|
||||||
* @param cap A value that should not be passed via passive generation. If null, no cap is applied.
|
* @param cap A value that should not be passed via passive generation.
|
||||||
*/
|
*/
|
||||||
export function setupPassiveGeneration(
|
export function setupPassiveGeneration(
|
||||||
layer: BaseLayer,
|
layer: BaseLayer,
|
||||||
conversion: GenericConversion,
|
conversion: GenericConversion,
|
||||||
rate: Computable<DecimalSource> = 1,
|
rate: Computable<DecimalSource> = 1,
|
||||||
cap: Computable<DecimalSource | null> = null
|
cap: Computable<DecimalSource> = Decimal.dInf
|
||||||
): void {
|
): void {
|
||||||
const processedRate = convertComputable(rate);
|
const processedRate = convertComputable(rate);
|
||||||
const processedCap = convertComputable(cap);
|
const processedCap = convertComputable(cap);
|
||||||
|
@ -280,8 +288,25 @@ export function setupPassiveGeneration(
|
||||||
conversion.gainResource.value,
|
conversion.gainResource.value,
|
||||||
Decimal.times(currRate, diff).times(Decimal.ceil(unref(conversion.actualGain)))
|
Decimal.times(currRate, diff).times(Decimal.ceil(unref(conversion.actualGain)))
|
||||||
)
|
)
|
||||||
.min(unref(processedCap) ?? Decimal.dInf)
|
.min(unref(processedCap))
|
||||||
.max(conversion.gainResource.value);
|
.max(conversion.gainResource.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
117
src/features/decorators/bonusDecorator.ts
Normal file
117
src/features/decorators/bonusDecorator.ts
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import { Replace } from "features/feature";
|
||||||
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
ProcessedComputable,
|
||||||
|
processComputable
|
||||||
|
} from "util/computed";
|
||||||
|
import { Ref, computed, unref } from "vue";
|
||||||
|
import { Decorator } from "./common";
|
||||||
|
|
||||||
|
export interface BonusAmountFeatureOptions {
|
||||||
|
bonusAmount: Computable<DecimalSource>;
|
||||||
|
totalAmount?: Computable<DecimalSource>;
|
||||||
|
}
|
||||||
|
export interface BonusCompletionsFeatureOptions {
|
||||||
|
bonusCompletions: Computable<DecimalSource>;
|
||||||
|
totalCompletions?: Computable<DecimalSource>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseBonusAmountFeature {
|
||||||
|
amount: Ref<DecimalSource>;
|
||||||
|
bonusAmount: ProcessedComputable<DecimalSource>;
|
||||||
|
totalAmount?: Ref<DecimalSource>;
|
||||||
|
}
|
||||||
|
export interface BaseBonusCompletionsFeature {
|
||||||
|
completions: Ref<DecimalSource>;
|
||||||
|
bonusCompletions: ProcessedComputable<DecimalSource>;
|
||||||
|
totalCompletions?: Ref<DecimalSource>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BonusAmountFeature<T extends BonusAmountFeatureOptions> = Replace<
|
||||||
|
T,
|
||||||
|
{ bonusAmount: GetComputableType<T["bonusAmount"]> }
|
||||||
|
>;
|
||||||
|
export type BonusCompletionsFeature<T extends BonusCompletionsFeatureOptions> = Replace<
|
||||||
|
T,
|
||||||
|
{ bonusAmount: GetComputableType<T["bonusCompletions"]> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericBonusAmountFeature = Replace<
|
||||||
|
BonusAmountFeature<BonusAmountFeatureOptions>,
|
||||||
|
{
|
||||||
|
bonusAmount: ProcessedComputable<DecimalSource>;
|
||||||
|
totalAmount: ProcessedComputable<DecimalSource>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export type GenericBonusCompletionsFeature = Replace<
|
||||||
|
BonusCompletionsFeature<BonusCompletionsFeatureOptions>,
|
||||||
|
{
|
||||||
|
bonusCompletions: ProcessedComputable<DecimalSource>;
|
||||||
|
totalCompletions: ProcessedComputable<DecimalSource>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount".
|
||||||
|
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusAmountFeatureOptions}.
|
||||||
|
* Additionally, the base feature must have an `amount` property.
|
||||||
|
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusAmountFeature}.
|
||||||
|
* @example ```ts
|
||||||
|
* createRepeatable<RepeatableOptions & BonusAmountFeatureOptions>(() => ({
|
||||||
|
* bonusAmount: noPersist(otherRepeatable.amount),
|
||||||
|
* ...
|
||||||
|
* }), bonusAmountDecorator) as GenericRepeatable & GenericBonusAmountFeature
|
||||||
|
*/
|
||||||
|
export const bonusAmountDecorator: Decorator<
|
||||||
|
BonusAmountFeatureOptions,
|
||||||
|
BaseBonusAmountFeature,
|
||||||
|
GenericBonusAmountFeature
|
||||||
|
> = {
|
||||||
|
postConstruct(feature) {
|
||||||
|
if (feature.amount === undefined) {
|
||||||
|
console.error(
|
||||||
|
`Decorated feature ${feature.id} does not contain the required 'amount' property"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
processComputable(feature, "bonusAmount");
|
||||||
|
if (feature.totalAmount === undefined) {
|
||||||
|
feature.totalAmount = computed(() =>
|
||||||
|
Decimal.add(
|
||||||
|
unref(feature.amount ?? 0),
|
||||||
|
unref(feature.bonusAmount as ProcessedComputable<DecimalSource>)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the addition of "bonus levels" to the decorated feature, with an accompanying "total amount".
|
||||||
|
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode BonusCompletionFeatureOptions}.
|
||||||
|
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericBonusCompletionFeature}.
|
||||||
|
* @example ```ts
|
||||||
|
* createChallenge<ChallengeOptions & BonusCompletionFeatureOptions>(() => ({
|
||||||
|
* bonusCompletions: noPersist(otherChallenge.completions),
|
||||||
|
* ...
|
||||||
|
* }), bonusCompletionDecorator) as GenericChallenge & GenericBonusCompletionFeature
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const bonusCompletionsDecorator: Decorator<
|
||||||
|
BonusCompletionsFeatureOptions,
|
||||||
|
BaseBonusCompletionsFeature,
|
||||||
|
GenericBonusCompletionsFeature
|
||||||
|
> = {
|
||||||
|
postConstruct(feature) {
|
||||||
|
processComputable(feature, "bonusCompletions");
|
||||||
|
if (feature.totalCompletions === undefined) {
|
||||||
|
feature.totalCompletions = computed(() =>
|
||||||
|
Decimal.add(
|
||||||
|
unref(feature.completions ?? 0),
|
||||||
|
unref(feature.bonusCompletions as ProcessedComputable<DecimalSource>)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
59
src/features/decorators/common.ts
Normal file
59
src/features/decorators/common.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { Replace, OptionsObject } from "../feature";
|
||||||
|
import {
|
||||||
|
Computable,
|
||||||
|
GetComputableType,
|
||||||
|
processComputable,
|
||||||
|
ProcessedComputable
|
||||||
|
} from "util/computed";
|
||||||
|
import { Persistent, State } from "game/persistence";
|
||||||
|
|
||||||
|
export type Decorator<
|
||||||
|
FeatureOptions,
|
||||||
|
BaseFeature = object,
|
||||||
|
GenericFeature = BaseFeature,
|
||||||
|
S extends State = State
|
||||||
|
> = {
|
||||||
|
getPersistentData?(): Record<string, Persistent<S>>;
|
||||||
|
preConstruct?(
|
||||||
|
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||||
|
): void;
|
||||||
|
postConstruct?(
|
||||||
|
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||||
|
): void;
|
||||||
|
getGatheredProps?(
|
||||||
|
feature: OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>
|
||||||
|
): Partial<OptionsObject<FeatureOptions, BaseFeature & { id: string }, GenericFeature>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenericDecorator = Decorator<unknown>;
|
||||||
|
|
||||||
|
export interface EffectFeatureOptions<T = unknown> {
|
||||||
|
effect: Computable<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EffectFeature<T extends EffectFeatureOptions> = Replace<
|
||||||
|
T,
|
||||||
|
{ effect: GetComputableType<T["effect"]> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GenericEffectFeature<T = unknown> = Replace<
|
||||||
|
EffectFeature<EffectFeatureOptions>,
|
||||||
|
{ effect: ProcessedComputable<T> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the usage of an `effect` field in the decorated feature.
|
||||||
|
* To function properly, the `createFeature()` function must have its generic type extended by {@linkcode EffectFeatureOptions}.
|
||||||
|
* To allow access to the decorated values outside the `createFeature()` function, the output type must be extended by {@linkcode GenericEffectFeature}.
|
||||||
|
* @example ```ts
|
||||||
|
* createRepeatable<RepeatableOptions & EffectFeatureOptions>(() => ({
|
||||||
|
* effect() { return Decimal.pow(2, this.amount); },
|
||||||
|
* ...
|
||||||
|
* }), effectDecorator) as GenericUpgrade & GenericEffectFeature;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const effectDecorator: Decorator<EffectFeatureOptions, unknown, GenericEffectFeature> = {
|
||||||
|
postConstruct(feature) {
|
||||||
|
processComputable(feature, "effect");
|
||||||
|
}
|
||||||
|
};
|
|
@ -42,9 +42,9 @@ export type Replace<T, S> = S & Omit<T, keyof S>;
|
||||||
* with "this" bound to what the type will eventually be processed into.
|
* with "this" bound to what the type will eventually be processed into.
|
||||||
* Intended for making lazily evaluated objects.
|
* Intended for making lazily evaluated objects.
|
||||||
*/
|
*/
|
||||||
export type OptionsFunc<T, R = Record<string, unknown>, S = R> = () => T &
|
export type OptionsFunc<T, R = unknown, S = R> = (obj: R) => OptionsObject<T, R, S>;
|
||||||
Partial<R> &
|
|
||||||
ThisType<T & S>;
|
export type OptionsObject<T, R = unknown, S = R> = T & Partial<R> & ThisType<T & S>;
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,14 +21,22 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Grid} features. */
|
||||||
export const GridType = Symbol("Grid");
|
export const GridType = Symbol("Grid");
|
||||||
|
|
||||||
|
/** A type representing a computable value for a cell in the grid. */
|
||||||
export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T);
|
export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T);
|
||||||
|
|
||||||
|
/** Create proxy to more easily get the properties of cells on a grid. */
|
||||||
function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> {
|
function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> {
|
||||||
return new Proxy({}, getGridHandler(grid)) as Record<string | number, GridCell>;
|
return new Proxy({}, getGridHandler(grid)) as Record<string | number, GridCell>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns traps for a proxy that will give cell proxies when accessing any numerical key.
|
||||||
|
* @param grid The grid to get the cells from.
|
||||||
|
* @see {@link createGridProxy}
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number, GridCell>> {
|
function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number, GridCell>> {
|
||||||
const keys = computed(() => {
|
const keys = computed(() => {
|
||||||
|
@ -86,6 +94,12 @@ function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns traps for a proxy that will get the properties for the specified cell
|
||||||
|
* @param id The grid cell ID to get properties from.
|
||||||
|
* @see {@link getGridHandler}
|
||||||
|
* @see {@link createGridProxy}
|
||||||
|
*/
|
||||||
function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
||||||
const keys = [
|
const keys = [
|
||||||
"id",
|
"id",
|
||||||
|
@ -175,47 +189,90 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a cell within a grid. These properties will typically be accessed via a cell proxy that calls functions on the grid to get the properties for a specific cell.
|
||||||
|
* @see {@link createGridProxy}
|
||||||
|
*/
|
||||||
export interface GridCell {
|
export interface GridCell {
|
||||||
|
/** A unique identifier for the grid cell. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Whether this cell should be visible. */
|
||||||
visibility: Visibility | boolean;
|
visibility: Visibility | boolean;
|
||||||
|
/** Whether this cell can be clicked. */
|
||||||
canClick: boolean;
|
canClick: boolean;
|
||||||
|
/** The initial persistent state of this cell. */
|
||||||
startState: State;
|
startState: State;
|
||||||
|
/** The persistent state of this cell. */
|
||||||
state: State;
|
state: State;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: StyleValue;
|
style?: StyleValue;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Record<string, boolean>;
|
classes?: Record<string, boolean>;
|
||||||
|
/** A header to appear at the top of the display. */
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
|
/** The main text that appears in the display. */
|
||||||
display: CoercableComponent;
|
display: CoercableComponent;
|
||||||
|
/** A function that is called when the cell is clicked. */
|
||||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
|
/** A function that is called when the cell is held down. */
|
||||||
onHold?: VoidFunction;
|
onHold?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Grid}.
|
||||||
|
*/
|
||||||
export interface GridOptions {
|
export interface GridOptions {
|
||||||
|
/** Whether this grid should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The number of rows in the grid. */
|
||||||
rows: Computable<number>;
|
rows: Computable<number>;
|
||||||
|
/** The number of columns in the grid. */
|
||||||
cols: Computable<number>;
|
cols: Computable<number>;
|
||||||
|
/** A computable to determine the visibility of a cell. */
|
||||||
getVisibility?: CellComputable<Visibility | boolean>;
|
getVisibility?: CellComputable<Visibility | boolean>;
|
||||||
|
/** A computable to determine if a cell can be clicked. */
|
||||||
getCanClick?: CellComputable<boolean>;
|
getCanClick?: CellComputable<boolean>;
|
||||||
|
/** A computable to get the initial persistent state of a cell. */
|
||||||
getStartState: Computable<State> | ((id: string | number) => State);
|
getStartState: Computable<State> | ((id: string | number) => State);
|
||||||
|
/** A computable to get the CSS styles for a cell. */
|
||||||
getStyle?: CellComputable<StyleValue>;
|
getStyle?: CellComputable<StyleValue>;
|
||||||
|
/** A computable to get the CSS classes for a cell. */
|
||||||
getClasses?: CellComputable<Record<string, boolean>>;
|
getClasses?: CellComputable<Record<string, boolean>>;
|
||||||
|
/** A computable to get the title component for a cell. */
|
||||||
getTitle?: CellComputable<CoercableComponent>;
|
getTitle?: CellComputable<CoercableComponent>;
|
||||||
|
/** A computable to get the display component for a cell. */
|
||||||
getDisplay: CellComputable<CoercableComponent>;
|
getDisplay: CellComputable<CoercableComponent>;
|
||||||
|
/** A function that is called when a cell is clicked. */
|
||||||
onClick?: (id: string | number, state: State, e?: MouseEvent | TouchEvent) => void;
|
onClick?: (id: string | number, state: State, e?: MouseEvent | TouchEvent) => void;
|
||||||
|
/** A function that is called when a cell is held down. */
|
||||||
onHold?: (id: string | number, state: State) => void;
|
onHold?: (id: string | number, state: State) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
|
||||||
|
*/
|
||||||
export interface BaseGrid {
|
export interface BaseGrid {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
|
||||||
getID: (id: string | number, state: State) => string;
|
getID: (id: string | number, state: State) => string;
|
||||||
|
/** Get the persistent state of the given cell. */
|
||||||
getState: (id: string | number) => State;
|
getState: (id: string | number) => State;
|
||||||
|
/** Set the persistent state of the given cell. */
|
||||||
setState: (id: string | number, state: State) => void;
|
setState: (id: string | number, state: State) => void;
|
||||||
|
/** A dictionary of cells within this grid. */
|
||||||
cells: Record<string | number, GridCell>;
|
cells: Record<string | number, GridCell>;
|
||||||
|
/** The persistent state of this grid, which is a dictionary of cell states. */
|
||||||
cellState: Persistent<Record<string | number, State>>;
|
cellState: Persistent<Record<string | number, State>>;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof GridType;
|
type: typeof GridType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
|
||||||
export type Grid<T extends GridOptions> = Replace<
|
export type Grid<T extends GridOptions> = Replace<
|
||||||
T & BaseGrid,
|
T & BaseGrid,
|
||||||
{
|
{
|
||||||
|
@ -232,6 +289,7 @@ export type Grid<T extends GridOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Grid} object. */
|
||||||
export type GenericGrid = Replace<
|
export type GenericGrid = Replace<
|
||||||
Grid<GridOptions>,
|
Grid<GridOptions>,
|
||||||
{
|
{
|
||||||
|
@ -241,12 +299,16 @@ export type GenericGrid = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a grid with the given options.
|
||||||
|
* @param optionsFunc Grid options.
|
||||||
|
*/
|
||||||
export function createGrid<T extends GridOptions>(
|
export function createGrid<T extends GridOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseGrid, GenericGrid>
|
optionsFunc: OptionsFunc<T, BaseGrid, GenericGrid>
|
||||||
): Grid<T> {
|
): Grid<T> {
|
||||||
const cellState = persistent<Record<string | number, State>>({}, false);
|
const cellState = persistent<Record<string | number, State>>({}, false);
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const grid = optionsFunc();
|
const grid = optionsFunc.call(feature, feature);
|
||||||
grid.id = getUniqueID("grid-");
|
grid.id = getUniqueID("grid-");
|
||||||
grid[Component] = GridComponent as GenericComponent;
|
grid[Component] = GridComponent as GenericComponent;
|
||||||
|
|
||||||
|
|
|
@ -15,20 +15,34 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import { shallowReactive, unref } from "vue";
|
import { shallowReactive, unref } from "vue";
|
||||||
import Hotkey from "components/Hotkey.vue";
|
import Hotkey from "components/Hotkey.vue";
|
||||||
|
|
||||||
|
/** A dictionary of all hotkeys. */
|
||||||
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
|
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
|
||||||
|
/** A symbol used to identify {@link Hotkey} features. */
|
||||||
export const HotkeyType = Symbol("Hotkey");
|
export const HotkeyType = Symbol("Hotkey");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Hotkey}.
|
||||||
|
*/
|
||||||
export interface HotkeyOptions {
|
export interface HotkeyOptions {
|
||||||
|
/** Whether or not this hotkey is currently enabled. */
|
||||||
enabled?: Computable<boolean>;
|
enabled?: Computable<boolean>;
|
||||||
|
/** The key tied to this hotkey */
|
||||||
key: string;
|
key: string;
|
||||||
|
/** The description of this hotkey, to display in the settings. */
|
||||||
description: Computable<string>;
|
description: Computable<string>;
|
||||||
|
/** What to do upon pressing the key. */
|
||||||
onPress: VoidFunction;
|
onPress: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link HotkeyOptions} to create an {@link Hotkey}.
|
||||||
|
*/
|
||||||
export interface BaseHotkey {
|
export interface BaseHotkey {
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof HotkeyType;
|
type: typeof HotkeyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a hotkey shortcut that performs an action upon a key sequence being pressed. */
|
||||||
export type Hotkey<T extends HotkeyOptions> = Replace<
|
export type Hotkey<T extends HotkeyOptions> = Replace<
|
||||||
T & BaseHotkey,
|
T & BaseHotkey,
|
||||||
{
|
{
|
||||||
|
@ -37,6 +51,7 @@ export type Hotkey<T extends HotkeyOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Hotkey} object. */
|
||||||
export type GenericHotkey = Replace<
|
export type GenericHotkey = Replace<
|
||||||
Hotkey<HotkeyOptions>,
|
Hotkey<HotkeyOptions>,
|
||||||
{
|
{
|
||||||
|
@ -46,11 +61,15 @@ export type GenericHotkey = Replace<
|
||||||
|
|
||||||
const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
|
const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a hotkey with the given options.
|
||||||
|
* @param optionsFunc Hotkey options.
|
||||||
|
*/
|
||||||
export function createHotkey<T extends HotkeyOptions>(
|
export function createHotkey<T extends HotkeyOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseHotkey, GenericHotkey>
|
optionsFunc: OptionsFunc<T, BaseHotkey, GenericHotkey>
|
||||||
): Hotkey<T> {
|
): Hotkey<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const hotkey = optionsFunc();
|
const hotkey = optionsFunc.call(feature, feature);
|
||||||
hotkey.type = HotkeyType;
|
hotkey.type = HotkeyType;
|
||||||
|
|
||||||
processComputable(hotkey as T, "enabled");
|
processComputable(hotkey as T, "enabled");
|
||||||
|
|
|
@ -19,27 +19,48 @@ import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { unref } from "vue";
|
import { unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Infobox} features. */
|
||||||
export const InfoboxType = Symbol("Infobox");
|
export const InfoboxType = Symbol("Infobox");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures an {@link Infobox}.
|
||||||
|
*/
|
||||||
export interface InfoboxOptions {
|
export interface InfoboxOptions {
|
||||||
|
/** Whether this clickable should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The background color of the Infobox. */
|
||||||
color?: Computable<string>;
|
color?: Computable<string>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** CSS to apply to the title of the infobox. */
|
||||||
titleStyle?: Computable<StyleValue>;
|
titleStyle?: Computable<StyleValue>;
|
||||||
|
/** CSS to apply to the body of the infobox. */
|
||||||
bodyStyle?: Computable<StyleValue>;
|
bodyStyle?: Computable<StyleValue>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** A header to appear at the top of the display. */
|
||||||
title: Computable<CoercableComponent>;
|
title: Computable<CoercableComponent>;
|
||||||
|
/** The main text that appears in the display. */
|
||||||
display: Computable<CoercableComponent>;
|
display: Computable<CoercableComponent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link InfoboxOptions} to create an {@link Infobox}.
|
||||||
|
*/
|
||||||
export interface BaseInfobox {
|
export interface BaseInfobox {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Whether or not this infobox is collapsed. */
|
||||||
collapsed: Persistent<boolean>;
|
collapsed: Persistent<boolean>;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof InfoboxType;
|
type: typeof InfoboxType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that displays information in a collapsible way. */
|
||||||
export type Infobox<T extends InfoboxOptions> = Replace<
|
export type Infobox<T extends InfoboxOptions> = Replace<
|
||||||
T & BaseInfobox,
|
T & BaseInfobox,
|
||||||
{
|
{
|
||||||
|
@ -54,6 +75,7 @@ export type Infobox<T extends InfoboxOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Infobox} object. */
|
||||||
export type GenericInfobox = Replace<
|
export type GenericInfobox = Replace<
|
||||||
Infobox<InfoboxOptions>,
|
Infobox<InfoboxOptions>,
|
||||||
{
|
{
|
||||||
|
@ -61,12 +83,16 @@ export type GenericInfobox = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates an infobox with the given options.
|
||||||
|
* @param optionsFunc Infobox options.
|
||||||
|
*/
|
||||||
export function createInfobox<T extends InfoboxOptions>(
|
export function createInfobox<T extends InfoboxOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseInfobox, GenericInfobox>
|
optionsFunc: OptionsFunc<T, BaseInfobox, GenericInfobox>
|
||||||
): Infobox<T> {
|
): Infobox<T> {
|
||||||
const collapsed = persistent<boolean>(false, false);
|
const collapsed = persistent<boolean>(false, false);
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const infobox = optionsFunc();
|
const infobox = optionsFunc.call(feature, feature);
|
||||||
infobox.id = getUniqueID("infobox-");
|
infobox.id = getUniqueID("infobox-");
|
||||||
infobox.type = InfoboxType;
|
infobox.type = InfoboxType;
|
||||||
infobox[Component] = InfoboxComponent as GenericComponent;
|
infobox[Component] = InfoboxComponent as GenericComponent;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<line
|
<line
|
||||||
stroke-width="15px"
|
stroke-width="15px"
|
||||||
stroke="white"
|
stroke="white"
|
||||||
v-bind="link"
|
v-bind="linkProps"
|
||||||
:x1="startPosition.x"
|
:x1="startPosition.x"
|
||||||
:y1="startPosition.y"
|
:y1="startPosition.y"
|
||||||
:x2="endPosition.x"
|
:x2="endPosition.x"
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Link } from "features/links/links";
|
import type { Link } from "features/links/links";
|
||||||
import type { FeatureNode } from "game/layers";
|
import type { FeatureNode } from "game/layers";
|
||||||
|
import { kebabifyObject } from "util/vue";
|
||||||
import { computed, toRefs } from "vue";
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
|
@ -54,4 +55,6 @@ const endPosition = computed(() => {
|
||||||
}
|
}
|
||||||
return position;
|
return position;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import type { SVGAttributes } from "vue";
|
import type { SVGAttributes } from "vue";
|
||||||
import LinksComponent from "./Links.vue";
|
import LinksComponent from "./Links.vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Links} features. */
|
||||||
export const LinksType = Symbol("Links");
|
export const LinksType = Symbol("Links");
|
||||||
|
|
||||||
|
/** Represents a link between two nodes. It will be displayed as an SVG line, and can take any appropriate properties for an SVG line element. */
|
||||||
export interface Link extends SVGAttributes {
|
export interface Link extends SVGAttributes {
|
||||||
startNode: { id: string };
|
startNode: { id: string };
|
||||||
endNode: { id: string };
|
endNode: { id: string };
|
||||||
|
@ -16,16 +18,25 @@ export interface Link extends SVGAttributes {
|
||||||
offsetEnd?: Position;
|
offsetEnd?: Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that configures a {@link Links}. */
|
||||||
export interface LinksOptions {
|
export interface LinksOptions {
|
||||||
|
/** The list of links to display. */
|
||||||
links: Computable<Link[]>;
|
links: Computable<Link[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link LinksOptions} to create an {@link Links}.
|
||||||
|
*/
|
||||||
export interface BaseLinks {
|
export interface BaseLinks {
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof LinksType;
|
type: typeof LinksType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
|
||||||
export type Links<T extends LinksOptions> = Replace<
|
export type Links<T extends LinksOptions> = Replace<
|
||||||
T & BaseLinks,
|
T & BaseLinks,
|
||||||
{
|
{
|
||||||
|
@ -33,6 +44,7 @@ export type Links<T extends LinksOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Links} object. */
|
||||||
export type GenericLinks = Replace<
|
export type GenericLinks = Replace<
|
||||||
Links<LinksOptions>,
|
Links<LinksOptions>,
|
||||||
{
|
{
|
||||||
|
@ -40,11 +52,15 @@ export type GenericLinks = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates links with the given options.
|
||||||
|
* @param optionsFunc Links options.
|
||||||
|
*/
|
||||||
export function createLinks<T extends LinksOptions>(
|
export function createLinks<T extends LinksOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseLinks, GenericLinks>
|
optionsFunc: OptionsFunc<T, BaseLinks, GenericLinks>
|
||||||
): Links<T> {
|
): Links<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const links = optionsFunc();
|
const links = optionsFunc.call(feature, feature);
|
||||||
links.type = LinksType;
|
links.type = LinksType;
|
||||||
links[Component] = LinksComponent as GenericComponent;
|
links[Component] = LinksComponent as GenericComponent;
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { globalBus } from "game/events";
|
||||||
import "lib/pixi";
|
import "lib/pixi";
|
||||||
import { processedPropType } from "util/vue";
|
import { processedPropType } from "util/vue";
|
||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref } from "vue";
|
import { defineComponent, nextTick, onBeforeUnmount, onMounted, shallowRef, unref } from "vue";
|
||||||
|
|
||||||
// TODO get typing support on the Particles component
|
// TODO get typing support on the Particles component
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
|
@ -8,24 +8,49 @@ import type { Computable, GetComputableType } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { Ref, shallowRef, unref } from "vue";
|
import { Ref, shallowRef, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Particles} features. */
|
||||||
export const ParticlesType = Symbol("Particles");
|
export const ParticlesType = Symbol("Particles");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures {@link Particles}.
|
||||||
|
*/
|
||||||
export interface ParticlesOptions {
|
export interface ParticlesOptions {
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** A function that is called when the particles canvas is resized. */
|
||||||
onContainerResized?: (boundingRect: DOMRect) => void;
|
onContainerResized?: (boundingRect: DOMRect) => void;
|
||||||
|
/** A function that is called whenever the particles element is reloaded during development. For restarting particle effects. */
|
||||||
onHotReload?: VoidFunction;
|
onHotReload?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ParticlesOptions} to create an {@link Particles}.
|
||||||
|
*/
|
||||||
export interface BaseParticles {
|
export interface BaseParticles {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** The Pixi.JS Application powering this particles canvas. */
|
||||||
app: Ref<null | Application>;
|
app: Ref<null | Application>;
|
||||||
|
/**
|
||||||
|
* A function to asynchronously add an emitter to the canvas.
|
||||||
|
* The returned emitter can then be positioned as appropriate and started.
|
||||||
|
* @see {@link Particles}
|
||||||
|
*/
|
||||||
addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
|
addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof ParticlesType;
|
type: typeof ParticlesType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that represents a feature that display particle effects on the screen.
|
||||||
|
* The config should typically be gotten by designing the effect using the [online particle effect editor](https://pixijs.io/pixi-particles-editor/) and passing it into the {@link upgradeConfig} from @pixi/particle-emitter.
|
||||||
|
*/
|
||||||
export type Particles<T extends ParticlesOptions> = Replace<
|
export type Particles<T extends ParticlesOptions> = Replace<
|
||||||
T & BaseParticles,
|
T & BaseParticles,
|
||||||
{
|
{
|
||||||
|
@ -34,16 +59,23 @@ export type Particles<T extends ParticlesOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Particles} object. */
|
||||||
export type GenericParticles = Particles<ParticlesOptions>;
|
export type GenericParticles = Particles<ParticlesOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates particles with the given options.
|
||||||
|
* @param optionsFunc Particles options.
|
||||||
|
*/
|
||||||
export function createParticles<T extends ParticlesOptions>(
|
export function createParticles<T extends ParticlesOptions>(
|
||||||
optionsFunc?: OptionsFunc<T, BaseParticles, GenericParticles>
|
optionsFunc?: OptionsFunc<T, BaseParticles, GenericParticles>
|
||||||
): Particles<T> {
|
): Particles<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const particles = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
const particles =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
particles.id = getUniqueID("particles-");
|
particles.id = getUniqueID("particles-");
|
||||||
particles.type = ParticlesType;
|
particles.type = ParticlesType;
|
||||||
particles[Component] = ParticlesComponent;
|
particles[Component] = ParticlesComponent as GenericComponent;
|
||||||
|
|
||||||
particles.app = shallowRef(null);
|
particles.app = shallowRef(null);
|
||||||
particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
|
particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
|
||||||
|
|
|
@ -7,14 +7,14 @@ import type {
|
||||||
Replace,
|
Replace,
|
||||||
StyleValue
|
StyleValue
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
|
import { Component, GatherProps, Visibility, getUniqueID, jsx, setDefault } from "features/feature";
|
||||||
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
import { DefaultValue, Persistent, persistent } from "game/persistence";
|
||||||
import {
|
import {
|
||||||
|
Requirements,
|
||||||
createVisibilityRequirement,
|
createVisibilityRequirement,
|
||||||
displayRequirements,
|
displayRequirements,
|
||||||
maxRequirementsMet,
|
maxRequirementsMet,
|
||||||
payRequirements,
|
payRequirements,
|
||||||
Requirements,
|
|
||||||
requirementsMet
|
requirementsMet
|
||||||
} from "game/requirements";
|
} from "game/requirements";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
|
@ -30,6 +30,7 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import { coerceComponent, isCoercableComponent } from "util/vue";
|
import { coerceComponent, isCoercableComponent } from "util/vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
import { GenericDecorator } from "./decorators/common";
|
||||||
|
|
||||||
/** A symbol used to identify {@link Repeatable} features. */
|
/** A symbol used to identify {@link Repeatable} features. */
|
||||||
export const RepeatableType = Symbol("Repeatable");
|
export const RepeatableType = Symbol("Repeatable");
|
||||||
|
@ -42,7 +43,7 @@ export type RepeatableDisplay =
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
/** The main text that appears in the display. */
|
/** The main text that appears in the display. */
|
||||||
description?: CoercableComponent;
|
description?: CoercableComponent;
|
||||||
/** A description of the current effect of this repeatable, bsed off its amount. */
|
/** A description of the current effect of this repeatable, based off its amount. */
|
||||||
effectDisplay?: CoercableComponent;
|
effectDisplay?: CoercableComponent;
|
||||||
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
|
/** Whether or not to show the current amount of this repeatable at the bottom of the display. */
|
||||||
showAmount?: boolean;
|
showAmount?: boolean;
|
||||||
|
@ -66,8 +67,6 @@ export interface RepeatableOptions {
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
/** Toggles a smaller design for the feature. */
|
/** Toggles a smaller design for the feature. */
|
||||||
small?: Computable<boolean>;
|
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. */
|
/** The display to use for this repeatable. */
|
||||||
display?: Computable<RepeatableDisplay>;
|
display?: Computable<RepeatableDisplay>;
|
||||||
}
|
}
|
||||||
|
@ -76,7 +75,7 @@ export interface RepeatableOptions {
|
||||||
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
|
* The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
|
||||||
*/
|
*/
|
||||||
export interface BaseRepeatable {
|
export interface BaseRepeatable {
|
||||||
/** An auto-generated ID for identifying features that appear in the DOM. Will not persistent between refreshes or updates. */
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
/** The current amount this repeatable has. */
|
/** The current amount this repeatable has. */
|
||||||
amount: Persistent<DecimalSource>;
|
amount: Persistent<DecimalSource>;
|
||||||
|
@ -86,7 +85,6 @@ export interface BaseRepeatable {
|
||||||
canClick: ProcessedComputable<boolean>;
|
canClick: ProcessedComputable<boolean>;
|
||||||
/**
|
/**
|
||||||
* How much amount can be increased by, or 1 if unclickable.
|
* How much amount can be increased by, or 1 if unclickable.
|
||||||
* Capped at 1 if {@link RepeatableOptions.maximize} is false.
|
|
||||||
**/
|
**/
|
||||||
amountToIncrease: Ref<DecimalSource>;
|
amountToIncrease: Ref<DecimalSource>;
|
||||||
/** A function that gets called when this repeatable is clicked. */
|
/** A function that gets called when this repeatable is clicked. */
|
||||||
|
@ -110,7 +108,6 @@ export type Repeatable<T extends RepeatableOptions> = Replace<
|
||||||
style: GetComputableType<T["style"]>;
|
style: GetComputableType<T["style"]>;
|
||||||
mark: GetComputableType<T["mark"]>;
|
mark: GetComputableType<T["mark"]>;
|
||||||
small: GetComputableType<T["small"]>;
|
small: GetComputableType<T["small"]>;
|
||||||
maximize: GetComputableType<T["maximize"]>;
|
|
||||||
display: Ref<CoercableComponent>;
|
display: Ref<CoercableComponent>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
@ -129,19 +126,30 @@ export type GenericRepeatable = Replace<
|
||||||
* @param optionsFunc Repeatable options.
|
* @param optionsFunc Repeatable options.
|
||||||
*/
|
*/
|
||||||
export function createRepeatable<T extends RepeatableOptions>(
|
export function createRepeatable<T extends RepeatableOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>
|
optionsFunc: OptionsFunc<T, BaseRepeatable, GenericRepeatable>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Repeatable<T> {
|
): Repeatable<T> {
|
||||||
const amount = persistent<DecimalSource>(0);
|
const amount = persistent<DecimalSource>(0);
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const repeatable = optionsFunc();
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy<Repeatable<T>, Repeatable<T>>(feature => {
|
||||||
|
const repeatable = optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
repeatable.id = getUniqueID("repeatable-");
|
repeatable.id = getUniqueID("repeatable-");
|
||||||
repeatable.type = RepeatableType;
|
repeatable.type = RepeatableType;
|
||||||
repeatable[Component] = ClickableComponent as GenericComponent;
|
repeatable[Component] = ClickableComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(repeatable);
|
||||||
|
}
|
||||||
|
|
||||||
repeatable.amount = amount;
|
repeatable.amount = amount;
|
||||||
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
|
||||||
|
|
||||||
|
Object.assign(repeatable, decoratedData);
|
||||||
|
|
||||||
const limitRequirement = {
|
const limitRequirement = {
|
||||||
requirementMet: computed(() =>
|
requirementMet: computed(() =>
|
||||||
Decimal.sub(
|
Decimal.sub(
|
||||||
|
@ -150,7 +158,8 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
requiresPay: false,
|
requiresPay: false,
|
||||||
visibility: Visibility.None
|
visibility: Visibility.None,
|
||||||
|
canMaximize: true
|
||||||
} as const;
|
} as const;
|
||||||
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
|
||||||
if (isArray(repeatable.requirements)) {
|
if (isArray(repeatable.requirements)) {
|
||||||
|
@ -182,9 +191,7 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
return currClasses;
|
return currClasses;
|
||||||
});
|
});
|
||||||
repeatable.amountToIncrease = computed(() =>
|
repeatable.amountToIncrease = computed(() =>
|
||||||
unref((repeatable as GenericRepeatable).maximize)
|
Decimal.clampMin(maxRequirementsMet(repeatable.requirements), 1)
|
||||||
? maxRequirementsMet(repeatable.requirements)
|
|
||||||
: 1
|
|
||||||
);
|
);
|
||||||
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
|
||||||
const onClick = repeatable.onClick;
|
const onClick = repeatable.onClick;
|
||||||
|
@ -193,8 +200,12 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
if (!unref(genericRepeatable.canClick)) {
|
if (!unref(genericRepeatable.canClick)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease));
|
const amountToIncrease = unref(repeatable.amountToIncrease) ?? 1;
|
||||||
genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
|
payRequirements(repeatable.requirements, amountToIncrease);
|
||||||
|
genericRepeatable.amount.value = Decimal.add(
|
||||||
|
genericRepeatable.amount.value,
|
||||||
|
amountToIncrease
|
||||||
|
);
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
};
|
};
|
||||||
processComputable(repeatable as T, "display");
|
processComputable(repeatable as T, "display");
|
||||||
|
@ -223,14 +234,10 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
{currDisplay.showAmount === false ? null : (
|
{currDisplay.showAmount === false ? null : (
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
{unref(genericRepeatable.limit) === Decimal.dInf ? (
|
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
||||||
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
|
{Decimal.isFinite(unref(genericRepeatable.limit)) ? (
|
||||||
) : (
|
<> / {formatWhole(unref(genericRepeatable.limit))}</>
|
||||||
<>
|
) : undefined}
|
||||||
Amount: {formatWhole(genericRepeatable.amount.value)} /{" "}
|
|
||||||
{formatWhole(unref(genericRepeatable.limit))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currDisplay.effectDisplay == null ? null : (
|
{currDisplay.effectDisplay == null ? null : (
|
||||||
|
@ -261,8 +268,15 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
processComputable(repeatable as T, "style");
|
processComputable(repeatable as T, "style");
|
||||||
processComputable(repeatable as T, "mark");
|
processComputable(repeatable as T, "mark");
|
||||||
processComputable(repeatable as T, "small");
|
processComputable(repeatable as T, "small");
|
||||||
processComputable(repeatable as T, "maximize");
|
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(repeatable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(repeatable)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
repeatable[GatherProps] = function (this: GenericRepeatable) {
|
||||||
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
const { display, visibility, style, classes, onClick, canClick, small, mark, id } =
|
||||||
this;
|
this;
|
||||||
|
@ -275,7 +289,8 @@ export function createRepeatable<T extends RepeatableOptions>(
|
||||||
canClick,
|
canClick,
|
||||||
small,
|
small,
|
||||||
mark,
|
mark,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { OptionsFunc, Replace } from "features/feature";
|
import type { OptionsFunc, Replace } from "features/feature";
|
||||||
import { getUniqueID } from "features/feature";
|
import { getUniqueID } from "features/feature";
|
||||||
import { globalBus } from "game/events";
|
import { globalBus } from "game/events";
|
||||||
|
import Formula from "game/formulas/formulas";
|
||||||
import type { BaseLayer } from "game/layers";
|
import type { BaseLayer } from "game/layers";
|
||||||
import type { NonPersistent, Persistent } from "game/persistence";
|
import { NonPersistent, Persistent, SkipPersistence } from "game/persistence";
|
||||||
import { DefaultValue, persistent } from "game/persistence";
|
import { DefaultValue, persistent } from "game/persistence";
|
||||||
import type { Unsubscribe } from "nanoevents";
|
import type { Unsubscribe } from "nanoevents";
|
||||||
import Decimal from "util/bignum";
|
import Decimal from "util/bignum";
|
||||||
|
@ -11,19 +12,32 @@ import { processComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { isRef, unref } from "vue";
|
import { isRef, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Reset} features. */
|
||||||
export const ResetType = Symbol("Reset");
|
export const ResetType = Symbol("Reset");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Clickable}.
|
||||||
|
*/
|
||||||
export interface ResetOptions {
|
export interface ResetOptions {
|
||||||
thingsToReset: Computable<Record<string, unknown>[]>;
|
/** List of things to reset. Can include objects which will be recursed over for persistent values. */
|
||||||
|
thingsToReset: Computable<unknown[]>;
|
||||||
|
/** A function that is called when the reset is performed. */
|
||||||
onReset?: VoidFunction;
|
onReset?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link ResetOptions} to create an {@link Reset}.
|
||||||
|
*/
|
||||||
export interface BaseReset {
|
export interface BaseReset {
|
||||||
|
/** An auto-generated ID for identifying which reset is being performed. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Trigger the reset. */
|
||||||
reset: VoidFunction;
|
reset: VoidFunction;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof ResetType;
|
type: typeof ResetType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a reset mechanic, which resets progress back to its initial state. */
|
||||||
export type Reset<T extends ResetOptions> = Replace<
|
export type Reset<T extends ResetOptions> = Replace<
|
||||||
T & BaseReset,
|
T & BaseReset,
|
||||||
{
|
{
|
||||||
|
@ -31,19 +45,32 @@ export type Reset<T extends ResetOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Reset} object. */
|
||||||
export type GenericReset = Reset<ResetOptions>;
|
export type GenericReset = Reset<ResetOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a reset with the given options.
|
||||||
|
* @param optionsFunc Reset options.
|
||||||
|
*/
|
||||||
export function createReset<T extends ResetOptions>(
|
export function createReset<T extends ResetOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseReset, GenericReset>
|
optionsFunc: OptionsFunc<T, BaseReset, GenericReset>
|
||||||
): Reset<T> {
|
): Reset<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const reset = optionsFunc();
|
const reset = optionsFunc.call(feature, feature);
|
||||||
reset.id = getUniqueID("reset-");
|
reset.id = getUniqueID("reset-");
|
||||||
reset.type = ResetType;
|
reset.type = ResetType;
|
||||||
|
|
||||||
reset.reset = function () {
|
reset.reset = function () {
|
||||||
const handleObject = (obj: unknown) => {
|
const handleObject = (obj: unknown) => {
|
||||||
if (obj != null && typeof obj === "object") {
|
if (
|
||||||
|
obj != null &&
|
||||||
|
typeof obj === "object" &&
|
||||||
|
!(obj instanceof Decimal) &&
|
||||||
|
!(obj instanceof Formula)
|
||||||
|
) {
|
||||||
|
if (SkipPersistence in obj && obj[SkipPersistence] === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (DefaultValue in obj) {
|
if (DefaultValue in obj) {
|
||||||
const persistent = obj as NonPersistent;
|
const persistent = obj as NonPersistent;
|
||||||
persistent.value = persistent[DefaultValue];
|
persistent.value = persistent[DefaultValue];
|
||||||
|
@ -66,6 +93,11 @@ export function createReset<T extends ResetOptions>(
|
||||||
}
|
}
|
||||||
|
|
||||||
const listeners: Record<string, Unsubscribe | undefined> = {};
|
const listeners: Record<string, Unsubscribe | undefined> = {};
|
||||||
|
/**
|
||||||
|
* Track the time since the specified reset last occured.
|
||||||
|
* @param layer The layer the reset is attached to
|
||||||
|
* @param reset The reset mechanic to track the time since
|
||||||
|
*/
|
||||||
export function trackResetTime(layer: BaseLayer, reset: GenericReset): Persistent<Decimal> {
|
export function trackResetTime(layer: BaseLayer, reset: GenericReset): Persistent<Decimal> {
|
||||||
const resetTime = persistent<Decimal>(new Decimal(0));
|
const resetTime = persistent<Decimal>(new Decimal(0));
|
||||||
globalBus.on("addLayer", layerBeingAdded => {
|
globalBus.on("addLayer", layerBeingAdded => {
|
||||||
|
|
|
@ -8,12 +8,23 @@ import { loadingSave } from "util/save";
|
||||||
import type { ComputedRef, Ref } from "vue";
|
import type { ComputedRef, Ref } from "vue";
|
||||||
import { computed, isRef, ref, unref, watch } from "vue";
|
import { computed, isRef, ref, unref, watch } from "vue";
|
||||||
|
|
||||||
|
/** An object that represents a named and quantifiable resource in the game. */
|
||||||
export interface Resource<T = DecimalSource> extends Ref<T> {
|
export interface Resource<T = DecimalSource> extends Ref<T> {
|
||||||
|
/** The name of this resource. */
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
/** When displaying the value of this resource, how many significant digits to display. */
|
||||||
precision: number;
|
precision: number;
|
||||||
|
/** Whether or not to display very small values using scientific notation, or rounding to 0. */
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a resource.
|
||||||
|
* @param defaultValue The initial value of the resource
|
||||||
|
* @param displayName The human readable name of this resource
|
||||||
|
* @param precision The number of significant digits to display by default
|
||||||
|
* @param small Whether or not to display very small values or round to 0, by default
|
||||||
|
*/
|
||||||
export function createResource<T extends State>(
|
export function createResource<T extends State>(
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
|
@ -49,6 +60,7 @@ export function createResource<T extends State>(
|
||||||
return resource as Resource<T>;
|
return resource as Resource<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a reference to the highest amount of the resource ever owned, which is updated automatically. */
|
||||||
export function trackBest(resource: Resource): Ref<DecimalSource> {
|
export function trackBest(resource: Resource): Ref<DecimalSource> {
|
||||||
const best = persistent(resource.value);
|
const best = persistent(resource.value);
|
||||||
watch(resource, amount => {
|
watch(resource, amount => {
|
||||||
|
@ -62,6 +74,7 @@ export function trackBest(resource: Resource): Ref<DecimalSource> {
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns a reference to the total amount of the resource gained, updated automatically. "Refunds" count as gain. */
|
||||||
export function trackTotal(resource: Resource): Ref<DecimalSource> {
|
export function trackTotal(resource: Resource): Ref<DecimalSource> {
|
||||||
const total = persistent(resource.value);
|
const total = persistent(resource.value);
|
||||||
watch(resource, (amount, prevAmount) => {
|
watch(resource, (amount, prevAmount) => {
|
||||||
|
@ -77,6 +90,7 @@ export function trackTotal(resource: Resource): Ref<DecimalSource> {
|
||||||
|
|
||||||
const tetra8 = new Decimal("10^^8");
|
const tetra8 = new Decimal("10^^8");
|
||||||
const e100 = new Decimal("1e100");
|
const e100 = new Decimal("1e100");
|
||||||
|
/** Returns a reference to the amount of resource being gained in terms of orders of magnitude per second, calcualted over the last tick. Useful for situations where the gain rate is increasing very rapidly. */
|
||||||
export function trackOOMPS(
|
export function trackOOMPS(
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
pointGain?: ComputedRef<DecimalSource>
|
pointGain?: ComputedRef<DecimalSource>
|
||||||
|
@ -135,6 +149,7 @@ export function trackOOMPS(
|
||||||
return oompsString;
|
return oompsString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility for displaying a resource with the correct precision. */
|
||||||
export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string {
|
export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string {
|
||||||
const amount = overrideAmount ?? resource.value;
|
const amount = overrideAmount ?? resource.value;
|
||||||
if (Decimal.eq(resource.precision, 0)) {
|
if (Decimal.eq(resource.precision, 0)) {
|
||||||
|
@ -143,6 +158,7 @@ export function displayResource(resource: Resource, overrideAmount?: DecimalSour
|
||||||
return format(amount, resource.precision, resource.small);
|
return format(amount, resource.precision, resource.small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility for unwrapping a resource that may or may not be inside a ref. */
|
||||||
export function unwrapResource(resource: ProcessedComputable<Resource>): Resource {
|
export function unwrapResource(resource: ProcessedComputable<Resource>): Resource {
|
||||||
if ("displayName" in resource) {
|
if ("displayName" in resource) {
|
||||||
return resource;
|
return resource;
|
||||||
|
|
|
@ -10,21 +10,39 @@ import TabComponent from "features/tabs/Tab.vue";
|
||||||
import type { Computable, GetComputableType } from "util/computed";
|
import type { Computable, GetComputableType } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Tab} features. */
|
||||||
export const TabType = Symbol("Tab");
|
export const TabType = Symbol("Tab");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Tab}.
|
||||||
|
*/
|
||||||
export interface TabOptions {
|
export interface TabOptions {
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** The display to use for this tab. */
|
||||||
display: Computable<CoercableComponent>;
|
display: Computable<CoercableComponent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link TabOptions} to create an {@link Tab}.
|
||||||
|
*/
|
||||||
export interface BaseTab {
|
export interface BaseTab {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof TabType;
|
type: typeof TabType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representing a tab of content in a tabbed interface.
|
||||||
|
* @see {@link TabFamily}
|
||||||
|
*/
|
||||||
export type Tab<T extends TabOptions> = Replace<
|
export type Tab<T extends TabOptions> = Replace<
|
||||||
T & BaseTab,
|
T & BaseTab,
|
||||||
{
|
{
|
||||||
|
@ -34,13 +52,18 @@ export type Tab<T extends TabOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Tab} object. */
|
||||||
export type GenericTab = Tab<TabOptions>;
|
export type GenericTab = Tab<TabOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a tab with the given options.
|
||||||
|
* @param optionsFunc Tab options.
|
||||||
|
*/
|
||||||
export function createTab<T extends TabOptions>(
|
export function createTab<T extends TabOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseTab, GenericTab>
|
optionsFunc: OptionsFunc<T, BaseTab, GenericTab>
|
||||||
): Tab<T> {
|
): Tab<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const tab = optionsFunc();
|
const tab = optionsFunc.call(feature, feature);
|
||||||
tab.id = getUniqueID("tab-");
|
tab.id = getUniqueID("tab-");
|
||||||
tab.type = TabType;
|
tab.type = TabType;
|
||||||
tab[Component] = TabComponent as GenericComponent;
|
tab[Component] = TabComponent as GenericComponent;
|
||||||
|
|
|
@ -29,23 +29,43 @@ import type { Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
import type { GenericTab } from "./tab";
|
import type { GenericTab } from "./tab";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link TabButton} features. */
|
||||||
export const TabButtonType = Symbol("TabButton");
|
export const TabButtonType = Symbol("TabButton");
|
||||||
|
/** A symbol used to identify {@link TabFamily} features. */
|
||||||
export const TabFamilyType = Symbol("TabFamily");
|
export const TabFamilyType = Symbol("TabFamily");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link TabButton}.
|
||||||
|
*/
|
||||||
export interface TabButtonOptions {
|
export interface TabButtonOptions {
|
||||||
|
/** Whether this tab button should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The tab to display when this button is clicked. */
|
||||||
tab: Computable<GenericTab | CoercableComponent>;
|
tab: Computable<GenericTab | CoercableComponent>;
|
||||||
|
/** The label on this button. */
|
||||||
display: Computable<CoercableComponent>;
|
display: Computable<CoercableComponent>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** The color of the glow effect to display when this button is active. */
|
||||||
glowColor?: Computable<string>;
|
glowColor?: Computable<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link TabButtonOptions} to create an {@link TabButton}.
|
||||||
|
*/
|
||||||
export interface BaseTabButton {
|
export interface BaseTabButton {
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof TabButtonType;
|
type: typeof TabButtonType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that represents a button that can be clicked to change tabs in a tabbed interface.
|
||||||
|
* @see {@link TabFamily}
|
||||||
|
*/
|
||||||
export type TabButton<T extends TabButtonOptions> = Replace<
|
export type TabButton<T extends TabButtonOptions> = Replace<
|
||||||
T & BaseTabButton,
|
T & BaseTabButton,
|
||||||
{
|
{
|
||||||
|
@ -58,6 +78,7 @@ export type TabButton<T extends TabButtonOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link TabButton} object. */
|
||||||
export type GenericTabButton = Replace<
|
export type GenericTabButton = Replace<
|
||||||
TabButton<TabButtonOptions>,
|
TabButton<TabButtonOptions>,
|
||||||
{
|
{
|
||||||
|
@ -65,24 +86,46 @@ export type GenericTabButton = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link TabFamily}.
|
||||||
|
*/
|
||||||
export interface TabFamilyOptions {
|
export interface TabFamilyOptions {
|
||||||
|
/** Whether this tab button should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** A dictionary of CSS classes to apply to the list of buttons for changing tabs. */
|
||||||
buttonContainerClasses?: Computable<Record<string, boolean>>;
|
buttonContainerClasses?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to the list of buttons for changing tabs. */
|
||||||
buttonContainerStyle?: Computable<StyleValue>;
|
buttonContainerStyle?: Computable<StyleValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link TabFamilyOptions} to create an {@link TabFamily}.
|
||||||
|
*/
|
||||||
export interface BaseTabFamily {
|
export interface BaseTabFamily {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** All the tabs within this family. */
|
||||||
tabs: Record<string, TabButtonOptions>;
|
tabs: Record<string, TabButtonOptions>;
|
||||||
|
/** The currently active tab, if any. */
|
||||||
activeTab: Ref<GenericTab | CoercableComponent | null>;
|
activeTab: Ref<GenericTab | CoercableComponent | null>;
|
||||||
|
/** The name of the tab that is currently active. */
|
||||||
selected: Persistent<string>;
|
selected: Persistent<string>;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof TabFamilyType;
|
type: typeof TabFamilyType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that represents a tabbed interface.
|
||||||
|
* @see {@link TabFamily}
|
||||||
|
*/
|
||||||
export type TabFamily<T extends TabFamilyOptions> = Replace<
|
export type TabFamily<T extends TabFamilyOptions> = Replace<
|
||||||
T & BaseTabFamily,
|
T & BaseTabFamily,
|
||||||
{
|
{
|
||||||
|
@ -91,6 +134,7 @@ export type TabFamily<T extends TabFamilyOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link TabFamily} object. */
|
||||||
export type GenericTabFamily = Replace<
|
export type GenericTabFamily = Replace<
|
||||||
TabFamily<TabFamilyOptions>,
|
TabFamily<TabFamilyOptions>,
|
||||||
{
|
{
|
||||||
|
@ -98,18 +142,23 @@ export type GenericTabFamily = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a tab family with the given options.
|
||||||
|
* @param optionsFunc Tab family options.
|
||||||
|
*/
|
||||||
export function createTabFamily<T extends TabFamilyOptions>(
|
export function createTabFamily<T extends TabFamilyOptions>(
|
||||||
tabs: Record<string, () => TabButtonOptions>,
|
tabs: Record<string, () => TabButtonOptions>,
|
||||||
optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
|
optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
|
||||||
): TabFamily<T> {
|
): TabFamily<T> {
|
||||||
if (Object.keys(tabs).length === 0) {
|
if (Object.keys(tabs).length === 0) {
|
||||||
console.warn("Cannot create tab family with 0 tabs");
|
console.error("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);
|
const selected = persistent(Object.keys(tabs)[0], false);
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const tabFamily = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
const tabFamily =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
|
|
||||||
tabFamily.id = getUniqueID("tabFamily-");
|
tabFamily.id = getUniqueID("tabFamily-");
|
||||||
tabFamily.type = TabFamilyType;
|
tabFamily.type = TabFamilyType;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CoercableComponent, Replace, StyleValue } from "features/feature";
|
import type { CoercableComponent, GenericComponent, Replace, StyleValue } from "features/feature";
|
||||||
import { Component, GatherProps, setDefault } from "features/feature";
|
import { Component, GatherProps, setDefault } from "features/feature";
|
||||||
import { deletePersistent, Persistent, persistent } from "game/persistence";
|
import { deletePersistent, Persistent, persistent } from "game/persistence";
|
||||||
import { Direction } from "util/common";
|
import { Direction } from "util/common";
|
||||||
|
@ -21,24 +21,38 @@ declare module "@vue/runtime-dom" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Tooltip}.
|
||||||
|
*/
|
||||||
export interface TooltipOptions {
|
export interface TooltipOptions {
|
||||||
|
/** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
|
||||||
pinnable?: boolean;
|
pinnable?: boolean;
|
||||||
|
/** The text to display inside the tooltip. */
|
||||||
display: Computable<CoercableComponent>;
|
display: Computable<CoercableComponent>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** The direction in which to display the tooltip */
|
||||||
direction?: Computable<Direction>;
|
direction?: Computable<Direction>;
|
||||||
|
/** The x offset of the tooltip, in px. */
|
||||||
xoffset?: Computable<string>;
|
xoffset?: Computable<string>;
|
||||||
|
/** The y offset of the tooltip, in px. */
|
||||||
yoffset?: Computable<string>;
|
yoffset?: Computable<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link TooltipOptions} to create an {@link Tooltip}.
|
||||||
|
*/
|
||||||
export interface BaseTooltip {
|
export interface BaseTooltip {
|
||||||
pinned?: Ref<boolean>;
|
pinned?: Ref<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a tooltip that appears when hovering over an element. */
|
||||||
export type Tooltip<T extends TooltipOptions> = Replace<
|
export type Tooltip<T extends TooltipOptions> = Replace<
|
||||||
T & BaseTooltip,
|
T & BaseTooltip,
|
||||||
{
|
{
|
||||||
pinnable: T["pinnable"] extends undefined ? false : T["pinnable"];
|
pinnable: undefined extends T["pinnable"] ? false : T["pinnable"];
|
||||||
pinned: T["pinnable"] extends true ? Ref<boolean> : undefined;
|
pinned: T["pinnable"] extends true ? Ref<boolean> : undefined;
|
||||||
display: GetComputableType<T["display"]>;
|
display: GetComputableType<T["display"]>;
|
||||||
classes: GetComputableType<T["classes"]>;
|
classes: GetComputableType<T["classes"]>;
|
||||||
|
@ -49,6 +63,7 @@ export type Tooltip<T extends TooltipOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Tooltip} object. */
|
||||||
export type GenericTooltip = Replace<
|
export type GenericTooltip = Replace<
|
||||||
Tooltip<TooltipOptions>,
|
Tooltip<TooltipOptions>,
|
||||||
{
|
{
|
||||||
|
@ -58,6 +73,11 @@ export type GenericTooltip = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a tooltip on the given element with the given options.
|
||||||
|
* @param element The renderable feature to display the tooltip on.
|
||||||
|
* @param options Tooltip options.
|
||||||
|
*/
|
||||||
export function addTooltip<T extends TooltipOptions>(
|
export function addTooltip<T extends TooltipOptions>(
|
||||||
element: VueFeature,
|
element: VueFeature,
|
||||||
options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
|
options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
|
||||||
|
@ -75,20 +95,8 @@ export function addTooltip<T extends TooltipOptions>(
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
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];
|
const elementComponent = element[Component];
|
||||||
element[Component] = TooltipComponent;
|
element[Component] = TooltipComponent as GenericComponent;
|
||||||
const elementGatherProps = element[GatherProps].bind(element);
|
const elementGatherProps = element[GatherProps].bind(element);
|
||||||
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
|
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
|
||||||
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
|
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
|
||||||
|
|
|
@ -113,7 +113,6 @@ export default defineComponent({
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.25);
|
||||||
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0px 0px 20px var(--background);
|
box-shadow: -4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0px 0px 20px var(--background);
|
||||||
text-transform: capitalize;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Decorator, GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -25,30 +26,54 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, ref, shallowRef, unref } from "vue";
|
import { computed, ref, shallowRef, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link TreeNode} features. */
|
||||||
export const TreeNodeType = Symbol("TreeNode");
|
export const TreeNodeType = Symbol("TreeNode");
|
||||||
|
/** A symbol used to identify {@link Tree} features. */
|
||||||
export const TreeType = Symbol("Tree");
|
export const TreeType = Symbol("Tree");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link TreeNode}.
|
||||||
|
*/
|
||||||
export interface TreeNodeOptions {
|
export interface TreeNodeOptions {
|
||||||
|
/** Whether this tree node should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** Whether or not this tree node can be clicked. */
|
||||||
canClick?: Computable<boolean>;
|
canClick?: Computable<boolean>;
|
||||||
|
/** The background color for this node. */
|
||||||
color?: Computable<string>;
|
color?: Computable<string>;
|
||||||
|
/** The label to display on this tree node. */
|
||||||
display?: Computable<CoercableComponent>;
|
display?: Computable<CoercableComponent>;
|
||||||
|
/** The color of the glow effect shown to notify the user there's something to do with this node. */
|
||||||
glowColor?: Computable<string>;
|
glowColor?: Computable<string>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
mark?: Computable<boolean | string>;
|
mark?: Computable<boolean | string>;
|
||||||
|
/** A reset object attached to this node, used for propagating resets through the tree. */
|
||||||
reset?: GenericReset;
|
reset?: GenericReset;
|
||||||
|
/** A function that is called when the tree node is clicked. */
|
||||||
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
onClick?: (e?: MouseEvent | TouchEvent) => void;
|
||||||
|
/** A function that is called when the tree node is held down. */
|
||||||
onHold?: VoidFunction;
|
onHold?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}.
|
||||||
|
*/
|
||||||
export interface BaseTreeNode {
|
export interface BaseTreeNode {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof TreeNodeType;
|
type: typeof TreeNodeType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a node on a tree. */
|
||||||
export type TreeNode<T extends TreeNodeOptions> = Replace<
|
export type TreeNode<T extends TreeNodeOptions> = Replace<
|
||||||
T & BaseTreeNode,
|
T & BaseTreeNode,
|
||||||
{
|
{
|
||||||
|
@ -63,6 +88,7 @@ export type TreeNode<T extends TreeNodeOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link TreeNode} object. */
|
||||||
export type GenericTreeNode = Replace<
|
export type GenericTreeNode = Replace<
|
||||||
TreeNode<TreeNodeOptions>,
|
TreeNode<TreeNodeOptions>,
|
||||||
{
|
{
|
||||||
|
@ -71,15 +97,32 @@ export type GenericTreeNode = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a tree node with the given options.
|
||||||
|
* @param optionsFunc Tree Node options.
|
||||||
|
*/
|
||||||
export function createTreeNode<T extends TreeNodeOptions>(
|
export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>
|
optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): TreeNode<T> {
|
): TreeNode<T> {
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const treeNode = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const treeNode =
|
||||||
|
optionsFunc?.call(feature, feature) ??
|
||||||
|
({} as ReturnType<NonNullable<typeof optionsFunc>>);
|
||||||
treeNode.id = getUniqueID("treeNode-");
|
treeNode.id = getUniqueID("treeNode-");
|
||||||
treeNode.type = TreeNodeType;
|
treeNode.type = TreeNodeType;
|
||||||
treeNode[Component] = TreeNodeComponent as GenericComponent;
|
treeNode[Component] = TreeNodeComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(decoratedData);
|
||||||
|
|
||||||
processComputable(treeNode as T, "visibility");
|
processComputable(treeNode as T, "visibility");
|
||||||
setDefault(treeNode, "visibility", Visibility.Visible);
|
setDefault(treeNode, "visibility", Visibility.Visible);
|
||||||
processComputable(treeNode as T, "canClick");
|
processComputable(treeNode as T, "canClick");
|
||||||
|
@ -91,6 +134,10 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
processComputable(treeNode as T, "style");
|
processComputable(treeNode as T, "style");
|
||||||
processComputable(treeNode as T, "mark");
|
processComputable(treeNode as T, "mark");
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(treeNode);
|
||||||
|
}
|
||||||
|
|
||||||
if (treeNode.onClick) {
|
if (treeNode.onClick) {
|
||||||
const onClick = treeNode.onClick.bind(treeNode);
|
const onClick = treeNode.onClick.bind(treeNode);
|
||||||
treeNode.onClick = function (e) {
|
treeNode.onClick = function (e) {
|
||||||
|
@ -108,6 +155,10 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(treeNode)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
treeNode[GatherProps] = function (this: GenericTreeNode) {
|
treeNode[GatherProps] = function (this: GenericTreeNode) {
|
||||||
const {
|
const {
|
||||||
display,
|
display,
|
||||||
|
@ -133,7 +184,8 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
glowColor,
|
glowColor,
|
||||||
canClick,
|
canClick,
|
||||||
mark,
|
mark,
|
||||||
id
|
id,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,32 +193,52 @@ export function createTreeNode<T extends TreeNodeOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Represents a branch between two nodes in a tree. */
|
||||||
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
|
export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
|
||||||
startNode: GenericTreeNode;
|
startNode: GenericTreeNode;
|
||||||
endNode: GenericTreeNode;
|
endNode: GenericTreeNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Tree}.
|
||||||
|
*/
|
||||||
export interface TreeOptions {
|
export interface TreeOptions {
|
||||||
|
/** Whether this clickable should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** The nodes within the tree, in a 2D array. */
|
||||||
nodes: Computable<GenericTreeNode[][]>;
|
nodes: Computable<GenericTreeNode[][]>;
|
||||||
|
/** Nodes to show on the left side of the tree. */
|
||||||
leftSideNodes?: Computable<GenericTreeNode[]>;
|
leftSideNodes?: Computable<GenericTreeNode[]>;
|
||||||
|
/** Nodes to show on the right side of the tree. */
|
||||||
rightSideNodes?: Computable<GenericTreeNode[]>;
|
rightSideNodes?: Computable<GenericTreeNode[]>;
|
||||||
|
/** The branches between nodes within this tree. */
|
||||||
branches?: Computable<TreeBranch[]>;
|
branches?: Computable<TreeBranch[]>;
|
||||||
|
/** How to propagate resets through the tree. */
|
||||||
resetPropagation?: ResetPropagation;
|
resetPropagation?: ResetPropagation;
|
||||||
|
/** A function that is called when a node within the tree is reset. */
|
||||||
onReset?: (node: GenericTreeNode) => void;
|
onReset?: (node: GenericTreeNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseTree {
|
export interface BaseTree {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** The link objects for each of the branches of the tree. */
|
||||||
links: Ref<Link[]>;
|
links: Ref<Link[]>;
|
||||||
|
/** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
|
||||||
reset: (node: GenericTreeNode) => void;
|
reset: (node: GenericTreeNode) => void;
|
||||||
|
/** A flag that is true while the reset is still propagating through the tree. */
|
||||||
isResetting: Ref<boolean>;
|
isResetting: Ref<boolean>;
|
||||||
|
/** A reference to the node that caused the currently propagating reset. */
|
||||||
resettingNode: Ref<GenericTreeNode | null>;
|
resettingNode: Ref<GenericTreeNode | null>;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof TreeType;
|
type: typeof TreeType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */
|
||||||
export type Tree<T extends TreeOptions> = Replace<
|
export type Tree<T extends TreeOptions> = Replace<
|
||||||
T & BaseTree,
|
T & BaseTree,
|
||||||
{
|
{
|
||||||
|
@ -178,6 +250,7 @@ export type Tree<T extends TreeOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Tree} object. */
|
||||||
export type GenericTree = Replace<
|
export type GenericTree = Replace<
|
||||||
Tree<TreeOptions>,
|
Tree<TreeOptions>,
|
||||||
{
|
{
|
||||||
|
@ -185,11 +258,15 @@ export type GenericTree = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates a tree with the given options.
|
||||||
|
* @param optionsFunc Tree options.
|
||||||
|
*/
|
||||||
export function createTree<T extends TreeOptions>(
|
export function createTree<T extends TreeOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseTree, GenericTree>
|
optionsFunc: OptionsFunc<T, BaseTree, GenericTree>
|
||||||
): Tree<T> {
|
): Tree<T> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const tree = optionsFunc();
|
const tree = optionsFunc.call(feature, feature);
|
||||||
tree.id = getUniqueID("tree-");
|
tree.id = getUniqueID("tree-");
|
||||||
tree.type = TreeType;
|
tree.type = TreeType;
|
||||||
tree[Component] = TreeComponent as GenericComponent;
|
tree[Component] = TreeComponent as GenericComponent;
|
||||||
|
@ -227,10 +304,12 @@ export function createTree<T extends TreeOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A function that is used to propagate resets through a tree. */
|
||||||
export type ResetPropagation = {
|
export type ResetPropagation = {
|
||||||
(tree: GenericTree, resettingNode: GenericTreeNode): void;
|
(tree: GenericTree, resettingNode: GenericTreeNode): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||||
export const defaultResetPropagation = function (
|
export const defaultResetPropagation = function (
|
||||||
tree: GenericTree,
|
tree: GenericTree,
|
||||||
resettingNode: GenericTreeNode
|
resettingNode: GenericTreeNode
|
||||||
|
@ -242,6 +321,7 @@ export const defaultResetPropagation = function (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Propagate resets down the tree by resetting every node in a lower row. */
|
||||||
export const invertedResetPropagation = function (
|
export const invertedResetPropagation = function (
|
||||||
tree: GenericTree,
|
tree: GenericTree,
|
||||||
resettingNode: GenericTreeNode
|
resettingNode: GenericTreeNode
|
||||||
|
@ -253,41 +333,33 @@ export const invertedResetPropagation = function (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Propagate resets down the branches of the tree. */
|
||||||
export const branchedResetPropagation = function (
|
export const branchedResetPropagation = function (
|
||||||
tree: GenericTree,
|
tree: GenericTree,
|
||||||
resettingNode: GenericTreeNode
|
resettingNode: GenericTreeNode
|
||||||
): void {
|
): void {
|
||||||
const visitedNodes = [resettingNode];
|
const links = unref(tree.branches);
|
||||||
let currentNodes = [resettingNode];
|
if (links == null) return;
|
||||||
if (tree.branches != null) {
|
const reset: GenericTreeNode[] = [];
|
||||||
const branches = unref(tree.branches);
|
let current = [resettingNode];
|
||||||
while (currentNodes.length > 0) {
|
while (current.length != 0) {
|
||||||
const nextNodes: GenericTreeNode[] = [];
|
const next: GenericTreeNode[] = [];
|
||||||
currentNodes.forEach(node => {
|
for (const node of current) {
|
||||||
branches
|
for (const link of links.filter(link => link.startNode === node)) {
|
||||||
.filter(branch => branch.startNode === node || branch.endNode === node)
|
if ([...reset, ...current].includes(link.endNode)) continue
|
||||||
.map(branch => {
|
next.push(link.endNode);
|
||||||
if (branch.startNode === node) {
|
link.endNode.reset?.reset();
|
||||||
return branch.endNode;
|
}
|
||||||
}
|
};
|
||||||
return branch.startNode;
|
reset.push(...current);
|
||||||
})
|
current = next;
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource.
|
||||||
|
* It sounds oddly specific, but comes up a lot.
|
||||||
|
*/
|
||||||
export function createResourceTooltip(
|
export function createResourceTooltip(
|
||||||
resource: Resource,
|
resource: Resource,
|
||||||
requiredResource: Resource | null = null,
|
requiredResource: Resource | null = null,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { isArray } from "@vue/shared";
|
import { isArray } from "@vue/shared";
|
||||||
|
import { GenericDecorator } from "features/decorators/common";
|
||||||
import type {
|
import type {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
GenericComponent,
|
GenericComponent,
|
||||||
|
@ -8,20 +9,20 @@ import type {
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
findFeatures,
|
|
||||||
GatherProps,
|
GatherProps,
|
||||||
|
Visibility,
|
||||||
|
findFeatures,
|
||||||
getUniqueID,
|
getUniqueID,
|
||||||
setDefault,
|
setDefault
|
||||||
Visibility
|
|
||||||
} from "features/feature";
|
} from "features/feature";
|
||||||
import UpgradeComponent from "features/upgrades/Upgrade.vue";
|
import UpgradeComponent from "features/upgrades/Upgrade.vue";
|
||||||
import type { GenericLayer } from "game/layers";
|
import type { GenericLayer } from "game/layers";
|
||||||
import type { Persistent } from "game/persistence";
|
import type { Persistent } from "game/persistence";
|
||||||
import { persistent } from "game/persistence";
|
import { persistent } from "game/persistence";
|
||||||
import {
|
import {
|
||||||
|
Requirements,
|
||||||
createVisibilityRequirement,
|
createVisibilityRequirement,
|
||||||
payRequirements,
|
payRequirements,
|
||||||
Requirements,
|
|
||||||
requirementsMet
|
requirementsMet
|
||||||
} from "game/requirements";
|
} from "game/requirements";
|
||||||
import { isFunction } from "util/common";
|
import { isFunction } from "util/common";
|
||||||
|
@ -36,35 +37,60 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
|
||||||
|
/** A symbol used to identify {@link Upgrade} features. */
|
||||||
export const UpgradeType = Symbol("Upgrade");
|
export const UpgradeType = Symbol("Upgrade");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that configures a {@link Upgrade}.
|
||||||
|
*/
|
||||||
export interface UpgradeOptions {
|
export interface UpgradeOptions {
|
||||||
|
/** Whether this clickable should be visible. */
|
||||||
visibility?: Computable<Visibility | boolean>;
|
visibility?: Computable<Visibility | boolean>;
|
||||||
|
/** Dictionary of CSS classes to apply to this feature. */
|
||||||
classes?: Computable<Record<string, boolean>>;
|
classes?: Computable<Record<string, boolean>>;
|
||||||
|
/** CSS to apply to this feature. */
|
||||||
style?: Computable<StyleValue>;
|
style?: Computable<StyleValue>;
|
||||||
|
/** Shows a marker on the corner of the feature. */
|
||||||
|
mark?: Computable<boolean | string>;
|
||||||
|
/** The display to use for this clickable. */
|
||||||
display?: Computable<
|
display?: Computable<
|
||||||
| CoercableComponent
|
| CoercableComponent
|
||||||
| {
|
| {
|
||||||
|
/** A header to appear at the top of the display. */
|
||||||
title?: CoercableComponent;
|
title?: CoercableComponent;
|
||||||
|
/** The main text that appears in the display. */
|
||||||
description: CoercableComponent;
|
description: CoercableComponent;
|
||||||
|
/** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
|
||||||
effectDisplay?: CoercableComponent;
|
effectDisplay?: CoercableComponent;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
/** The requirements to purchase this upgrade. */
|
||||||
requirements: Requirements;
|
requirements: Requirements;
|
||||||
mark?: Computable<boolean | string>;
|
/** A function that is called when the upgrade is purchased. */
|
||||||
onPurchase?: VoidFunction;
|
onPurchase?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The properties that are added onto a processed {@link UpgradeOptions} to create an {@link Upgrade}.
|
||||||
|
*/
|
||||||
export interface BaseUpgrade {
|
export interface BaseUpgrade {
|
||||||
|
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** Whether or not this upgrade has been purchased. */
|
||||||
bought: Persistent<boolean>;
|
bought: Persistent<boolean>;
|
||||||
|
/** Whether or not the upgrade can currently be purchased. */
|
||||||
canPurchase: Ref<boolean>;
|
canPurchase: Ref<boolean>;
|
||||||
|
/** Purchase the upgrade */
|
||||||
purchase: VoidFunction;
|
purchase: VoidFunction;
|
||||||
|
/** A symbol that helps identify features of the same type. */
|
||||||
type: typeof UpgradeType;
|
type: typeof UpgradeType;
|
||||||
|
/** The Vue component used to render this feature. */
|
||||||
[Component]: GenericComponent;
|
[Component]: GenericComponent;
|
||||||
|
/** A function to gather the props the vue component requires for this feature. */
|
||||||
[GatherProps]: () => Record<string, unknown>;
|
[GatherProps]: () => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An object that represents a feature that can be purchased a single time. */
|
||||||
export type Upgrade<T extends UpgradeOptions> = Replace<
|
export type Upgrade<T extends UpgradeOptions> = Replace<
|
||||||
T & BaseUpgrade,
|
T & BaseUpgrade,
|
||||||
{
|
{
|
||||||
|
@ -77,6 +103,7 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/** A type that matches any valid {@link Upgrade} object. */
|
||||||
export type GenericUpgrade = Replace<
|
export type GenericUpgrade = Replace<
|
||||||
Upgrade<UpgradeOptions>,
|
Upgrade<UpgradeOptions>,
|
||||||
{
|
{
|
||||||
|
@ -84,18 +111,35 @@ export type GenericUpgrade = Replace<
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily creates an upgrade with the given options.
|
||||||
|
* @param optionsFunc Upgrade options.
|
||||||
|
*/
|
||||||
export function createUpgrade<T extends UpgradeOptions>(
|
export function createUpgrade<T extends UpgradeOptions>(
|
||||||
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>
|
optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>,
|
||||||
|
...decorators: GenericDecorator[]
|
||||||
): Upgrade<T> {
|
): Upgrade<T> {
|
||||||
const bought = persistent<boolean>(false, false);
|
const bought = persistent<boolean>(false, false);
|
||||||
return createLazyProxy(() => {
|
const decoratedData = decorators.reduce(
|
||||||
const upgrade = optionsFunc();
|
(current, next) => Object.assign(current, next.getPersistentData?.()),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return createLazyProxy(feature => {
|
||||||
|
const upgrade = optionsFunc.call(feature, feature);
|
||||||
upgrade.id = getUniqueID("upgrade-");
|
upgrade.id = getUniqueID("upgrade-");
|
||||||
upgrade.type = UpgradeType;
|
upgrade.type = UpgradeType;
|
||||||
upgrade[Component] = UpgradeComponent as GenericComponent;
|
upgrade[Component] = UpgradeComponent as GenericComponent;
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.preConstruct?.(upgrade);
|
||||||
|
}
|
||||||
|
|
||||||
upgrade.bought = bought;
|
upgrade.bought = bought;
|
||||||
upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
|
Object.assign(upgrade, decoratedData);
|
||||||
|
|
||||||
|
upgrade.canPurchase = computed(
|
||||||
|
() => !bought.value && requirementsMet(upgrade.requirements)
|
||||||
|
);
|
||||||
upgrade.purchase = function () {
|
upgrade.purchase = function () {
|
||||||
const genericUpgrade = upgrade as GenericUpgrade;
|
const genericUpgrade = upgrade as GenericUpgrade;
|
||||||
if (!unref(genericUpgrade.canPurchase)) {
|
if (!unref(genericUpgrade.canPurchase)) {
|
||||||
|
@ -120,6 +164,14 @@ export function createUpgrade<T extends UpgradeOptions>(
|
||||||
processComputable(upgrade as T, "display");
|
processComputable(upgrade as T, "display");
|
||||||
processComputable(upgrade as T, "mark");
|
processComputable(upgrade as T, "mark");
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
decorator.postConstruct?.(upgrade);
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratedProps = decorators.reduce(
|
||||||
|
(current, next) => Object.assign(current, next.getGatheredProps?.(upgrade)),
|
||||||
|
{}
|
||||||
|
);
|
||||||
upgrade[GatherProps] = function (this: GenericUpgrade) {
|
upgrade[GatherProps] = function (this: GenericUpgrade) {
|
||||||
const {
|
const {
|
||||||
display,
|
display,
|
||||||
|
@ -143,7 +195,8 @@ export function createUpgrade<T extends UpgradeOptions>(
|
||||||
bought,
|
bought,
|
||||||
mark,
|
mark,
|
||||||
id,
|
id,
|
||||||
purchase
|
purchase,
|
||||||
|
...decoratedProps
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -151,6 +204,12 @@ export function createUpgrade<T extends UpgradeOptions>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to auto purchase a list of upgrades whenever they're affordable.
|
||||||
|
* @param layer The layer the upgrades are apart of
|
||||||
|
* @param autoActive Whether or not the upgrades should currently be auto-purchasing
|
||||||
|
* @param upgrades The specific upgrades to upgrade. If unspecified, uses all upgrades on the layer.
|
||||||
|
*/
|
||||||
export function setupAutoPurchase(
|
export function setupAutoPurchase(
|
||||||
layer: GenericLayer,
|
layer: GenericLayer,
|
||||||
autoActive: Computable<boolean>,
|
autoActive: Computable<boolean>,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,12 @@
|
||||||
import Decimal, { DecimalSource } from "util/bignum";
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
|
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
|
||||||
import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
|
import {
|
||||||
|
FormulaSource,
|
||||||
|
GenericFormula,
|
||||||
|
InvertFunction,
|
||||||
|
InvertibleFormula,
|
||||||
|
SubstitutionStack
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const ln10 = Decimal.ln(10);
|
const ln10 = Decimal.ln(10);
|
||||||
|
|
||||||
|
@ -8,18 +14,33 @@ export function passthrough<T extends GenericFormula | DecimalSource>(value: T):
|
||||||
return value;
|
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) {
|
export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.neg(value));
|
return lhs.invert(Decimal.neg(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
return Formula.neg(lhs.getIntegralFormula(stack));
|
return Formula.neg(lhs.getIntegralFormula(stack));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySubstitutionNeg(value: GenericFormula) {
|
export function applySubstitutionNeg(value: GenericFormula) {
|
||||||
|
@ -32,18 +53,28 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
|
return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
|
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x);
|
return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateInnerAdd(
|
export function integrateInnerAdd(
|
||||||
|
@ -52,13 +83,22 @@ export function integrateInnerAdd(
|
||||||
rhs: FormulaSource
|
rhs: FormulaSource
|
||||||
) {
|
) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.add(x, rhs);
|
return Formula.add(x, rhs);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.add(x, lhs);
|
return Formula.add(x, lhs);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -67,18 +107,28 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
|
return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
|
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x);
|
return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateInnerSub(
|
export function integrateInnerSub(
|
||||||
|
@ -87,13 +137,22 @@ export function integrateInnerSub(
|
||||||
rhs: FormulaSource
|
rhs: FormulaSource
|
||||||
) {
|
) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.sub(x, rhs);
|
return Formula.sub(x, rhs);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.sub(x, lhs);
|
return Formula.sub(x, lhs);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -102,18 +161,28 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
|
return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.times(x, rhs);
|
return Formula.times(x, rhs);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.times(x, lhs);
|
return Formula.times(x, lhs);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySubstitutionMul(
|
export function applySubstitutionMul(
|
||||||
|
@ -126,7 +195,8 @@ export function applySubstitutionMul(
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return Formula.div(value, lhs);
|
return Formula.div(value, lhs);
|
||||||
}
|
}
|
||||||
throw new Error("Could not apply substitution due to no input being a variable");
|
console.error("Could not apply substitution due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -135,18 +205,28 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
|
return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.div(x, rhs);
|
return Formula.div(x, rhs);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.div(lhs, x);
|
return Formula.div(lhs, x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySubstitutionDiv(
|
export function applySubstitutionDiv(
|
||||||
|
@ -159,29 +239,37 @@ export function applySubstitutionDiv(
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return Formula.mul(value, lhs);
|
return Formula.mul(value, lhs);
|
||||||
}
|
}
|
||||||
throw new Error("Could not apply substitution due to no input being a variable");
|
console.error("Could not apply substitution due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
|
export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.recip(value));
|
return lhs.invert(Decimal.recip(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.ln(x);
|
return Formula.ln(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
|
export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.pow10(value));
|
return lhs.invert(Decimal.pow10(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalIntegrateLog10(lhs: DecimalSource) {
|
function internalIntegrateLog10(lhs: DecimalSource) {
|
||||||
|
@ -193,18 +281,24 @@ function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
|
||||||
const numerator = ln10.times(value);
|
const numerator = ln10.times(value);
|
||||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [lhs.getIntegralFormula(stack)],
|
inputs: [lhs.getIntegralFormula(stack)],
|
||||||
evaluate: internalIntegrateLog10,
|
evaluate: internalIntegrateLog10,
|
||||||
invert: internalInvertIntegralLog10
|
invert: internalInvertIntegralLog10
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -213,7 +307,8 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
|
function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
|
||||||
|
@ -225,25 +320,32 @@ function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs
|
||||||
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
|
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
|
||||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [lhs.getIntegralFormula(stack), rhs],
|
inputs: [lhs.getIntegralFormula(stack), rhs],
|
||||||
evaluate: internalIntegrateLog,
|
evaluate: internalIntegrateLog,
|
||||||
invert: internalInvertIntegralLog
|
invert: internalInvertIntegralLog
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
|
export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.pow(2, value));
|
return lhs.invert(Decimal.pow(2, value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalIntegrateLog2(lhs: DecimalSource) {
|
function internalIntegrateLog2(lhs: DecimalSource) {
|
||||||
|
@ -255,25 +357,32 @@ function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
|
||||||
const numerator = Decimal.ln(2).times(value);
|
const numerator = Decimal.ln(2).times(value);
|
||||||
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [lhs.getIntegralFormula(stack)],
|
inputs: [lhs.getIntegralFormula(stack)],
|
||||||
evaluate: internalIntegrateLog2,
|
evaluate: internalIntegrateLog2,
|
||||||
invert: internalInvertIntegralLog2
|
invert: internalInvertIntegralLog2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertLn(value: DecimalSource, lhs: FormulaSource) {
|
export function invertLn(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.exp(value));
|
return lhs.invert(Decimal.exp(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function internalIntegrateLn(lhs: DecimalSource) {
|
function internalIntegrateLn(lhs: DecimalSource) {
|
||||||
|
@ -284,18 +393,24 @@ function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw()));
|
return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw()));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
return new Formula({
|
return new Formula({
|
||||||
inputs: [lhs.getIntegralFormula(stack)],
|
inputs: [lhs.getIntegralFormula(stack)],
|
||||||
evaluate: internalIntegrateLn,
|
evaluate: internalIntegrateLn,
|
||||||
invert: internalInvertIntegralLn
|
invert: internalInvertIntegralLn
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -304,55 +419,81 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
|
return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
const pow = Formula.add(rhs, 1);
|
const pow = Formula.add(rhs, 1);
|
||||||
return Formula.pow(x, pow).div(pow);
|
return Formula.pow(x, pow).div(pow);
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
return Formula.pow(lhs, x).div(Formula.ln(lhs));
|
return Formula.pow(lhs, x).div(Formula.ln(lhs));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
|
export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.root(value, 10));
|
return lhs.invert(Decimal.root(value, 10));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.pow10(x).div(Formula.ln(10));
|
return Formula.pow10(x).div(Formula.ln(10));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
|
return lhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(rhs))));
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.pow(rhs, x).div(Formula.ln(rhs));
|
return Formula.pow(rhs, x).div(Formula.ln(rhs));
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
|
if (!rhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = rhs.getIntegralFormula(stack);
|
const x = rhs.getIntegralFormula(stack);
|
||||||
const denominator = Formula.add(lhs, 1);
|
const denominator = Formula.add(lhs, 1);
|
||||||
return Formula.pow(x, denominator).div(denominator);
|
return Formula.pow(x, denominator).div(denominator);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
|
@ -361,30 +502,42 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
|
||||||
} else if (hasVariable(rhs)) {
|
} else if (hasVariable(rhs)) {
|
||||||
return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
|
return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1));
|
return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertExp(value: DecimalSource, lhs: FormulaSource) {
|
export function invertExp(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.ln(value));
|
return lhs.invert(Decimal.ln(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.exp(x);
|
return Formula.exp(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tetrate(
|
export function tetrate(
|
||||||
|
@ -406,7 +559,8 @@ export function invertTetrate(
|
||||||
return base.invert(Decimal.ssqrt(value));
|
return base.invert(Decimal.ssqrt(value));
|
||||||
}
|
}
|
||||||
// Other params can't be inverted ATM
|
// Other params can't be inverted ATM
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function iteratedexp(
|
export function iteratedexp(
|
||||||
|
@ -434,7 +588,8 @@ export function invertIteratedExp(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Other params can't be inverted ATM
|
// Other params can't be inverted ATM
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function iteratedLog(
|
export function iteratedLog(
|
||||||
|
@ -458,7 +613,8 @@ export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: Formul
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Other params can't be inverted ATM
|
// Other params can't be inverted ATM
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
|
export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
|
||||||
|
@ -481,21 +637,24 @@ export function invertLayeradd(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Other params can't be inverted ATM
|
// Other params can't be inverted ATM
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
|
export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.pow(Math.E, value).times(value));
|
return lhs.invert(Decimal.pow(Math.E, value).times(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
|
export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.tetrate(value, 2));
|
return lhs.invert(Decimal.tetrate(value, 2));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
|
export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
|
||||||
|
@ -507,190 +666,262 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.asin(value));
|
return lhs.invert(Decimal.asin(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.cos(x).neg();
|
return Formula.cos(x).neg();
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertCos(value: DecimalSource, lhs: FormulaSource) {
|
export function invertCos(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.acos(value));
|
return lhs.invert(Decimal.acos(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.sin(x);
|
return Formula.sin(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertTan(value: DecimalSource, lhs: FormulaSource) {
|
export function invertTan(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.atan(value));
|
return lhs.invert(Decimal.atan(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.cos(x).ln().neg();
|
return Formula.cos(x).ln().neg();
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.sin(value));
|
return lhs.invert(Decimal.sin(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.asin(x)
|
return Formula.asin(x)
|
||||||
.times(x)
|
.times(x)
|
||||||
.add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
.add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.cos(value));
|
return lhs.invert(Decimal.cos(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.acos(x)
|
return Formula.acos(x)
|
||||||
.times(x)
|
.times(x)
|
||||||
.sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
.sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.tan(value));
|
return lhs.invert(Decimal.tan(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.atan(x)
|
return Formula.atan(x)
|
||||||
.times(x)
|
.times(x)
|
||||||
.sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
|
.sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.asinh(value));
|
return lhs.invert(Decimal.asinh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.cosh(x);
|
return Formula.cosh(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.acosh(value));
|
return lhs.invert(Decimal.acosh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.sinh(x);
|
return Formula.sinh(x);
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.atanh(value));
|
return lhs.invert(Decimal.atanh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.cosh(x).ln();
|
return Formula.cosh(x).ln();
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.sinh(value));
|
return lhs.invert(Decimal.sinh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt());
|
return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt());
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.cosh(value));
|
return lhs.invert(Decimal.cosh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.acosh(x)
|
return Formula.acosh(x)
|
||||||
.times(x)
|
.times(x)
|
||||||
.sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
|
.sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
|
export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
return lhs.invert(Decimal.tanh(value));
|
return lhs.invert(Decimal.tanh(value));
|
||||||
}
|
}
|
||||||
throw new Error("Could not invert due to no input being a variable");
|
console.error("Could not invert due to no input being a variable");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
|
||||||
if (hasVariable(lhs)) {
|
if (hasVariable(lhs)) {
|
||||||
|
if (!lhs.isIntegrable()) {
|
||||||
|
console.error("Could not integrate due to variable not being integrable");
|
||||||
|
return Formula.constant(0);
|
||||||
|
}
|
||||||
const x = lhs.getIntegralFormula(stack);
|
const x = lhs.getIntegralFormula(stack);
|
||||||
return Formula.atanh(x)
|
return Formula.atanh(x)
|
||||||
.times(x)
|
.times(x)
|
||||||
.add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
|
.add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
|
||||||
}
|
}
|
||||||
throw new Error("Could not integrate due to no input being a variable");
|
console.error("Could not integrate due to no input being a variable");
|
||||||
|
return Formula.constant(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPassthroughBinaryFormula(
|
export function createPassthroughBinaryFormula(
|
||||||
|
|
28
src/game/formulas/types.d.ts
vendored
28
src/game/formulas/types.d.ts
vendored
|
@ -1,32 +1,38 @@
|
||||||
import Formula from "game/formulas/formulas";
|
import { InternalFormula } from "game/formulas/formulas";
|
||||||
import { DecimalSource } from "util/bignum";
|
import { DecimalSource } from "util/bignum";
|
||||||
import { ProcessedComputable } from "util/computed";
|
import { ProcessedComputable } from "util/computed";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type GenericFormula = Formula<any>;
|
type GenericFormula = InternalFormula<any>;
|
||||||
type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
|
type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
|
||||||
type InvertibleFormula = GenericFormula & {
|
type InvertibleFormula = GenericFormula & {
|
||||||
invert: (value: DecimalSource) => DecimalSource;
|
invert: NonNullable<GenericFormula["invert"]>;
|
||||||
};
|
};
|
||||||
type IntegrableFormula = GenericFormula & {
|
type IntegrableFormula = InvertibleFormula & {
|
||||||
evaluateIntegral: (variable?: DecimalSource) => DecimalSource;
|
evaluateIntegral: NonNullable<GenericFormula["evaluateIntegral"]>;
|
||||||
|
getIntegralFormula: NonNullable<GenericFormula["getIntegralFormula"]>;
|
||||||
|
calculateConstantOfIntegration: NonNullable<GenericFormula["calculateConstantOfIntegration"]>;
|
||||||
};
|
};
|
||||||
type InvertibleIntegralFormula = GenericFormula & {
|
type InvertibleIntegralFormula = IntegrableFormula & {
|
||||||
invertIntegral: (value: DecimalSource) => DecimalSource;
|
invertIntegral: NonNullable<GenericFormula["invertIntegral"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EvaluateFunction<T> = (
|
type EvaluateFunction<T> = (
|
||||||
this: Formula<T>,
|
this: InternalFormula<T>,
|
||||||
...inputs: GuardedFormulasToDecimals<T>
|
...inputs: GuardedFormulasToDecimals<T>
|
||||||
) => DecimalSource;
|
) => DecimalSource;
|
||||||
type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
|
type InvertFunction<T> = (
|
||||||
|
this: InternalFormula<T>,
|
||||||
|
value: DecimalSource,
|
||||||
|
...inputs: T
|
||||||
|
) => DecimalSource;
|
||||||
type IntegrateFunction<T> = (
|
type IntegrateFunction<T> = (
|
||||||
this: Formula<T>,
|
this: InternalFormula<T>,
|
||||||
stack: SubstitutionStack | undefined,
|
stack: SubstitutionStack | undefined,
|
||||||
...inputs: T
|
...inputs: T
|
||||||
) => GenericFormula;
|
) => GenericFormula;
|
||||||
type SubstitutionFunction<T> = (
|
type SubstitutionFunction<T> = (
|
||||||
this: Formula<T>,
|
this: InternalFormula<T>,
|
||||||
variable: GenericFormula,
|
variable: GenericFormula,
|
||||||
...inputs: T
|
...inputs: T
|
||||||
) => GenericFormula;
|
) => GenericFormula;
|
||||||
|
|
|
@ -220,12 +220,14 @@ export function createLayer<T extends LayerOptions>(
|
||||||
addingLayers.push(id);
|
addingLayers.push(id);
|
||||||
persistentRefs[id] = new Set();
|
persistentRefs[id] = new Set();
|
||||||
layer.minimized = persistent(false, false);
|
layer.minimized = persistent(false, false);
|
||||||
Object.assign(layer, optionsFunc.call(layer as BaseLayer));
|
Object.assign(layer, optionsFunc.call(layer, layer as BaseLayer));
|
||||||
if (
|
if (
|
||||||
addingLayers[addingLayers.length - 1] == null ||
|
addingLayers[addingLayers.length - 1] == null ||
|
||||||
addingLayers[addingLayers.length - 1] !== id
|
addingLayers[addingLayers.length - 1] !== id
|
||||||
) {
|
) {
|
||||||
throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`;
|
throw new Error(
|
||||||
|
`Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
addingLayers.pop();
|
addingLayers.pop();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "components/common/modifiers.css";
|
import "components/common/modifiers.css";
|
||||||
import type { CoercableComponent } from "features/feature";
|
import type { CoercableComponent, OptionsFunc } from "features/feature";
|
||||||
import { jsx } from "features/feature";
|
import { jsx } from "features/feature";
|
||||||
import settings from "game/settings";
|
import settings from "game/settings";
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
|
@ -10,6 +10,8 @@ import { convertComputable } from "util/computed";
|
||||||
import { createLazyProxy } from "util/proxies";
|
import { createLazyProxy } from "util/proxies";
|
||||||
import { renderJSX } from "util/vue";
|
import { renderJSX } from "util/vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
|
import Formula from "./formulas/formulas";
|
||||||
|
import { FormulaSource, GenericFormula } from "./formulas/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that can be used to apply or unapply some modification to a number.
|
* An object that can be used to apply or unapply some modification to a number.
|
||||||
|
@ -21,7 +23,9 @@ export interface Modifier {
|
||||||
/** Applies some operation on the input and returns the result. */
|
/** Applies some operation on the input and returns the result. */
|
||||||
apply: (gain: DecimalSource) => DecimalSource;
|
apply: (gain: DecimalSource) => DecimalSource;
|
||||||
/** Reverses the operation applied by the apply property. Required by some features. */
|
/** Reverses the operation applied by the apply property. Required by some features. */
|
||||||
revert?: (gain: DecimalSource) => DecimalSource;
|
invert?: (gain: DecimalSource) => DecimalSource;
|
||||||
|
/** Get a formula for this modifier. Required by some features. */
|
||||||
|
getFormula?: (gain: FormulaSource) => GenericFormula;
|
||||||
/**
|
/**
|
||||||
* Whether or not this modifier should be considered enabled.
|
* Whether or not this modifier should be considered enabled.
|
||||||
* Typically for use with modifiers passed into {@link createSequentialModifier}.
|
* Typically for use with modifiers passed into {@link createSequentialModifier}.
|
||||||
|
@ -37,22 +41,22 @@ export interface 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).
|
* 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> = T extends undefined
|
export type ModifierFromOptionalParams<T, S> = undefined extends T
|
||||||
? S extends undefined
|
? undefined extends S
|
||||||
? Omit<WithRequired<Modifier, "revert">, "description" | "enabled">
|
? Omit<WithRequired<Modifier, "invert" | "getFormula">, "description" | "enabled">
|
||||||
: Omit<WithRequired<Modifier, "revert" | "enabled">, "description">
|
: Omit<WithRequired<Modifier, "invert" | "enabled" | "getFormula">, "description">
|
||||||
: S extends undefined
|
: undefined extends S
|
||||||
? Omit<WithRequired<Modifier, "revert" | "description">, "enabled">
|
? Omit<WithRequired<Modifier, "invert" | "description" | "getFormula">, "enabled">
|
||||||
: WithRequired<Modifier, "revert" | "enabled" | "description">;
|
: WithRequired<Modifier, "invert" | "enabled" | "description" | "getFormula">;
|
||||||
|
|
||||||
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
|
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
|
||||||
export interface AdditiveModifierOptions {
|
export interface AdditiveModifierOptions {
|
||||||
/** The amount to add to the input value. */
|
/** The amount to add to the input value. */
|
||||||
addend: Computable<DecimalSource>;
|
addend: Computable<DecimalSource>;
|
||||||
/** Description of what this modifier is doing. */
|
/** Description of what this modifier is doing. */
|
||||||
description?: Computable<CoercableComponent> | undefined;
|
description?: Computable<CoercableComponent>;
|
||||||
/** A computable that will be processed and passed directly into the returned modifier. */
|
/** A computable that will be processed and passed directly into the returned modifier. */
|
||||||
enabled?: Computable<boolean> | undefined;
|
enabled?: Computable<boolean>;
|
||||||
/** Determines if numbers larger or smaller than 0 should be displayed as red. */
|
/** Determines if numbers larger or smaller than 0 should be displayed as red. */
|
||||||
smallerIsBetter?: boolean;
|
smallerIsBetter?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -62,17 +66,21 @@ export interface AdditiveModifierOptions {
|
||||||
* @param optionsFunc Additive modifier options.
|
* @param optionsFunc Additive modifier options.
|
||||||
*/
|
*/
|
||||||
export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
||||||
optionsFunc: () => T
|
optionsFunc: OptionsFunc<T>
|
||||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const { addend, description, enabled, smallerIsBetter } = optionsFunc();
|
const { addend, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||||
|
feature,
|
||||||
|
feature
|
||||||
|
);
|
||||||
|
|
||||||
const processedAddend = convertComputable(addend);
|
const processedAddend = convertComputable(addend);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||||
return {
|
return {
|
||||||
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
|
apply: (gain: DecimalSource) => Decimal.add(gain, unref(processedAddend)),
|
||||||
revert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)),
|
invert: (gain: DecimalSource) => Decimal.sub(gain, unref(processedAddend)),
|
||||||
|
getFormula: (gain: FormulaSource) => Formula.add(gain, processedAddend),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description:
|
description:
|
||||||
description == null
|
description == null
|
||||||
|
@ -123,17 +131,21 @@ export interface MultiplicativeModifierOptions {
|
||||||
* @param optionsFunc Multiplicative modifier options.
|
* @param optionsFunc Multiplicative modifier options.
|
||||||
*/
|
*/
|
||||||
export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
|
export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
|
||||||
optionsFunc: () => T
|
optionsFunc: OptionsFunc<T>
|
||||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc();
|
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||||
|
feature,
|
||||||
|
feature
|
||||||
|
);
|
||||||
|
|
||||||
const processedMultiplier = convertComputable(multiplier);
|
const processedMultiplier = convertComputable(multiplier);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
const processedEnabled = enabled == null ? undefined : convertComputable(enabled);
|
||||||
return {
|
return {
|
||||||
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
|
apply: (gain: DecimalSource) => Decimal.times(gain, unref(processedMultiplier)),
|
||||||
revert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)),
|
invert: (gain: DecimalSource) => Decimal.div(gain, unref(processedMultiplier)),
|
||||||
|
getFormula: (gain: FormulaSource) => Formula.times(gain, processedMultiplier),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description:
|
description:
|
||||||
description == null
|
description == null
|
||||||
|
@ -185,11 +197,11 @@ export interface ExponentialModifierOptions {
|
||||||
* @param optionsFunc Exponential modifier options.
|
* @param optionsFunc Exponential modifier options.
|
||||||
*/
|
*/
|
||||||
export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||||
optionsFunc: () => T
|
optionsFunc: OptionsFunc<T>
|
||||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
||||||
optionsFunc();
|
optionsFunc.call(feature, feature);
|
||||||
|
|
||||||
const processedExponent = convertComputable(exponent);
|
const processedExponent = convertComputable(exponent);
|
||||||
const processedDescription = convertComputable(description);
|
const processedDescription = convertComputable(description);
|
||||||
|
@ -206,7 +218,7 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
revert: (gain: DecimalSource) => {
|
invert: (gain: DecimalSource) => {
|
||||||
let result = gain;
|
let result = gain;
|
||||||
if (supportLowNumbers) {
|
if (supportLowNumbers) {
|
||||||
result = Decimal.add(result, 1);
|
result = Decimal.add(result, 1);
|
||||||
|
@ -217,6 +229,10 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
getFormula: (gain: FormulaSource) =>
|
||||||
|
supportLowNumbers
|
||||||
|
? Formula.add(gain, 1).pow(processedExponent).sub(1)
|
||||||
|
: Formula.pow(gain, processedExponent),
|
||||||
enabled: processedEnabled,
|
enabled: processedEnabled,
|
||||||
description:
|
description:
|
||||||
description == null
|
description == null
|
||||||
|
@ -259,9 +275,9 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||||
*/
|
*/
|
||||||
export function createSequentialModifier<
|
export function createSequentialModifier<
|
||||||
T extends Modifier[],
|
T extends Modifier[],
|
||||||
S = T extends WithRequired<Modifier, "revert">[]
|
S = T extends WithRequired<Modifier, "invert">[]
|
||||||
? WithRequired<Modifier, "description" | "revert">
|
? WithRequired<Modifier, "description" | "invert">
|
||||||
: Omit<WithRequired<Modifier, "description">, "revert">
|
: Omit<WithRequired<Modifier, "description">, "invert">
|
||||||
>(modifiersFunc: () => T): S {
|
>(modifiersFunc: () => T): S {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(() => {
|
||||||
const modifiers = modifiersFunc();
|
const modifiers = modifiersFunc();
|
||||||
|
@ -271,24 +287,35 @@ export function createSequentialModifier<
|
||||||
modifiers
|
modifiers
|
||||||
.filter(m => unref(m.enabled) !== false)
|
.filter(m => unref(m.enabled) !== false)
|
||||||
.reduce((gain, modifier) => modifier.apply(gain), gain),
|
.reduce((gain, modifier) => modifier.apply(gain), gain),
|
||||||
revert: modifiers.every(m => m.revert != null)
|
invert: modifiers.every(m => m.invert != null)
|
||||||
? (gain: DecimalSource) =>
|
? (gain: DecimalSource) =>
|
||||||
modifiers
|
modifiers
|
||||||
.filter(m => unref(m.enabled) !== false)
|
.filter(m => unref(m.enabled) !== false)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
.reduceRight((gain, modifier) => modifier.revert!(gain), gain)
|
.reduceRight((gain, modifier) => modifier.invert!(gain), gain)
|
||||||
: undefined,
|
: undefined,
|
||||||
enabled: computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0),
|
getFormula: modifiers.every(m => m.getFormula != null)
|
||||||
description: jsx(() => (
|
? (gain: FormulaSource) =>
|
||||||
<>
|
modifiers
|
||||||
{(
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
modifiers
|
.reduce((acc, curr) => Formula.if(acc, curr.enabled ?? true,
|
||||||
.filter(m => unref(m.enabled) !== false)
|
acc => curr.getFormula!(acc), acc => acc), gain)
|
||||||
.map(m => unref(m.description))
|
: undefined,
|
||||||
.filter(d => d) as CoercableComponent[]
|
enabled: modifiers.some(m => m.enabled != null)
|
||||||
).map(renderJSX)}
|
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
|
||||||
</>
|
: undefined,
|
||||||
))
|
description: modifiers.some(m => m.description != null)
|
||||||
|
? jsx(() => (
|
||||||
|
<>
|
||||||
|
{(
|
||||||
|
modifiers
|
||||||
|
.filter(m => unref(m.enabled) !== false)
|
||||||
|
.map(m => unref(m.description))
|
||||||
|
.filter(d => d) as CoercableComponent[]
|
||||||
|
).map(renderJSX)}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
: undefined
|
||||||
};
|
};
|
||||||
}) as unknown as S;
|
}) as unknown as S;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { Ref, WritableComputedRef } from "vue";
|
||||||
import { computed, isReactive, isRef, ref } from "vue";
|
import { computed, isReactive, isRef, ref } from "vue";
|
||||||
import player from "./player";
|
import player from "./player";
|
||||||
import state from "./state";
|
import state from "./state";
|
||||||
|
import Formula from "./formulas/formulas";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A symbol used in {@link Persistent} objects.
|
* A symbol used in {@link Persistent} objects.
|
||||||
|
@ -46,6 +47,11 @@ export const SaveDataPath = Symbol("SaveDataPath");
|
||||||
*/
|
*/
|
||||||
export const CheckNaN = Symbol("CheckNaN");
|
export const CheckNaN = Symbol("CheckNaN");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A symbol used to flag objects that should not be checked for persistent values.
|
||||||
|
*/
|
||||||
|
export const SkipPersistence = Symbol("SkipPersistence");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a union of things that should be safely stringifiable without needing special processes or knowing what to load them in as.
|
* This is a union of things that should be safely stringifiable without needing special processes or knowing what to load them in as.
|
||||||
* - Decimals aren't allowed because we'd need to know to parse them back.
|
* - Decimals aren't allowed because we'd need to know to parse them back.
|
||||||
|
@ -110,12 +116,7 @@ function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T)
|
||||||
state.NaNPath = persistent[SaveDataPath];
|
state.NaNPath = persistent[SaveDataPath];
|
||||||
state.NaNPersistent = persistent as Persistent<DecimalSource>;
|
state.NaNPersistent = persistent as Persistent<DecimalSource>;
|
||||||
}
|
}
|
||||||
console.error(
|
console.error(`Attempted to save NaN value to ${persistent[SaveDataPath]?.join(".")}`);
|
||||||
`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;
|
persistent[PersistentState].value = value;
|
||||||
}
|
}
|
||||||
|
@ -196,12 +197,42 @@ export function isPersistent(value: unknown): value is Persistent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unwraps the non-persistent ref inside of persistent refs, to be passed to other features without duplicating values in the save data object.
|
* Unwraps the non-persistent ref inside of persistent refs, to be passed to other features without duplicating values in the save data object.
|
||||||
* @param persistent The persistent ref to unwrap
|
* @param persistent The persistent ref to unwrap, or an object to ignore all persistent refs within
|
||||||
*/
|
*/
|
||||||
export function noPersist<T extends Persistent<S>, S extends State>(
|
export function noPersist<T extends Persistent<S>, S extends State>(
|
||||||
persistent: T
|
persistent: T
|
||||||
): T[typeof NonPersistent] {
|
): T[typeof NonPersistent];
|
||||||
return persistent[NonPersistent];
|
export function noPersist<T extends object>(persistent: T): T;
|
||||||
|
export function noPersist<T extends Persistent<S>, S extends State>(persistent: T | object) {
|
||||||
|
// Check for proxy state so if it's a lazy proxy we don't evaluate it's function
|
||||||
|
// Lazy proxies are not persistent refs themselves, so we know we want to wrap them
|
||||||
|
return !(ProxyState in persistent) && NonPersistent in persistent
|
||||||
|
? persistent[NonPersistent]
|
||||||
|
: new Proxy(persistent, {
|
||||||
|
get(target, p) {
|
||||||
|
if (p === PersistentState) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (p === SkipPersistence) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return target[p as keyof typeof target];
|
||||||
|
},
|
||||||
|
set(target, key, value) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(target as Record<PropertyKey, any>)[key] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
has(target, key) {
|
||||||
|
if (key === PersistentState) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key == SkipPersistence) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Reflect.has(target, key);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,6 +257,9 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
Object.keys(obj).forEach(key => {
|
Object.keys(obj).forEach(key => {
|
||||||
let value = obj[key];
|
let value = obj[key];
|
||||||
if (value != null && typeof value === "object") {
|
if (value != null && typeof value === "object") {
|
||||||
|
if ((value as Record<PropertyKey, unknown>)[SkipPersistence] === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (ProxyState in value) {
|
if (ProxyState in value) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value = (value as any)[ProxyState] as object;
|
value = (value as any)[ProxyState] as object;
|
||||||
|
@ -253,8 +287,8 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
"."
|
"."
|
||||||
)}\` when it's already present at \`${value[SaveDataPath].join(
|
)}\` when it's already present at \`${value[SaveDataPath].join(
|
||||||
"."
|
"."
|
||||||
)}\`. This can cause unexpected behavior when loading saves between updates.`,
|
)}\`.`,
|
||||||
value
|
"This can cause unexpected behavior when loading saves between updates."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
value[SaveDataPath] = newPath;
|
value[SaveDataPath] = newPath;
|
||||||
|
@ -287,6 +321,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
!(value instanceof Decimal) &&
|
!(value instanceof Decimal) &&
|
||||||
|
!(value instanceof Formula) &&
|
||||||
!isRef(value) &&
|
!isRef(value) &&
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
!features.includes(value as { type: typeof Symbol })
|
!features.includes(value as { type: typeof Symbol })
|
||||||
|
@ -328,9 +363,9 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error(
|
console.error(
|
||||||
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
|
`Created persistent ref in ${layer.id} without registering it to the layer!`,
|
||||||
persistent,
|
"Make sure to include everything persistent in the returned object.\n\nCreated at:\n" +
|
||||||
"\nCreated at:\n" + persistent[StackTrace]
|
persistent[StackTrace]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
persistentRefs[layer.id].clear();
|
persistentRefs[layer.id].clear();
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
CoercableComponent,
|
CoercableComponent,
|
||||||
isVisible,
|
isVisible,
|
||||||
jsx,
|
jsx,
|
||||||
|
OptionsFunc,
|
||||||
Replace,
|
Replace,
|
||||||
setDefault,
|
setDefault,
|
||||||
Visibility
|
Visibility
|
||||||
|
@ -19,7 +20,7 @@ import { createLazyProxy } from "util/proxies";
|
||||||
import { joinJSX, renderJSX } from "util/vue";
|
import { joinJSX, renderJSX } from "util/vue";
|
||||||
import { computed, unref } from "vue";
|
import { computed, unref } from "vue";
|
||||||
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
|
||||||
import type { GenericFormula, InvertibleFormula } from "./formulas/types";
|
import type { GenericFormula } from "./formulas/types";
|
||||||
import { DefaultValue, Persistent } from "./persistence";
|
import { DefaultValue, Persistent } from "./persistence";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,7 +86,15 @@ 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.
|
* 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}
|
* @see {Formula}
|
||||||
*/
|
*/
|
||||||
spendResources?: Computable<boolean>;
|
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>;
|
||||||
/**
|
/**
|
||||||
* Pass-through to {@link Requirement.pay}. May be required for maximizing support.
|
* Pass-through to {@link Requirement.pay}. May be required for maximizing support.
|
||||||
* @see {@link cost} for restrictions on maximizing support.
|
* @see {@link cost} for restrictions on maximizing support.
|
||||||
|
@ -99,7 +108,8 @@ export type CostRequirement = Replace<
|
||||||
cost: ProcessedComputable<DecimalSource> | GenericFormula;
|
cost: ProcessedComputable<DecimalSource> | GenericFormula;
|
||||||
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
|
||||||
requiresPay: ProcessedComputable<boolean>;
|
requiresPay: ProcessedComputable<boolean>;
|
||||||
spendResources: ProcessedComputable<boolean>;
|
cumulativeCost: ProcessedComputable<boolean>;
|
||||||
|
canMaximize: ProcessedComputable<boolean>;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -108,10 +118,10 @@ export type CostRequirement = Replace<
|
||||||
* @param optionsFunc Cost requirement options.
|
* @param optionsFunc Cost requirement options.
|
||||||
*/
|
*/
|
||||||
export function createCostRequirement<T extends CostRequirementOptions>(
|
export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
optionsFunc: () => T
|
optionsFunc: OptionsFunc<T>
|
||||||
): CostRequirement {
|
): CostRequirement {
|
||||||
return createLazyProxy(() => {
|
return createLazyProxy(feature => {
|
||||||
const req = optionsFunc() as T & Partial<Requirement>;
|
const req = optionsFunc.call(feature, feature) as T & Partial<Requirement>;
|
||||||
|
|
||||||
req.partialDisplay = amount => (
|
req.partialDisplay = amount => (
|
||||||
<span
|
<span
|
||||||
|
@ -124,7 +134,12 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
{displayResource(
|
{displayResource(
|
||||||
req.resource,
|
req.resource,
|
||||||
req.cost instanceof Formula
|
req.cost instanceof Formula
|
||||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
? calculateCost(
|
||||||
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(req.cumulativeCost) as boolean,
|
||||||
|
unref(req.directSum) as number
|
||||||
|
)
|
||||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{req.resource.displayName}
|
{req.resource.displayName}
|
||||||
|
@ -136,7 +151,12 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
{displayResource(
|
{displayResource(
|
||||||
req.resource,
|
req.resource,
|
||||||
req.cost instanceof Formula
|
req.cost instanceof Formula
|
||||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
? calculateCost(
|
||||||
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(req.cumulativeCost) as boolean,
|
||||||
|
unref(req.directSum) as number
|
||||||
|
)
|
||||||
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
: unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{req.resource.displayName}
|
{req.resource.displayName}
|
||||||
|
@ -148,35 +168,62 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
||||||
processComputable(req as T, "cost");
|
processComputable(req as T, "cost");
|
||||||
processComputable(req as T, "requiresPay");
|
processComputable(req as T, "requiresPay");
|
||||||
setDefault(req, "requiresPay", true);
|
setDefault(req, "requiresPay", true);
|
||||||
processComputable(req as T, "spendResources");
|
processComputable(req as T, "cumulativeCost");
|
||||||
setDefault(req, "spendResources", true);
|
setDefault(req, "cumulativeCost", true);
|
||||||
|
processComputable(req as T, "maxBulkAmount");
|
||||||
|
setDefault(req, "maxBulkAmount", 1);
|
||||||
|
processComputable(req as T, "directSum");
|
||||||
setDefault(req, "pay", function (amount?: DecimalSource) {
|
setDefault(req, "pay", function (amount?: DecimalSource) {
|
||||||
const cost =
|
const cost =
|
||||||
req.cost instanceof Formula
|
req.cost instanceof Formula
|
||||||
? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
|
? calculateCost(
|
||||||
|
req.cost,
|
||||||
|
amount ?? 1,
|
||||||
|
unref(req.cumulativeCost) as boolean,
|
||||||
|
unref(req.directSum) as number
|
||||||
|
)
|
||||||
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
: unref(req.cost as ProcessedComputable<DecimalSource>);
|
||||||
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.canMaximize = req.cost instanceof Formula && req.cost.isInvertible();
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
if (req.canMaximize) {
|
if (req.cost instanceof Formula) {
|
||||||
req.requirementMet = calculateMaxAffordable(
|
req.requirementMet = calculateMaxAffordable(
|
||||||
req.cost as InvertibleFormula,
|
req.cost,
|
||||||
req.resource,
|
req.resource,
|
||||||
unref(req.spendResources) as boolean
|
req.cumulativeCost ?? true,
|
||||||
|
req.directSum,
|
||||||
|
req.maxBulkAmount
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
req.requirementMet = computed(() => {
|
req.requirementMet = computed(() =>
|
||||||
if (req.cost instanceof Formula) {
|
Decimal.gte(
|
||||||
return Decimal.gte(req.resource.value, req.cost.evaluate());
|
req.resource.value,
|
||||||
} else {
|
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||||
return Decimal.gte(
|
) ? 1 : 0
|
||||||
req.resource.value,
|
);
|
||||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return req as CostRequirement;
|
return req as CostRequirement;
|
||||||
|
@ -266,6 +313,7 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS
|
||||||
<div>
|
<div>
|
||||||
Costs:{" "}
|
Costs:{" "}
|
||||||
{joinJSX(
|
{joinJSX(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
withCosts.map(r => r.partialDisplay!(amount)),
|
withCosts.map(r => r.partialDisplay!(amount)),
|
||||||
<>, </>
|
<>, </>
|
||||||
)}
|
)}
|
||||||
|
@ -275,6 +323,7 @@ export function displayRequirements(requirements: Requirements, amount: DecimalS
|
||||||
<div>
|
<div>
|
||||||
Requires:{" "}
|
Requires:{" "}
|
||||||
{joinJSX(
|
{joinJSX(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
withoutCosts.map(r => r.partialDisplay!(amount)),
|
withoutCosts.map(r => r.partialDisplay!(amount)),
|
||||||
<>, </>
|
<>, </>
|
||||||
)}
|
)}
|
||||||
|
@ -305,7 +354,7 @@ export function payByDivision(this: CostRequirement, amount?: DecimalSource) {
|
||||||
? calculateCost(
|
? calculateCost(
|
||||||
this.cost,
|
this.cost,
|
||||||
amount ?? 1,
|
amount ?? 1,
|
||||||
unref(this.spendResources as ProcessedComputable<boolean> | undefined) ?? true
|
unref(this.cumulativeCost as ProcessedComputable<boolean> | undefined) ?? true
|
||||||
)
|
)
|
||||||
: unref(this.cost as ProcessedComputable<DecimalSource>);
|
: unref(this.cost as ProcessedComputable<DecimalSource>);
|
||||||
this.resource.value = Decimal.div(this.resource.value, cost);
|
this.resource.value = Decimal.div(this.resource.value, cost);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { DecimalSource } from "util/bignum";
|
import type { DecimalSource } from "util/bignum";
|
||||||
import { shallowReactive } from "vue";
|
import { reactive, shallowReactive } from "vue";
|
||||||
import type { Persistent } from "./persistence";
|
import type { Persistent } from "./persistence";
|
||||||
|
|
||||||
/** An object of global data that is not persistent. */
|
/** An object of global data that is not persistent. */
|
||||||
|
@ -12,6 +12,8 @@ export interface Transient {
|
||||||
NaNPath?: string[];
|
NaNPath?: string[];
|
||||||
/** The ref that was being set to NaN. */
|
/** The ref that was being set to NaN. */
|
||||||
NaNPersistent?: Persistent<DecimalSource>;
|
NaNPersistent?: Persistent<DecimalSource>;
|
||||||
|
/** List of errors that have occurred, to show the user. */
|
||||||
|
errors: Error[];
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -24,5 +26,6 @@ declare global {
|
||||||
export default window.state = shallowReactive<Transient>({
|
export default window.state = shallowReactive<Transient>({
|
||||||
lastTenTicks: [],
|
lastTenTicks: [],
|
||||||
hasNaN: false,
|
hasNaN: false,
|
||||||
NaNPath: []
|
NaNPath: [],
|
||||||
|
errors: reactive([])
|
||||||
});
|
});
|
||||||
|
|
29
src/main.ts
29
src/main.ts
|
@ -2,6 +2,7 @@ import "@fontsource/material-icons";
|
||||||
import App from "App.vue";
|
import App from "App.vue";
|
||||||
import projInfo from "data/projInfo.json";
|
import projInfo from "data/projInfo.json";
|
||||||
import "game/notifications";
|
import "game/notifications";
|
||||||
|
import state from "game/state";
|
||||||
import { load } from "util/save";
|
import { load } from "util/save";
|
||||||
import { useRegisterSW } from "virtual:pwa-register/vue";
|
import { useRegisterSW } from "virtual:pwa-register/vue";
|
||||||
import type { App as VueApp } from "vue";
|
import type { App as VueApp } from "vue";
|
||||||
|
@ -23,11 +24,32 @@ 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;
|
document.title = projInfo.title;
|
||||||
window.projInfo = projInfo;
|
window.projInfo = projInfo;
|
||||||
if (projInfo.id === "") {
|
if (projInfo.id === "") {
|
||||||
throw new Error(
|
console.error(
|
||||||
"Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"
|
"Project ID is empty!",
|
||||||
|
"Please select a unique ID for this project in /src/data/projInfo.json"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +65,9 @@ requestAnimationFrame(async () => {
|
||||||
|
|
||||||
// Create Vue
|
// Create Vue
|
||||||
const vue = (window.vue = createApp(App));
|
const vue = (window.vue = createApp(App));
|
||||||
|
vue.config.errorHandler = function (err, instance, info) {
|
||||||
|
console.error(err, info, instance);
|
||||||
|
};
|
||||||
globalBus.emit("setupVue", vue);
|
globalBus.emit("setupVue", vue);
|
||||||
vue.mount("#app");
|
vue.mount("#app");
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,11 @@ export function camelToTitle(camel: string): string {
|
||||||
return title;
|
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>(
|
export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
|
||||||
functionOrValue: ((...args: S) => T) | R
|
functionOrValue: ((...args: S) => T) | R
|
||||||
): functionOrValue is (...args: S) => T {
|
): functionOrValue is (...args: S) => T {
|
||||||
|
|
|
@ -31,14 +31,19 @@ export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown>
|
||||||
// Takes a function that returns an object and pretends to be that object
|
// Takes a function that returns an object and pretends to be that object
|
||||||
// Note that the object is lazily calculated
|
// Note that the object is lazily calculated
|
||||||
export function createLazyProxy<T extends object, S extends T>(
|
export function createLazyProxy<T extends object, S extends T>(
|
||||||
objectFunc: (baseObject: S) => T & S,
|
objectFunc: (this: S, baseObject: S) => T & S,
|
||||||
baseObject: S = {} as S
|
baseObject: S = {} as S
|
||||||
): T {
|
): T {
|
||||||
const obj: S & Partial<T> = baseObject;
|
const obj: S & Partial<T> = baseObject;
|
||||||
let calculated = false;
|
let calculated = false;
|
||||||
|
let calculating = false;
|
||||||
function calculateObj(): T {
|
function calculateObj(): T {
|
||||||
if (!calculated) {
|
if (!calculated) {
|
||||||
Object.assign(obj, objectFunc(obj));
|
if (calculating) {
|
||||||
|
console.error("Cyclical dependency detected. Cannot evaluate lazy proxy.");
|
||||||
|
}
|
||||||
|
calculating = true;
|
||||||
|
Object.assign(obj, objectFunc.call(obj, obj));
|
||||||
calculated = true;
|
calculated = true;
|
||||||
}
|
}
|
||||||
return obj as S & T;
|
return obj as S & T;
|
||||||
|
@ -73,7 +78,7 @@ export function createLazyProxy<T extends object, S extends T>(
|
||||||
},
|
},
|
||||||
getOwnPropertyDescriptor(target, key) {
|
getOwnPropertyDescriptor(target, key) {
|
||||||
if (!calculated) {
|
if (!calculated) {
|
||||||
Object.assign(obj, objectFunc(obj));
|
Object.assign(obj, objectFunc.call(obj, obj));
|
||||||
calculated = true;
|
calculated = true;
|
||||||
}
|
}
|
||||||
return Object.getOwnPropertyDescriptor(target, key);
|
return Object.getOwnPropertyDescriptor(target, key);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
unref,
|
unref,
|
||||||
watchEffect
|
watchEffect
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
import { camelToKebab } from "./common";
|
||||||
|
|
||||||
export function coerceComponent(
|
export function coerceComponent(
|
||||||
component: CoercableComponent,
|
component: CoercableComponent,
|
||||||
|
@ -241,3 +242,10 @@ export function trackHover(element: VueFeature): Ref<boolean> {
|
||||||
|
|
||||||
return isHovered;
|
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>);
|
||||||
|
}
|
||||||
|
|
518
tests/features/conversions.test.ts
Normal file
518
tests/features/conversions.test.ts
Normal file
|
@ -0,0 +1,518 @@
|
||||||
|
import {
|
||||||
|
createCumulativeConversion,
|
||||||
|
createIndependentConversion,
|
||||||
|
GenericConversion,
|
||||||
|
setupPassiveGeneration
|
||||||
|
} from "features/conversion";
|
||||||
|
import { createResource, Resource } from "features/resources/resource";
|
||||||
|
import { InvertibleIntegralFormula } from "game/formulas/types";
|
||||||
|
import { createLayer, GenericLayer } from "game/layers";
|
||||||
|
import Decimal from "util/bignum";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { ref, unref } from "vue";
|
||||||
|
import "../utils";
|
||||||
|
|
||||||
|
describe("Creating conversion", () => {
|
||||||
|
let baseResource: Resource;
|
||||||
|
let gainResource: Resource;
|
||||||
|
let formula: (x: InvertibleIntegralFormula) => InvertibleIntegralFormula;
|
||||||
|
beforeEach(() => {
|
||||||
|
baseResource = createResource(ref(40));
|
||||||
|
gainResource = createResource(ref(1));
|
||||||
|
formula = x => x.div(10).sqrt();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Cumulative conversion", () => {
|
||||||
|
describe("Calculates currentGain correctly", () => {
|
||||||
|
let conversion: GenericConversion;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(99);
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(99);
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(
|
||||||
|
Decimal.pow(100, 2).times(10)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(Decimal.pow(99, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(100, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(baseResource.value).compare_tolerance(0);
|
||||||
|
expect(gainResource.value).compare_tolerance(3);
|
||||||
|
});
|
||||||
|
describe("Obeys buy max", () => {
|
||||||
|
test("buyMax = false", () => {
|
||||||
|
const conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: false
|
||||||
|
}));
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(1);
|
||||||
|
});
|
||||||
|
test("buyMax = true", () => {
|
||||||
|
const conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("Spends correctly", () => {
|
||||||
|
const conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(baseResource.value).compare_tolerance(0);
|
||||||
|
});
|
||||||
|
test("Calls onConvert", () => {
|
||||||
|
const onConvert = vi.fn();
|
||||||
|
const conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(onConvert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Independent conversion", () => {
|
||||||
|
describe("Calculates currentGain correctly", () => {
|
||||||
|
let conversion: GenericConversion;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(99);
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(99);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(98);
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(
|
||||||
|
Decimal.pow(100, 2).times(10)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(Decimal.pow(99, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(
|
||||||
|
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;
|
||||||
|
beforeEach(() => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
test("Exactly enough", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10);
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just under", () => {
|
||||||
|
baseResource.value = Decimal.pow(100, 2).times(10).sub(1);
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(100, 2).times(10));
|
||||||
|
});
|
||||||
|
test("Just over", () => {
|
||||||
|
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(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(baseResource.value).compare_tolerance(0);
|
||||||
|
expect(gainResource.value).compare_tolerance(2);
|
||||||
|
});
|
||||||
|
describe("Obeys buy max", () => {
|
||||||
|
test("buyMax = false", () => {
|
||||||
|
const conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: false
|
||||||
|
}));
|
||||||
|
baseResource.value = 90;
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(1);
|
||||||
|
});
|
||||||
|
test("buyMax = true", () => {
|
||||||
|
const conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
buyMax: true
|
||||||
|
}));
|
||||||
|
baseResource.value = 90;
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("Spends correctly", () => {
|
||||||
|
const conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(baseResource.value).compare_tolerance(0);
|
||||||
|
});
|
||||||
|
test("Calls onConvert", () => {
|
||||||
|
const onConvert = vi.fn();
|
||||||
|
const conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(onConvert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Custom conversion", () => {
|
||||||
|
describe("Custom cumulative", () => {
|
||||||
|
let conversion: GenericConversion;
|
||||||
|
const convert = vi.fn();
|
||||||
|
const spend = vi.fn();
|
||||||
|
const onConvert = vi.fn();
|
||||||
|
beforeAll(() => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
currentGain() {
|
||||||
|
return 10;
|
||||||
|
},
|
||||||
|
actualGain() {
|
||||||
|
return 5;
|
||||||
|
},
|
||||||
|
currentAt() {
|
||||||
|
return 100;
|
||||||
|
},
|
||||||
|
nextAt() {
|
||||||
|
return 1000;
|
||||||
|
},
|
||||||
|
convert,
|
||||||
|
spend,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
test("Calculates currentGain correctly", () => {
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(10);
|
||||||
|
});
|
||||||
|
test("Calculates actualGain correctly", () => {
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(5);
|
||||||
|
});
|
||||||
|
test("Calculates currentAt correctly", () => {
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
test("Calculates nextAt correctly", () => {
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(1000);
|
||||||
|
});
|
||||||
|
test("Calls convert", () => {
|
||||||
|
conversion.convert();
|
||||||
|
expect(convert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
test("Calls spend and onConvert", () => {
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
spend,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(spend).toHaveBeenCalled();
|
||||||
|
expect(spend).toHaveBeenCalledWith(expect.compare_tolerance(2));
|
||||||
|
expect(onConvert).toHaveBeenCalled();
|
||||||
|
expect(onConvert).toHaveBeenCalledWith(expect.compare_tolerance(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Custom independent", () => {
|
||||||
|
let conversion: GenericConversion;
|
||||||
|
const convert = vi.fn();
|
||||||
|
const spend = vi.fn();
|
||||||
|
const onConvert = vi.fn();
|
||||||
|
beforeAll(() => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
currentGain() {
|
||||||
|
return 10;
|
||||||
|
},
|
||||||
|
actualGain() {
|
||||||
|
return 5;
|
||||||
|
},
|
||||||
|
currentAt() {
|
||||||
|
return 100;
|
||||||
|
},
|
||||||
|
nextAt() {
|
||||||
|
return 1000;
|
||||||
|
},
|
||||||
|
convert,
|
||||||
|
spend,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
test("Calculates currentGain correctly", () => {
|
||||||
|
expect(unref(conversion.currentGain)).compare_tolerance(10);
|
||||||
|
});
|
||||||
|
test("Calculates actualGain correctly", () => {
|
||||||
|
expect(unref(conversion.actualGain)).compare_tolerance(5);
|
||||||
|
});
|
||||||
|
test("Calculates currentAt correctly", () => {
|
||||||
|
expect(unref(conversion.currentAt)).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
test("Calculates nextAt correctly", () => {
|
||||||
|
expect(unref(conversion.nextAt)).compare_tolerance(1000);
|
||||||
|
});
|
||||||
|
test("Calls convert", () => {
|
||||||
|
conversion.convert();
|
||||||
|
expect(convert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
test("Calls spend and onConvert", () => {
|
||||||
|
conversion = createIndependentConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula,
|
||||||
|
spend,
|
||||||
|
onConvert
|
||||||
|
}));
|
||||||
|
conversion.convert();
|
||||||
|
expect(spend).toHaveBeenCalled();
|
||||||
|
expect(spend).toHaveBeenCalledWith(expect.compare_tolerance(1));
|
||||||
|
expect(onConvert).toHaveBeenCalled();
|
||||||
|
expect(onConvert).toHaveBeenCalledWith(expect.compare_tolerance(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Passive generation", () => {
|
||||||
|
let baseResource: Resource;
|
||||||
|
let gainResource: Resource;
|
||||||
|
let formula: (x: InvertibleIntegralFormula) => InvertibleIntegralFormula;
|
||||||
|
let conversion: GenericConversion;
|
||||||
|
let layer: GenericLayer;
|
||||||
|
beforeEach(() => {
|
||||||
|
baseResource = createResource(ref(10));
|
||||||
|
gainResource = createResource(ref(1));
|
||||||
|
formula = x => x.div(10).sqrt();
|
||||||
|
conversion = createCumulativeConversion(() => ({
|
||||||
|
baseResource,
|
||||||
|
gainResource,
|
||||||
|
formula
|
||||||
|
}));
|
||||||
|
layer = createLayer("dummy", () => ({ display: "" }));
|
||||||
|
});
|
||||||
|
test("Rate is 0", () => {
|
||||||
|
setupPassiveGeneration(layer, conversion, 0);
|
||||||
|
layer.emit("preUpdate", 1);
|
||||||
|
expect(gainResource.value).compare_tolerance(1);
|
||||||
|
});
|
||||||
|
test("Rate is 1", () => {
|
||||||
|
setupPassiveGeneration(layer, conversion);
|
||||||
|
layer.emit("preUpdate", 1);
|
||||||
|
expect(gainResource.value).compare_tolerance(2);
|
||||||
|
});
|
||||||
|
test("Rate is 100", () => {
|
||||||
|
setupPassiveGeneration(layer, conversion, () => 100);
|
||||||
|
layer.emit("preUpdate", 1);
|
||||||
|
expect(gainResource.value).compare_tolerance(101);
|
||||||
|
});
|
||||||
|
test("Obeys cap", () => {
|
||||||
|
setupPassiveGeneration(layer, conversion, 100, () => 100);
|
||||||
|
layer.emit("preUpdate", 1);
|
||||||
|
expect(gainResource.value).compare_tolerance(100);
|
||||||
|
});
|
||||||
|
});
|
14057
tests/game/__snapshots__/modifiers.test.ts.snap
Normal file
14057
tests/game/__snapshots__/modifiers.test.ts.snap
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,20 +2,24 @@ import { createResource, Resource } from "features/resources/resource";
|
||||||
import Formula, {
|
import Formula, {
|
||||||
calculateCost,
|
calculateCost,
|
||||||
calculateMaxAffordable,
|
calculateMaxAffordable,
|
||||||
printFormula,
|
|
||||||
unrefFormulaSource
|
unrefFormulaSource
|
||||||
} from "game/formulas/formulas";
|
} from "game/formulas/formulas";
|
||||||
import type { GenericFormula, InvertibleFormula } from "game/formulas/types";
|
import type { GenericFormula, IntegrableFormula, InvertibleFormula } from "game/formulas/types";
|
||||||
import Decimal, { DecimalSource, format } from "util/bignum";
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
import { beforeAll, describe, expect, test } from "vitest";
|
import { beforeAll, describe, expect, test } from "vitest";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import "../utils";
|
import "../utils";
|
||||||
|
import { InvertibleIntegralFormula } from "game/formulas/types";
|
||||||
|
|
||||||
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
|
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
|
||||||
|
|
||||||
const testValues = [-1, "0", Decimal.dOne] as const;
|
const testValues = [-2, "0", new Decimal(10.5)] as const;
|
||||||
|
|
||||||
const invertibleZeroParamFunctionNames = [
|
const invertibleZeroParamFunctionNames = [
|
||||||
|
"round",
|
||||||
|
"floor",
|
||||||
|
"ceil",
|
||||||
|
"trunc",
|
||||||
"neg",
|
"neg",
|
||||||
"recip",
|
"recip",
|
||||||
"log10",
|
"log10",
|
||||||
|
@ -48,10 +52,6 @@ const invertibleZeroParamFunctionNames = [
|
||||||
const nonInvertibleZeroParamFunctionNames = [
|
const nonInvertibleZeroParamFunctionNames = [
|
||||||
"abs",
|
"abs",
|
||||||
"sign",
|
"sign",
|
||||||
"round",
|
|
||||||
"floor",
|
|
||||||
"ceil",
|
|
||||||
"trunc",
|
|
||||||
"pLog10",
|
"pLog10",
|
||||||
"absLog10",
|
"absLog10",
|
||||||
"factorial",
|
"factorial",
|
||||||
|
@ -85,6 +85,10 @@ const integrableZeroParamFunctionNames = [
|
||||||
] as const;
|
] as const;
|
||||||
const nonIntegrableZeroParamFunctionNames = [
|
const nonIntegrableZeroParamFunctionNames = [
|
||||||
...nonInvertibleZeroParamFunctionNames,
|
...nonInvertibleZeroParamFunctionNames,
|
||||||
|
"round",
|
||||||
|
"floor",
|
||||||
|
"ceil",
|
||||||
|
"trunc",
|
||||||
"lambertw",
|
"lambertw",
|
||||||
"ssqrt"
|
"ssqrt"
|
||||||
] as const;
|
] as const;
|
||||||
|
@ -225,10 +229,17 @@ describe("Creating Formulas", () => {
|
||||||
expect(formula.hasVariable()).toBe(false));
|
expect(formula.hasVariable()).toBe(false));
|
||||||
test("Evaluates correctly", () =>
|
test("Evaluates correctly", () =>
|
||||||
expect(formula.evaluate()).compare_tolerance(expectedValue));
|
expect(formula.evaluate()).compare_tolerance(expectedValue));
|
||||||
test("Invert throws", () => expect(() => formula.invert(25)).toThrow());
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow());
|
/* @ts-ignore */
|
||||||
test("Invert integral throws", () =>
|
test("Invert errors", () => expect(() => formula.invert(25)).toLogError());
|
||||||
expect(() => formula.invertIntegral(25)).toThrow());
|
test("Integrate errors", () =>
|
||||||
|
// 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());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
testConstant("number", () => Formula.constant(10));
|
testConstant("number", () => Formula.constant(10));
|
||||||
|
@ -250,8 +261,10 @@ describe("Creating Formulas", () => {
|
||||||
// None of these formulas have variables, so they should all behave the same
|
// 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 marked as having a variable", () => expect(formula.hasVariable()).toBe(false));
|
||||||
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
|
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
|
||||||
test(`Formula throws if trying to invert`, () =>
|
test(`Formula errors if trying to invert`, () =>
|
||||||
expect(() => formula.invert(10)).toThrow());
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/* @ts-ignore */
|
||||||
|
expect(() => formula.invert(10)).toLogError());
|
||||||
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
|
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
|
||||||
test("Has a non-invertible integral", () =>
|
test("Has a non-invertible integral", () =>
|
||||||
expect(formula.isIntegralInvertible()).toBe(false));
|
expect(formula.isIntegralInvertible()).toBe(false));
|
||||||
|
@ -362,7 +375,7 @@ describe("Variables", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Inverting", () => {
|
describe("Inverting", () => {
|
||||||
let variable: GenericFormula;
|
let variable: IntegrableFormula;
|
||||||
let constant: GenericFormula;
|
let constant: GenericFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
variable = Formula.variable(10);
|
variable = Formula.variable(10);
|
||||||
|
@ -438,8 +451,8 @@ describe("Inverting", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Inverting calculates the value of the variable", () => {
|
describe("Inverting calculates the value of the variable", () => {
|
||||||
let variable: GenericFormula;
|
let variable: IntegrableFormula;
|
||||||
let constant: GenericFormula;
|
let constant: IntegrableFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
variable = Formula.variable(2);
|
variable = Formula.variable(2);
|
||||||
constant = Formula.constant(3);
|
constant = Formula.constant(3);
|
||||||
|
@ -449,7 +462,8 @@ describe("Inverting", () => {
|
||||||
test(`${name}(var, const).invert()`, () => {
|
test(`${name}(var, const).invert()`, () => {
|
||||||
const formula = Formula[name](variable, constant);
|
const formula = Formula[name](variable, constant);
|
||||||
const result = formula.evaluate();
|
const result = formula.evaluate();
|
||||||
expect(formula.invert(result)).compare_tolerance(2);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
expect(formula.invert!(result)).compare_tolerance(2);
|
||||||
});
|
});
|
||||||
if (name !== "layeradd") {
|
if (name !== "layeradd") {
|
||||||
test(`${name}(const, var).invert()`, () => {
|
test(`${name}(const, var).invert()`, () => {
|
||||||
|
@ -478,20 +492,27 @@ describe("Inverting", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Inverting nested formulas", () => {
|
test("Inverting nested formulas", () => {
|
||||||
const formula = Formula.add(variable, constant).times(constant);
|
const formula = Formula.add(variable, constant).times(constant).floor();
|
||||||
expect(formula.invert(100)).compare_tolerance(0);
|
expect(formula.invert(100)).compare_tolerance(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Inverting with non-invertible sections", () => {
|
describe("Inverting with non-invertible sections", () => {
|
||||||
const formula = Formula.add(variable, constant.ceil());
|
test("Non-invertible constant", () => {
|
||||||
expect(formula.isInvertible()).toBe(true);
|
const formula = Formula.add(variable, constant.sign());
|
||||||
expect(formula.invert(10)).compare_tolerance(0);
|
expect(formula.isInvertible()).toBe(true);
|
||||||
|
expect(() => formula.invert(10)).not.toLogError();
|
||||||
|
});
|
||||||
|
test("Non-invertible variable", () => {
|
||||||
|
const formula = Formula.add(variable.sign(), constant);
|
||||||
|
expect(formula.isInvertible()).toBe(false);
|
||||||
|
expect(() => formula.invert(10)).toLogError();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Integrating", () => {
|
describe("Integrating", () => {
|
||||||
let variable: GenericFormula;
|
let variable: IntegrableFormula;
|
||||||
let constant: GenericFormula;
|
let constant: IntegrableFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
variable = Formula.variable(ref(10));
|
variable = Formula.variable(ref(10));
|
||||||
constant = Formula.constant(10);
|
constant = Formula.constant(10);
|
||||||
|
@ -503,7 +524,7 @@ describe("Integrating", () => {
|
||||||
expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
|
expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
|
||||||
|
|
||||||
describe("Integrable functions marked as such", () => {
|
describe("Integrable functions marked as such", () => {
|
||||||
function checkFormula(formula: GenericFormula) {
|
function checkFormula(formula: IntegrableFormula) {
|
||||||
expect(formula.isIntegrable()).toBe(true);
|
expect(formula.isIntegrable()).toBe(true);
|
||||||
expect(() => formula.evaluateIntegral()).to.not.throw();
|
expect(() => formula.evaluateIntegral()).to.not.throw();
|
||||||
}
|
}
|
||||||
|
@ -608,13 +629,26 @@ describe("Integrating", () => {
|
||||||
|
|
||||||
test("Integrating nested complex formulas", () => {
|
test("Integrating nested complex formulas", () => {
|
||||||
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
||||||
expect(() => formula.evaluateIntegral()).toThrow();
|
expect(() => formula.evaluateIntegral()).toLogError();
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
test("Non-integrable variable", () => {
|
||||||
|
const formula = Formula.add(variable.ceil(), constant);
|
||||||
|
expect(formula.isIntegrable()).toBe(false);
|
||||||
|
expect(() => formula.evaluateIntegral()).toLogError();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Inverting integrals", () => {
|
describe("Inverting integrals", () => {
|
||||||
let variable: GenericFormula;
|
let variable: InvertibleIntegralFormula;
|
||||||
let constant: GenericFormula;
|
let constant: InvertibleIntegralFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
variable = Formula.variable(10);
|
variable = Formula.variable(10);
|
||||||
constant = Formula.constant(10);
|
constant = Formula.constant(10);
|
||||||
|
@ -626,9 +660,9 @@ describe("Inverting integrals", () => {
|
||||||
));
|
));
|
||||||
|
|
||||||
describe("Invertible Integral functions marked as such", () => {
|
describe("Invertible Integral functions marked as such", () => {
|
||||||
function checkFormula(formula: GenericFormula) {
|
function checkFormula(formula: InvertibleIntegralFormula) {
|
||||||
expect(formula.isIntegralInvertible()).toBe(true);
|
expect(formula.isIntegralInvertible()).toBe(true);
|
||||||
expect(() => formula.invertIntegral(10)).to.not.throw();
|
expect(() => formula.invertIntegral(10)).not.toLogError();
|
||||||
}
|
}
|
||||||
invertibleIntegralZeroPramFunctionNames.forEach(name => {
|
invertibleIntegralZeroPramFunctionNames.forEach(name => {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
|
@ -647,7 +681,7 @@ describe("Inverting integrals", () => {
|
||||||
test(`${name}(var, var) is marked as not having an invertible integral`, () => {
|
test(`${name}(var, var) is marked as not having an invertible integral`, () => {
|
||||||
const formula = Formula[name](variable, variable);
|
const formula = Formula[name](variable, variable);
|
||||||
expect(formula.isIntegralInvertible()).toBe(false);
|
expect(formula.isIntegralInvertible()).toBe(false);
|
||||||
expect(() => formula.invertIntegral(10)).to.throw();
|
expect(() => formula.invertIntegral(10)).toLogError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -703,7 +737,7 @@ describe("Inverting integrals", () => {
|
||||||
|
|
||||||
test("Inverting integral of nested complex formulas", () => {
|
test("Inverting integral of nested complex formulas", () => {
|
||||||
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
||||||
expect(() => formula.invertIntegral(100)).toThrow();
|
expect(() => formula.invertIntegral(100)).toLogError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -736,7 +770,7 @@ describe("Step-wise", () => {
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral()
|
Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral()
|
||||||
).toThrow();
|
).toLogError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Formula never marked as having an invertible integral", () => {
|
test("Formula never marked as having an invertible integral", () => {
|
||||||
|
@ -745,7 +779,7 @@ describe("Step-wise", () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10)
|
Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10)
|
||||||
).toThrow();
|
).toLogError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
||||||
|
@ -837,7 +871,7 @@ describe("Conditionals", () => {
|
||||||
);
|
);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral()
|
Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral()
|
||||||
).toThrow();
|
).toLogError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Formula never marked as having an invertible integral", () => {
|
test("Formula never marked as having an invertible integral", () => {
|
||||||
|
@ -846,7 +880,7 @@ describe("Conditionals", () => {
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10)
|
Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10)
|
||||||
).toThrow();
|
).toLogError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
||||||
|
@ -868,6 +902,26 @@ describe("Conditionals", () => {
|
||||||
Formula.if(variable, false, value => Formula.sqrt(value)).invert(10)
|
Formula.if(variable, false, value => Formula.sqrt(value)).invert(10)
|
||||||
).compare_tolerance(10));
|
).compare_tolerance(10));
|
||||||
});
|
});
|
||||||
|
describe("Evaluates correctly with condition false and else statement", () => {
|
||||||
|
test("Evaluates correctly", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(
|
||||||
|
constant,
|
||||||
|
false,
|
||||||
|
value => Formula.sqrt(value),
|
||||||
|
value => value.times(2)
|
||||||
|
).evaluate()
|
||||||
|
).compare_tolerance(20));
|
||||||
|
test("Inverts correctly with variable in input", () =>
|
||||||
|
expect(
|
||||||
|
Formula.if(
|
||||||
|
variable,
|
||||||
|
false,
|
||||||
|
value => Formula.sqrt(value),
|
||||||
|
value => value.times(2)
|
||||||
|
).invert(20)
|
||||||
|
).compare_tolerance(10));
|
||||||
|
});
|
||||||
|
|
||||||
describe("Evaluates correctly with condition true", () => {
|
describe("Evaluates correctly with condition true", () => {
|
||||||
test("Evaluates correctly", () =>
|
test("Evaluates correctly", () =>
|
||||||
|
@ -899,7 +953,7 @@ describe("Conditionals", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Custom Formulas", () => {
|
describe("Custom Formulas", () => {
|
||||||
let variable: GenericFormula;
|
let variable: InvertibleIntegralFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
variable = Formula.variable(1);
|
variable = Formula.variable(1);
|
||||||
});
|
});
|
||||||
|
@ -927,7 +981,7 @@ describe("Custom Formulas", () => {
|
||||||
evaluate: () => 6,
|
evaluate: () => 6,
|
||||||
invert: value => value
|
invert: value => value
|
||||||
}).invert(10)
|
}).invert(10)
|
||||||
).toThrow());
|
).toLogError());
|
||||||
test("One input inverts correctly", () =>
|
test("One input inverts correctly", () =>
|
||||||
expect(
|
expect(
|
||||||
new Formula({
|
new Formula({
|
||||||
|
@ -954,7 +1008,7 @@ describe("Custom Formulas", () => {
|
||||||
evaluate: () => 0,
|
evaluate: () => 0,
|
||||||
integrate: stack => variable
|
integrate: stack => variable
|
||||||
}).evaluateIntegral()
|
}).evaluateIntegral()
|
||||||
).toThrow());
|
).toLogError());
|
||||||
test("One input integrates correctly", () =>
|
test("One input integrates correctly", () =>
|
||||||
expect(
|
expect(
|
||||||
new Formula({
|
new Formula({
|
||||||
|
@ -981,7 +1035,7 @@ describe("Custom Formulas", () => {
|
||||||
evaluate: () => 0,
|
evaluate: () => 0,
|
||||||
integrate: stack => variable
|
integrate: stack => variable
|
||||||
}).invertIntegral(20)
|
}).invertIntegral(20)
|
||||||
).toThrow());
|
).toLogError());
|
||||||
test("One input inverts integral correctly", () =>
|
test("One input inverts integral correctly", () =>
|
||||||
expect(
|
expect(
|
||||||
new Formula({
|
new Formula({
|
||||||
|
@ -1001,7 +1055,7 @@ describe("Custom Formulas", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Formula as input", () => {
|
describe("Formula as input", () => {
|
||||||
let customFormula: GenericFormula;
|
let customFormula: InvertibleIntegralFormula;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
customFormula = new Formula({
|
customFormula = new Formula({
|
||||||
inputs: [variable],
|
inputs: [variable],
|
||||||
|
@ -1024,10 +1078,19 @@ describe("Buy Max", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
resource = createResource(ref(100000));
|
resource = createResource(ref(100000));
|
||||||
});
|
});
|
||||||
describe("Without spending", () => {
|
describe("Without cumulative cost", () => {
|
||||||
test("Throws on formula with non-invertible integral", () => {
|
test("Errors on calculating max affordable of non-invertible formula", () => {
|
||||||
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
|
const purchases = ref(1);
|
||||||
expect(() => maxAffordable.value).toThrow();
|
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();
|
||||||
});
|
});
|
||||||
test("Calculates max affordable and cost correctly", () => {
|
test("Calculates max affordable and cost correctly", () => {
|
||||||
const variable = Formula.variable(0);
|
const variable = Formula.variable(0);
|
||||||
|
@ -1038,11 +1101,32 @@ describe("Buy Max", () => {
|
||||||
Decimal.pow(1.05, 141).times(100)
|
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 spending", () => {
|
describe("With cumulative cost", () => {
|
||||||
test("Throws on non-invertible formula", () => {
|
test("Errors on calculating max affordable of non-invertible formula", () => {
|
||||||
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
|
const purchases = ref(1);
|
||||||
expect(() => maxAffordable.value).toThrow();
|
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();
|
||||||
});
|
});
|
||||||
test("Estimates max affordable and cost correctly with 0 purchases", () => {
|
test("Estimates max affordable and cost correctly with 0 purchases", () => {
|
||||||
const purchases = ref(0);
|
const purchases = ref(0);
|
||||||
|
@ -1100,7 +1184,7 @@ describe("Buy Max", () => {
|
||||||
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
||||||
).toBeLessThan(0.1);
|
).toBeLessThan(0.1);
|
||||||
});
|
});
|
||||||
test("Estimates max affordable and cost more accurately with summing last purchases", () => {
|
test("Estimates max affordable and cost more accurately with direct sum", () => {
|
||||||
const purchases = ref(1);
|
const purchases = ref(1);
|
||||||
const variable = Formula.variable(purchases);
|
const variable = Formula.variable(purchases);
|
||||||
const formula = Formula.pow(1.05, variable).times(100);
|
const formula = Formula.pow(1.05, variable).times(100);
|
||||||
|
@ -1127,7 +1211,7 @@ describe("Buy Max", () => {
|
||||||
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
||||||
).toBeLessThan(0.02);
|
).toBeLessThan(0.02);
|
||||||
});
|
});
|
||||||
test("Handles summing purchases when making few purchases", () => {
|
test("Handles direct sum when making few purchases", () => {
|
||||||
const purchases = ref(90);
|
const purchases = ref(90);
|
||||||
const variable = Formula.variable(purchases);
|
const variable = Formula.variable(purchases);
|
||||||
const formula = Formula.pow(1.05, variable).times(100);
|
const formula = Formula.pow(1.05, variable).times(100);
|
||||||
|
@ -1155,7 +1239,25 @@ describe("Buy Max", () => {
|
||||||
// Since we're summing all the purchases this should be equivalent
|
// Since we're summing all the purchases this should be equivalent
|
||||||
expect(calculatedCost).compare_tolerance(actualCost);
|
expect(calculatedCost).compare_tolerance(actualCost);
|
||||||
});
|
});
|
||||||
test("Handles summing purchases when over e308 purchases", () => {
|
test("Handles direct sum when making very few purchases", () => {
|
||||||
|
const purchases = ref(0);
|
||||||
|
const variable = Formula.variable(purchases);
|
||||||
|
const formula = variable.add(1);
|
||||||
|
const resource = createResource(ref(3));
|
||||||
|
const maxAffordable = calculateMaxAffordable(formula, resource, true);
|
||||||
|
expect(maxAffordable.value).compare_tolerance(2);
|
||||||
|
|
||||||
|
const actualCost = new Array(2)
|
||||||
|
.fill(null)
|
||||||
|
.reduce(
|
||||||
|
(acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
|
||||||
|
new Decimal(0)
|
||||||
|
);
|
||||||
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
||||||
|
// Since we're summing all the purchases this should be equivalent
|
||||||
|
expect(calculatedCost).compare_tolerance(actualCost);
|
||||||
|
});
|
||||||
|
test("Handles direct sum when over e308 purchases", () => {
|
||||||
resource.value = "1ee308";
|
resource.value = "1ee308";
|
||||||
const purchases = ref(0);
|
const purchases = ref(0);
|
||||||
const variable = Formula.variable(purchases);
|
const variable = Formula.variable(purchases);
|
||||||
|
@ -1166,5 +1268,10 @@ describe("Buy Max", () => {
|
||||||
expect(Decimal.isFinite(calculatedCost)).toBe(true);
|
expect(Decimal.isFinite(calculatedCost)).toBe(true);
|
||||||
resource.value = 100000;
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
411
tests/game/modifiers.test.ts
Normal file
411
tests/game/modifiers.test.ts
Normal file
|
@ -0,0 +1,411 @@
|
||||||
|
import { CoercableComponent, JSXFunction } from "features/feature";
|
||||||
|
import Formula, { printFormula } from "game/formulas/formulas";
|
||||||
|
import {
|
||||||
|
createAdditiveModifier,
|
||||||
|
createExponentialModifier,
|
||||||
|
createModifierSection,
|
||||||
|
createMultiplicativeModifier,
|
||||||
|
createSequentialModifier,
|
||||||
|
Modifier
|
||||||
|
} from "game/modifiers";
|
||||||
|
import Decimal, { DecimalSource } from "util/bignum";
|
||||||
|
import { WithRequired } from "util/common";
|
||||||
|
import { Computable } from "util/computed";
|
||||||
|
import { beforeAll, describe, expect, test } from "vitest";
|
||||||
|
import { Ref, ref, unref } from "vue";
|
||||||
|
import "../utils";
|
||||||
|
|
||||||
|
export type ModifierConstructorOptions = {
|
||||||
|
[S in "addend" | "multiplier" | "exponent"]: Computable<DecimalSource>;
|
||||||
|
} & {
|
||||||
|
description?: Computable<CoercableComponent>;
|
||||||
|
enabled?: Computable<boolean>;
|
||||||
|
smallerIsBetter?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function testModifiers<
|
||||||
|
T extends "addend" | "multiplier" | "exponent",
|
||||||
|
S extends ModifierConstructorOptions
|
||||||
|
>(
|
||||||
|
modifierConstructor: (optionsFunc: () => S) => WithRequired<Modifier, "invert" | "getFormula">,
|
||||||
|
property: T,
|
||||||
|
operation: (lhs: DecimalSource, rhs: DecimalSource) => DecimalSource
|
||||||
|
) {
|
||||||
|
// Util because adding [property] messes up typing
|
||||||
|
function createModifier(
|
||||||
|
value: Computable<DecimalSource>,
|
||||||
|
options: Partial<ModifierConstructorOptions> = {}
|
||||||
|
): WithRequired<Modifier, "invert" | "getFormula"> {
|
||||||
|
options[property] = value;
|
||||||
|
return modifierConstructor(() => options as S);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("operations", () => {
|
||||||
|
let modifier: WithRequired<Modifier, "invert" | "getFormula">;
|
||||||
|
beforeAll(() => {
|
||||||
|
modifier = createModifier(ref(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Applies correctly", () =>
|
||||||
|
expect(modifier.apply(10)).compare_tolerance(operation(10, 5)));
|
||||||
|
test("Inverts correctly", () =>
|
||||||
|
expect(modifier.invert(operation(10, 5))).compare_tolerance(10));
|
||||||
|
test("getFormula returns the right formula", () => {
|
||||||
|
const value = ref(10);
|
||||||
|
expect(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||||
|
`${operation.name}(x, 5.00)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies description correctly", () => {
|
||||||
|
test("without description", () => expect(createModifier(0).description).toBeUndefined());
|
||||||
|
test("with description", () => {
|
||||||
|
const desc = createModifier(0, { description: "test" }).description;
|
||||||
|
expect(desc).not.toBeUndefined();
|
||||||
|
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies enabled correctly", () => {
|
||||||
|
test("without enabled", () => expect(createModifier(0).enabled).toBeUndefined());
|
||||||
|
test("with enabled", () => {
|
||||||
|
const enabled = ref(false);
|
||||||
|
const modifier = createModifier(5, { enabled });
|
||||||
|
expect(modifier.enabled).toBe(enabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies smallerIsBetter correctly", () => {
|
||||||
|
describe("without smallerIsBetter false", () => {
|
||||||
|
test("negative value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(-5, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("zero value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(0, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("positive value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(5, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
});
|
||||||
|
describe("with smallerIsBetter true", () => {
|
||||||
|
test("negative value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(-5, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("zero value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(0, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("positive value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(5, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Additive Modifiers", () => testModifiers(createAdditiveModifier, "addend", Decimal.add));
|
||||||
|
describe("Multiplicative Modifiers", () =>
|
||||||
|
testModifiers(createMultiplicativeModifier, "multiplier", Decimal.mul));
|
||||||
|
describe("Exponential Modifiers", () =>
|
||||||
|
testModifiers(createExponentialModifier, "exponent", Decimal.pow));
|
||||||
|
|
||||||
|
describe("Sequential Modifiers", () => {
|
||||||
|
function createModifier(
|
||||||
|
value: Computable<DecimalSource>,
|
||||||
|
options: Partial<ModifierConstructorOptions> = {}
|
||||||
|
): WithRequired<Modifier, "invert" | "getFormula"> {
|
||||||
|
return createSequentialModifier(() => [
|
||||||
|
createAdditiveModifier(() => ({ ...options, addend: value })),
|
||||||
|
createMultiplicativeModifier(() => ({ ...options, multiplier: value })),
|
||||||
|
createExponentialModifier(() => ({ ...options, exponent: value }))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("operations", () => {
|
||||||
|
let modifier: WithRequired<Modifier, "invert" | "getFormula">;
|
||||||
|
beforeAll(() => {
|
||||||
|
modifier = createModifier(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Applies correctly", () =>
|
||||||
|
expect(modifier.apply(10)).compare_tolerance(Decimal.add(10, 5).times(5).pow(5)));
|
||||||
|
test("Inverts correctly", () =>
|
||||||
|
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(printFormula(modifier.getFormula(Formula.variable(value)))).toBe(
|
||||||
|
`pow(mul(add(x, 5.00), 5.00), 5.00)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies description correctly", () => {
|
||||||
|
test("without description", () => expect(createModifier(0).description).toBeUndefined());
|
||||||
|
test("with description", () => {
|
||||||
|
const desc = createModifier(0, { description: "test" }).description;
|
||||||
|
expect(desc).not.toBeUndefined();
|
||||||
|
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test("with both", () => {
|
||||||
|
const desc = createSequentialModifier(() => [
|
||||||
|
createAdditiveModifier(() => ({ addend: 0 })),
|
||||||
|
createMultiplicativeModifier(() => ({ multiplier: 0, description: "test" }))
|
||||||
|
]).description;
|
||||||
|
expect(desc).not.toBeUndefined();
|
||||||
|
expect((desc as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies enabled correctly", () => {
|
||||||
|
test("without enabled", () => expect(createModifier(0).enabled).toBeUndefined());
|
||||||
|
test("with enabled", () => {
|
||||||
|
const enabled = ref(false);
|
||||||
|
const modifier = createModifier(5, { enabled });
|
||||||
|
expect(modifier.enabled).not.toBeUndefined();
|
||||||
|
expect(unref(modifier.enabled)).toBe(false);
|
||||||
|
enabled.value = true;
|
||||||
|
expect(unref(modifier.enabled)).toBe(true);
|
||||||
|
});
|
||||||
|
test("with both", () => {
|
||||||
|
const enabled = ref(false);
|
||||||
|
const modifier = createSequentialModifier(() => [
|
||||||
|
createAdditiveModifier(() => ({ addend: 0 })),
|
||||||
|
createMultiplicativeModifier(() => ({ multiplier: 0, enabled }))
|
||||||
|
]);
|
||||||
|
expect(modifier.enabled).not.toBeUndefined();
|
||||||
|
// So long as one is true or undefined, enable should be true
|
||||||
|
expect(unref(modifier.enabled)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applies smallerIsBetter correctly", () => {
|
||||||
|
describe("without smallerIsBetter false", () => {
|
||||||
|
test("negative value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(-5, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("zero value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(0, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("positive value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(5, { description: "test", smallerIsBetter: false })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
});
|
||||||
|
describe("with smallerIsBetter true", () => {
|
||||||
|
test("negative value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(-5, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("zero value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(0, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("positive value", () =>
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
createModifier(5, { description: "test", smallerIsBetter: true })
|
||||||
|
.description as JSXFunction
|
||||||
|
)()
|
||||||
|
).toMatchSnapshot());
|
||||||
|
});
|
||||||
|
describe("with both", () => {
|
||||||
|
let value: Ref<DecimalSource>;
|
||||||
|
let modifier: Modifier;
|
||||||
|
beforeAll(() => {
|
||||||
|
value = ref(0);
|
||||||
|
modifier = createSequentialModifier(() => [
|
||||||
|
createAdditiveModifier(() => ({
|
||||||
|
addend: value,
|
||||||
|
description: "test",
|
||||||
|
smallerIsBetter: true
|
||||||
|
})),
|
||||||
|
createAdditiveModifier(() => ({
|
||||||
|
addend: value,
|
||||||
|
description: "test",
|
||||||
|
smallerIsBetter: false
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test("negative value", () => {
|
||||||
|
value.value = -5;
|
||||||
|
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test("zero value", () => {
|
||||||
|
value.value = 0;
|
||||||
|
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test("positive value", () => {
|
||||||
|
value.value = 5;
|
||||||
|
expect((modifier.description as JSXFunction)()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Create modifier sections", () => {
|
||||||
|
test("No optional values", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" }))
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("With subtitle", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "Subtitle",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" }))
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("With base", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" })),
|
||||||
|
base: 10
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("With unit", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" })),
|
||||||
|
unit: "/s"
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("With base", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" })),
|
||||||
|
baseText: "Based on"
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
test("With baseText", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" })),
|
||||||
|
baseText: "Based on"
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
describe("With smallerIsBetter", () => {
|
||||||
|
test("smallerIsBetter = false", () => {
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: -5,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: false
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: 0,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: false
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: 5,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: false
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test("smallerIsBetter = true", () => {
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: -5,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: true
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: 0,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: true
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
modifier: createAdditiveModifier(() => ({
|
||||||
|
addend: 5,
|
||||||
|
description: "Test Desc"
|
||||||
|
})),
|
||||||
|
smallerIsBetter: true
|
||||||
|
})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test("With everything", () =>
|
||||||
|
expect(
|
||||||
|
createModifierSection({
|
||||||
|
title: "Test",
|
||||||
|
subtitle: "Subtitle",
|
||||||
|
modifier: createAdditiveModifier(() => ({ addend: 5, description: "Test Desc" })),
|
||||||
|
base: 10,
|
||||||
|
unit: "/s",
|
||||||
|
baseText: "Based on",
|
||||||
|
smallerIsBetter: true
|
||||||
|
})
|
||||||
|
).toMatchSnapshot());
|
||||||
|
});
|
|
@ -11,20 +11,23 @@ import {
|
||||||
Requirement,
|
Requirement,
|
||||||
requirementsMet
|
requirementsMet
|
||||||
} from "game/requirements";
|
} from "game/requirements";
|
||||||
|
import Decimal from "util/bignum";
|
||||||
import { beforeAll, describe, expect, test } from "vitest";
|
import { beforeAll, describe, expect, test } from "vitest";
|
||||||
import { isRef, ref, unref } from "vue";
|
import { isRef, ref, unref } from "vue";
|
||||||
import "../utils";
|
import "../utils";
|
||||||
|
|
||||||
describe("Creating cost requirement", () => {
|
describe("Creating cost requirement", () => {
|
||||||
|
let resource: Resource;
|
||||||
|
beforeAll(() => {
|
||||||
|
resource = createResource(ref(10));
|
||||||
|
});
|
||||||
|
|
||||||
describe("Minimal requirement", () => {
|
describe("Minimal requirement", () => {
|
||||||
let resource: Resource;
|
|
||||||
let requirement: CostRequirement;
|
let requirement: CostRequirement;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
resource = createResource(ref(10));
|
|
||||||
requirement = createCostRequirement(() => ({
|
requirement = createCostRequirement(() => ({
|
||||||
resource,
|
resource,
|
||||||
cost: 10,
|
cost: 10
|
||||||
spendResources: false
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,22 +44,21 @@ describe("Creating cost requirement", () => {
|
||||||
});
|
});
|
||||||
test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
|
test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
|
||||||
test("requires pay", () => expect(requirement.requiresPay).toBe(true));
|
test("requires pay", () => expect(requirement.requiresPay).toBe(true));
|
||||||
test("does not spend resources", () => expect(requirement.spendResources).toBe(false));
|
test("does not spend resources", () => expect(requirement.cumulativeCost).toBe(true));
|
||||||
test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
|
test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Fully customized", () => {
|
describe("Fully customized", () => {
|
||||||
let resource: Resource;
|
|
||||||
let requirement: CostRequirement;
|
let requirement: CostRequirement;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
resource = createResource(ref(10));
|
|
||||||
requirement = createCostRequirement(() => ({
|
requirement = createCostRequirement(() => ({
|
||||||
resource,
|
resource,
|
||||||
cost: Formula.variable(resource).times(10),
|
cost: Formula.variable(resource).times(10),
|
||||||
visibility: Visibility.None,
|
visibility: Visibility.None,
|
||||||
requiresPay: false,
|
requiresPay: false,
|
||||||
maximize: true,
|
cumulativeCost: false,
|
||||||
spendResources: true,
|
maxBulkAmount: Decimal.dInf,
|
||||||
|
directSum: 5,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
pay() {}
|
pay() {}
|
||||||
}));
|
}));
|
||||||
|
@ -68,55 +70,149 @@ describe("Creating cost requirement", () => {
|
||||||
requirement.pay.length === 1);
|
requirement.pay.length === 1);
|
||||||
test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None));
|
test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None));
|
||||||
test("does not require pay", () => expect(requirement.requiresPay).toBe(false));
|
test("does not require pay", () => expect(requirement.requiresPay).toBe(false));
|
||||||
test("spends resources", () => expect(requirement.spendResources).toBe(true));
|
test("spends resources", () => expect(requirement.cumulativeCost).toBe(false));
|
||||||
test("can maximize", () => expect(unref(requirement.canMaximize)).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", () => {
|
test("Requirement met when meeting the cost", () => {
|
||||||
const resource = createResource(ref(10));
|
|
||||||
const requirement = createCostRequirement(() => ({
|
const requirement = createCostRequirement(() => ({
|
||||||
resource,
|
resource,
|
||||||
cost: 10,
|
cost: 10,
|
||||||
spendResources: false
|
cumulativeCost: false
|
||||||
}));
|
}));
|
||||||
expect(unref(requirement.requirementMet)).toBe(true);
|
expect(unref(requirement.requirementMet)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Requirement not met when not meeting the cost", () => {
|
test("Requirement not met when not meeting the cost", () => {
|
||||||
const resource = createResource(ref(10));
|
|
||||||
const requirement = createCostRequirement(() => ({
|
const requirement = createCostRequirement(() => ({
|
||||||
resource,
|
resource,
|
||||||
cost: 100,
|
cost: 100,
|
||||||
spendResources: false
|
cumulativeCost: false
|
||||||
}));
|
}));
|
||||||
expect(unref(requirement.requirementMet)).toBe(false);
|
expect(unref(requirement.requirementMet)).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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(() => ({
|
||||||
|
resource,
|
||||||
|
cost: () => 10
|
||||||
|
})).canMaximize
|
||||||
|
)
|
||||||
|
).toBe(false));
|
||||||
|
test("Non-invertible formula cannot maximize when max bulk amount is above direct sum", () =>
|
||||||
|
expect(
|
||||||
|
unref(
|
||||||
|
createCostRequirement(() => ({
|
||||||
|
resource,
|
||||||
|
cost: Formula.variable(resource).abs(),
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
|
})).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", () =>
|
||||||
|
expect(
|
||||||
|
unref(
|
||||||
|
createCostRequirement(() => ({
|
||||||
|
resource,
|
||||||
|
cost: Formula.variable(resource).lambertw(),
|
||||||
|
cumulativeCost: false,
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
|
})).canMaximize
|
||||||
|
)
|
||||||
|
).toBe(true));
|
||||||
|
test("Invertible formula cannot maximize if cumulativeCost is true", () =>
|
||||||
|
expect(
|
||||||
|
unref(
|
||||||
|
createCostRequirement(() => ({
|
||||||
|
resource,
|
||||||
|
cost: Formula.variable(resource).lambertw(),
|
||||||
|
cumulativeCost: true,
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
|
})).canMaximize
|
||||||
|
)
|
||||||
|
).toBe(false));
|
||||||
|
test("Integrable formula can maximize if cumulativeCost is false", () =>
|
||||||
|
expect(
|
||||||
|
unref(
|
||||||
|
createCostRequirement(() => ({
|
||||||
|
resource,
|
||||||
|
cost: Formula.variable(resource).pow(2),
|
||||||
|
cumulativeCost: false,
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
|
})).canMaximize
|
||||||
|
)
|
||||||
|
).toBe(true));
|
||||||
|
test("Integrable formula can maximize if cumulativeCost is true", () =>
|
||||||
|
expect(
|
||||||
|
unref(
|
||||||
|
createCostRequirement(() => ({
|
||||||
|
resource,
|
||||||
|
cost: Formula.variable(resource).pow(2),
|
||||||
|
cumulativeCost: true,
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
|
})).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));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Creating visibility requirement", () => {
|
test("Creating visibility requirement", () => {
|
||||||
test("Requirement met when visible", () => {
|
const visibility = ref<Visibility.None | Visibility.Visible | boolean>(Visibility.Visible);
|
||||||
const requirement = createVisibilityRequirement({ visibility: Visibility.Visible });
|
const requirement = createVisibilityRequirement({ visibility });
|
||||||
expect(unref(requirement.requirementMet)).toBe(true);
|
expect(unref(requirement.requirementMet)).toBe(true);
|
||||||
});
|
visibility.value = true;
|
||||||
|
expect(unref(requirement.requirementMet)).toBe(true);
|
||||||
test("Requirement not met when not visible", () => {
|
visibility.value = Visibility.None;
|
||||||
let requirement = createVisibilityRequirement({ visibility: Visibility.None });
|
expect(unref(requirement.requirementMet)).toBe(false);
|
||||||
expect(unref(requirement.requirementMet)).toBe(false);
|
visibility.value = false;
|
||||||
requirement = createVisibilityRequirement({ visibility: false });
|
expect(unref(requirement.requirementMet)).toBe(false);
|
||||||
expect(unref(requirement.requirementMet)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Creating boolean requirement", () => {
|
test("Creating boolean requirement", () => {
|
||||||
test("Requirement met when true", () => {
|
const req = ref(true);
|
||||||
const requirement = createBooleanRequirement(ref(true));
|
const requirement = createBooleanRequirement(req);
|
||||||
expect(unref(requirement.requirementMet)).toBe(true);
|
expect(unref(requirement.requirementMet)).toBe(true);
|
||||||
});
|
req.value = false;
|
||||||
|
expect(unref(requirement.requirementMet)).toBe(false);
|
||||||
test("Requirement not met when false", () => {
|
|
||||||
const requirement = createBooleanRequirement(ref(false));
|
|
||||||
expect(unref(requirement.requirementMet)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Checking all requirements met", () => {
|
describe("Checking all requirements met", () => {
|
||||||
|
@ -148,7 +244,7 @@ describe("Checking maximum levels of requirements met", () => {
|
||||||
createCostRequirement(() => ({
|
createCostRequirement(() => ({
|
||||||
resource: createResource(ref(10)),
|
resource: createResource(ref(10)),
|
||||||
cost: Formula.variable(0),
|
cost: Formula.variable(0),
|
||||||
spendResources: false
|
cumulativeCost: false
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
expect(maxRequirementsMet(requirements)).compare_tolerance(0);
|
expect(maxRequirementsMet(requirements)).compare_tolerance(0);
|
||||||
|
@ -160,7 +256,8 @@ describe("Checking maximum levels of requirements met", () => {
|
||||||
createCostRequirement(() => ({
|
createCostRequirement(() => ({
|
||||||
resource: createResource(ref(10)),
|
resource: createResource(ref(10)),
|
||||||
cost: Formula.variable(0),
|
cost: Formula.variable(0),
|
||||||
spendResources: false
|
cumulativeCost: false,
|
||||||
|
maxBulkAmount: Decimal.dInf
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
expect(maxRequirementsMet(requirements)).compare_tolerance(10);
|
expect(maxRequirementsMet(requirements)).compare_tolerance(10);
|
||||||
|
@ -173,12 +270,12 @@ test("Paying requirements", () => {
|
||||||
resource,
|
resource,
|
||||||
cost: 10,
|
cost: 10,
|
||||||
requiresPay: false,
|
requiresPay: false,
|
||||||
spendResources: false
|
cumulativeCost: false
|
||||||
}));
|
}));
|
||||||
const payment = createCostRequirement(() => ({
|
const payment = createCostRequirement(() => ({
|
||||||
resource,
|
resource,
|
||||||
cost: 10,
|
cost: 10,
|
||||||
spendResources: false
|
cumulativeCost: false
|
||||||
}));
|
}));
|
||||||
payRequirements([noPayment, payment]);
|
payRequirements([noPayment, payment]);
|
||||||
expect(resource.value).compare_tolerance(90);
|
expect(resource.value).compare_tolerance(90);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import Decimal, { DecimalSource, format } from "util/bignum";
|
import Decimal, { DecimalSource, format } from "util/bignum";
|
||||||
import { expect } from "vitest";
|
import { Mock, expect, vi } from "vitest";
|
||||||
|
|
||||||
interface CustomMatchers<R = unknown> {
|
interface CustomMatchers<R = unknown> {
|
||||||
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
|
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
|
||||||
|
toLogError(): R;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -36,5 +37,25 @@ expect.extend({
|
||||||
expected: format(expected),
|
expected: format(expected),
|
||||||
actual: format(received)
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue