Compare commits

...

75 commits

Author SHA1 Message Date
1b809a9550 Merge pull request 'Keep disabled modifiers when making formulas' (#56) from escapee/profectus-template:main into main
Reviewed-on: profectus/Profectus#56
2024-02-13 05:18:01 +00:00
Seth Posner
cf6265d8ce Keep disabled modifiers when making formulas 2024-02-12 07:58:39 -08:00
4f807aaf96 Merge pull request 'Add forgejo workflows support' (#53) from thepaperpilot/Profectus:main into main
Reviewed-on: profectus/Profectus#53
2024-02-11 19:43:37 +00:00
ffc42a5745 Merge pull request 'Fix reset button using currentAt when not yet gaining' (#25) from sethposner/profectus-template:main into main
Reviewed-on: profectus/Profectus#25
2023-11-13 22:29:49 +00:00
Seth Posner
8811996f64 Add tests confirming low-input conversion values 2023-11-13 14:10:00 -08:00
Seth Posner
7750a3368d Swap logic for nextAt display 2023-11-13 14:09:48 -08:00
2495dc9783 Implement forgejo actions workflows (#24)
Reviewed-on: profectus/Profectus#24
Co-authored-by: thepaperpilot <thepaperpilot@gmail.com>
Co-committed-by: thepaperpilot <thepaperpilot@gmail.com>
2023-11-05 17:00:28 +00:00
e66daad7a2 Merge pull request 'Implement isRednered' (#23) from thepaperpilot/Profectus:feature/is-rendered into main
Reviewed-on: profectus/Profectus#23
2023-11-05 16:59:37 +00:00
8065f8efa4 Approve install 2023-11-05 10:17:23 -06:00
e9283b5cca Update repos first 2023-11-05 10:16:38 -06:00
2c615ea524 Typo 2023-11-05 10:07:06 -06:00
953cd8047e Install rsync the normal way 2023-11-05 10:06:26 -06:00
766c600a70 Were the rsync logs wrong 2023-11-05 09:57:05 -06:00
c1d0b7eec6 Fully qualify >.< 2023-11-05 09:55:52 -06:00
52b500c9d8 setup rsync 2023-11-05 09:49:02 -06:00
aabb0a1bba Setup node 2023-11-05 09:13:56 -06:00
005bf5da9a Try alpine instead 2023-11-05 09:11:10 -06:00
7330a6bda4 Switch image to ubuntu:latest 2023-11-05 09:09:47 -06:00
65ff440e25 Fully qualify pages deploy action 2023-11-05 08:42:39 -06:00
acf1d24c15 Changed one of the wrong runs-on 2023-11-05 08:27:35 -06:00
d16bb55c3c Update runs-on 2023-11-05 07:46:30 -06:00
312cab1347 Rename workflow files 2023-11-05 07:42:28 -06:00
6d148da260 Add forgejo workflows 2023-11-05 07:28:43 -06:00
a5204106aa Forgot to comment the other signature 2023-10-11 21:44:02 -05:00
0cccf7aecc Add isRendered utility 2023-10-11 21:39:01 -05:00
d0fffd3b89 Update test.yml 2023-06-09 15:52:26 -05:00
3fe0311331 Try to allow actions to be run manually 2023-06-09 15:52:22 -05:00
eee5ac3e2d Fix passthroughs for inversions and make more operations invertible 2023-05-30 22:54:46 -05:00
9edda4d957 Make links ignore pointer events 2023-05-22 21:41:53 -05:00
6ad08c4052 Fix camelCase props not working on links 2023-05-21 17:27:29 -05:00
e0f1296b35 Rename "The Paper Pilot Community" to "Profectus & Friends" 2023-05-20 21:56:46 -05:00
c8ba77b89b Fix Direction.Left bars 2023-05-20 21:24:59 -05:00
63dcad4c12 Fix requirements tests 2023-05-20 08:30:07 -05:00
d6c9f95851 Fix error about pinnable tooltips 2023-05-20 08:28:27 -05:00
210c2290f0
Fix #9 2023-05-19 10:12:24 -05:00
e896fd84cf Change formula testing values to hopefully catch any other miscalculations 2023-05-19 08:04:20 -05:00
a5efed6e4a Add export save button to error component 2023-05-18 18:57:15 -05:00
3b7436ab89 Clarify progress is from 0 to 1 2023-05-18 18:57:10 -05:00
56279e3794 Fix thrown errors not appearing in console 2023-05-17 20:05:54 -05:00
5c1152460f Version Bump 2023-05-17 18:52:22 -05:00
73f20d6eb5 Merge branch 'feature/requirements-refactor' 2023-05-17 18:35:25 -05:00
7deacb41e1 Fixed tests 2023-05-17 08:23:10 -05:00
056aa4d2f7 Fix reset button showing currentAt if buyMax is true 2023-05-17 08:15:27 -05:00
3e23555b25 Fixed tests 2023-05-17 00:37:33 -05:00
a55f99daed Fix merge conflicts 2023-05-17 00:01:28 -05:00
ab3b180db8 Improve error handling 2023-05-16 23:49:25 -05:00
539282bef8 Improve error handling 2023-05-16 23:49:06 -05:00
d3a74da5ab Add utility for getting requirement that's met when a conversion can convert 2023-05-15 21:16:28 -05:00
500e412fdb Rebalance resource levels and implement portal costs 2023-05-15 21:10:52 -05:00
4e9fb1bc9b Fixed tests 2023-05-15 07:40:00 -05:00
bbe0aaa31e Fix directSum breaking formulas 2023-05-14 18:51:15 -05:00
f8095a9694 Made calculateMaxAffordable, calculateCost, and cost requirements interface a bit cleaner 2023-05-14 18:51:15 -05:00
Seth Posner
d7a2049ca2 Make effect decorator generic 2023-05-14 18:50:19 -05:00
006bfdf65d Make node links follow dragging 2023-05-14 18:30:59 -05:00
0991ef0865 Fix some persistence issues 2023-05-13 17:14:29 -05:00
d4f0069dd5 Fix repeatables not buying max correctly 2023-05-12 17:08:11 -05:00
9fa5ec971a Move amount check in bonus amount decorator to postConstruct 2023-05-12 10:59:59 -05:00
502fa99f5d Show selected node above others 2023-05-12 09:40:12 -05:00
8284baa1a0 Fix step-wise formulas causing issues with reactivity 2023-05-12 01:40:51 -05:00
213bdd6005 More board optimizations 2023-05-12 01:40:45 -05:00
2f3ae85eb1 Fix recursive rendering when panning 2023-05-12 01:40:22 -05:00
866685de2d Simplify TPS 2023-05-12 01:40:17 -05:00
cb4830e06b Fix reset typing 2023-05-10 00:35:16 -05:00
0e1915f511 Fix conversion utility showing currentAt instead of nextAt 2023-05-07 21:51:21 -05:00
4d7f03d543 Fix crash when calculating formula cost
Happened when spend resource was false and the formula was non-integrable, but the amount to buy were all going to be summed anyways
2023-05-05 19:11:57 -05:00
6786c27b89 Remove lint from readme 2023-05-03 15:51:27 -05:00
3413585c45 Fix bar misalignment on actions 2023-05-03 15:51:18 -05:00
dbdcf19b6d Fix actions not being constructible 2023-05-01 08:20:45 -05:00
8dd2cbe466 Make upgrade.canPurchase return false when already bought 2023-04-30 13:17:15 -05:00
04f14c17bd Fix extends undefined checks 2023-04-30 11:49:49 -05:00
5d17d67e00 Fix repeatable amount display 2023-04-30 11:49:47 -05:00
bffc27344a Fixed isInvertible and isIntegrable not working nested correctly 2023-04-30 11:49:43 -05:00
ff16397cc7 Fix bug with estimateTime not showing "Never" when it should 2023-04-27 22:49:47 -05:00
81058e10b4 Throw error if lazy proxies get cyclical 2023-04-27 20:41:20 -05:00
36fa4ece65 Fix mouse leave deselecting nodes 2023-04-25 23:51:48 -05:00
49 changed files with 1183 additions and 506 deletions

View 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.

View 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

View file

@ -3,6 +3,7 @@ on:
push:
branches:
- 'main'
workflow_dispatch:
jobs:
build-and-deploy:
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo

View file

@ -1,11 +1,11 @@
name: Build and Deploy
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
test:
runs-on: ubuntu-latest
steps:

View file

@ -6,6 +6,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
- **BREAKING** New requirements system

View file

@ -26,11 +26,6 @@ npm run build
npm run preview
```
### Lints and fixes files
```
npm run lint
```
### Runs the tests using vite-jest
```
npm run test

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "profectus",
"version": "0.6.0",
"version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "profectus",
"version": "0.6.0",
"version": "0.6.1",
"dependencies": {
"@fontsource/material-icons": "^4.5.4",
"@fontsource/roboto-mono": "^4.5.8",

View file

@ -1,6 +1,6 @@
{
"name": "profectus",
"version": "0.6.0",
"version": "0.6.1",
"private": true,
"scripts": {
"start": "vite",

View file

@ -1,18 +1,25 @@
<template>
<div id="modal-root" :style="theme" />
<div class="app" :style="theme" :class="{ useHeader }">
<Nav v-if="useHeader" />
<Game />
<TPS v-if="unref(showTPS)" />
<GameOverScreen />
<NaNScreen />
<component :is="gameComponent" />
</div>
<div v-if="appErrors.length > 0" class="error-container" :style="theme"><Error :errors="appErrors" /></div>
<template v-else>
<div id="modal-root" :style="theme" />
<div class="app" :style="theme" :class="{ useHeader }">
<Nav v-if="useHeader" />
<Game />
<TPS v-if="unref(showTPS)" />
<GameOverScreen />
<NaNScreen />
<component :is="gameComponent" />
</div>
</template>
</template>
<script setup lang="tsx">
import "@fontsource/roboto-mono";
import Error from "components/Error.vue";
import { jsx } from "features/feature";
import state from "game/state";
import { coerceComponent, render } from "util/vue";
import { CSSProperties, watch } from "vue";
import { computed, toRef, unref } from "vue";
import Game from "./components/Game.vue";
import GameOverScreen from "./components/GameOverScreen.vue";
@ -23,12 +30,11 @@ import projInfo from "./data/projInfo.json";
import themes from "./data/themes";
import settings, { gameComponents } from "./game/settings";
import "./main.css";
import "@fontsource/roboto-mono";
import type { CSSProperties } from "vue";
const useHeader = projInfo.useHeader;
const theme = computed(() => themes[settings.theme].variables as CSSProperties);
const showTPS = toRef(settings, "showTPS");
const appErrors = toRef(state, "errors");
const gameComponent = computed(() => {
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
@ -51,4 +57,15 @@ const gameComponent = computed(() => {
height: 100%;
color: var(--foreground);
}
.error-container {
background: var(--background);
overflow: auto;
width: 100%;
height: 100%;
}
.error-container > .error {
position: static;
}
</style>

135
src/components/Error.vue Normal file
View 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>

View file

@ -38,7 +38,7 @@
target="_blank"
>
<span class="material-icons info-modal-discord">discord</span>
The Paper Pilot Community
Profectus & Friends
</a>
</div>
<div>

View file

@ -1,5 +1,6 @@
<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
@ -28,12 +29,12 @@ import type { CoercableComponent } from "features/feature";
import type { FeatureNode } from "game/layers";
import player from "game/player";
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
import type { PropType, Ref } from "vue";
import { computed, defineComponent, toRefs, unref } from "vue";
import { PropType, Ref, computed, defineComponent, onErrorCaptured, ref, toRefs, unref } from "vue";
import Context from "./Context.vue";
import ErrorVue from "./Error.vue";
export default defineComponent({
components: { Context },
components: { Context, ErrorVue },
props: {
index: {
type: Number,
@ -77,13 +78,23 @@ export default defineComponent({
props.nodes.value = nodes;
}
const errors = ref<Error[]>([]);
onErrorCaptured((err, instance, info) => {
console.warn(`Error caught in "${props.name}" layer`, err, instance, info);
errors.value.push(
err instanceof Error ? (err as Error) : new Error(JSON.stringify(err))
);
return false;
});
return {
component,
minimizedComponent,
showGoBack,
updateNodes,
unref,
goBack
goBack,
errors
};
}
});

View file

@ -19,7 +19,7 @@
class="nan-modal-discord-link"
>
<span class="material-icons nan-modal-discord">discord</span>
{{ discordName || "The Paper Pilot Community" }}
{{ discordName || "Profectus & Friends" }}
</a>
</div>
<br />

View file

@ -15,9 +15,7 @@
<a :href="discordLink" target="_blank">{{ discordName }}</a>
</li>
<li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
>The Paper Pilot Community</a
>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
</li>
<li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
@ -82,9 +80,7 @@
<a :href="discordLink" target="_blank">{{ discordName }}</a>
</li>
<li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
>The Paper Pilot Community</a
>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
</li>
<li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>

View file

@ -1,17 +1,11 @@
<template>
<div class="tpsDisplay" v-if="!tps.isNan()">
TPS: {{ formatWhole(tps) }}
<transition name="fade"
><span v-if="showLow" class="low">{{ formatWhole(low) }}</span></transition
>
</div>
<div class="tpsDisplay" v-if="!tps.isNan()">TPS: {{ formatWhole(tps) }}</div>
</template>
<script setup lang="ts">
import state from "game/state";
import type { DecimalSource } from "util/bignum";
import Decimal, { formatWhole } from "util/bignum";
import { computed, ref, watchEffect } from "vue";
import { computed } from "vue";
const tps = computed(() =>
Decimal.div(
@ -19,20 +13,6 @@ const tps = computed(() =>
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
)
);
const lastTenFPS = ref<number[]>([]);
watchEffect(() => {
lastTenFPS.value.push(Math.round(tps.value.toNumber()));
if (lastTenFPS.value.length > 10) {
lastTenFPS.value = lastTenFPS.value.slice(1);
}
});
const low = computed(() =>
lastTenFPS.value.reduce<DecimalSource>((acc, curr) => Decimal.max(acc, curr), 0)
);
const showLow = computed(() => Decimal.sub(tps.value, low.value).gt(1));
</script>
<style scoped>

View file

@ -9,6 +9,7 @@ import { Resource, displayResource } from "features/resources/resource";
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
import { createTreeNode } from "features/trees/tree";
import type { GenericFormula } from "game/formulas/types";
import { BaseLayer } from "game/layers";
import type { Modifier } from "game/modifiers";
import type { Persistent } from "game/persistence";
import { DefaultValue, persistent } from "game/persistence";
@ -133,10 +134,10 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
{displayResource(
resetButton.conversion.baseResource,
unref(resetButton.conversion.buyMax) ||
Decimal.floor(unref(resetButton.conversion.actualGain)).neq(1)
? unref(resetButton.conversion.nextAt)
: unref(resetButton.conversion.currentAt)
!unref(resetButton.conversion.buyMax) &&
Decimal.gte(unref(resetButton.conversion.actualGain), 1)
? unref(resetButton.conversion.currentAt)
: unref(resetButton.conversion.nextAt)
)}{" "}
{resetButton.conversion.baseResource.displayName}
</div>
@ -437,7 +438,7 @@ export function estimateTime(
const currTarget = unref(processedTarget);
if (Decimal.gte(resource.value, currTarget)) {
return "Now";
} else if (Decimal.lt(currRate, 0)) {
} else if (Decimal.lte(currRate, 0)) {
return "Never";
}
return formatTime(Decimal.sub(currTarget, resource.value).div(currRate));
@ -459,7 +460,7 @@ export function createFormulaPreview(
const processedShowPreview = convertComputable(showPreview);
const processedPreviewAmount = convertComputable(previewAmount);
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 jsx(() => {
if (unref(processedShowPreview)) {
@ -485,3 +486,22 @@ export function createFormulaPreview(
return <>{formatSmall(formula.evaluate())}</>;
});
}
/**
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
* Note it will have a true value even if the feature is off screen.
* @param layer The layer the feature appears within
* @param id The ID of the feature
*/
export function isRendered(layer: BaseLayer, id: string): ComputedRef<boolean>;
/**
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
* Note it will have a true value even if the feature is off screen.
* @param layer The layer the feature appears within
* @param feature The feature that may be rendered
*/
export function isRendered(layer: BaseLayer, feature: { id: string }): ComputedRef<boolean>;
export function isRendered(layer: BaseLayer, idOrFeature: string | { id: string }) {
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
return computed(() => id in layer.nodes.value);
}

View file

@ -37,7 +37,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
color,
reset
}));
addTooltip(treeNode, {
const tooltip = addTooltip(treeNode, {
display: createResourceTooltip(points),
pinnable: true
});
@ -58,6 +58,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
name,
color,
points,
tooltip,
display: jsx(() => (
<>
<MainDisplay resource={points} color={color} />

View file

@ -160,6 +160,9 @@ export function createAchievement<T extends AchievementOptions>(
achievement.earned = earned;
achievement.complete = function () {
if (earned.value) {
return;
}
earned.value = true;
const genericAchievement = achievement as GenericAchievement;
genericAchievement.onComplete?.();

View file

@ -169,7 +169,6 @@ export function createAction<T extends ActionOptions>(
direction: Direction.Right,
width: 100,
height: 10,
style: "margin-top: 8px",
borderStyle: "border-color: black",
baseStyle: "margin-top: -1px",
progress: () => Decimal.div(progress.value, unref(genericAction.duration)),
@ -244,8 +243,9 @@ export function createAction<T extends ActionOptions>(
decorator.postConstruct?.(action);
}
const decoratedProps = decorators.reduce((current, next) =>
Object.assign(current, next.getGatheredProps?.(action))
const decoratedProps = decorators.reduce(
(current, next) => Object.assign(current, next.getGatheredProps?.(action)),
{}
);
action[GatherProps] = function (this: GenericAction) {
const {

View file

@ -120,7 +120,7 @@ export default defineComponent({
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
break;
case Direction.Left:
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
break;
case Direction.Default:
barStyle.clipPath = "inset(0% 50% 0% 0%)";

View file

@ -19,7 +19,7 @@
@touchstart="(e: TouchEvent) => mouseDown(e)"
@mouseup="() => endDragging(unref(draggingNode))"
@touchend.passive="() => endDragging(unref(draggingNode))"
@mouseleave="() => endDragging(unref(draggingNode))"
@mouseleave="() => endDragging(unref(draggingNode), true)"
>
<svg class="stage" width="100%" height="100%">
<g class="g1">
@ -28,7 +28,16 @@
v-for="link in unref(links) || []"
:key="`${link.startNode.id}-${link.endNode.id}`"
>
<BoardLinkVue :link="link" />
<BoardLinkVue
:link="link"
:dragging="unref(draggingNode)"
:dragged="
link.startNode === unref(draggingNode) ||
link.endNode === unref(draggingNode)
? dragged
: undefined
"
/>
</g>
</transition-group>
<transition-group name="grow" :duration="500" appear>
@ -38,10 +47,12 @@
:nodeType="types[node.type]"
:dragging="unref(draggingNode)"
:dragged="unref(draggingNode) === node ? dragged : undefined"
:hasDragged="hasDragged"
:receivingNode="unref(receivingNode)?.id === node.id"
:selectedNode="unref(selectedNode)"
:selectedAction="unref(selectedAction)"
:hasDragged="unref(draggingNode) == null ? false : hasDragged"
:receivingNode="unref(receivingNode) === node"
:isSelected="unref(selectedNode) === node"
:selectedAction="
unref(selectedNode) === node ? unref(selectedAction) : null
"
@mouseDown="mouseDown"
@endDragging="endDragging"
@clickAction="(actionId: string) => clickAction(node, actionId)"
@ -97,6 +108,10 @@ const stage = ref<any>(null);
const sortedNodes = computed(() => {
const nodes = props.nodes.value.slice();
if (props.selectedNode.value) {
const node = nodes.splice(nodes.indexOf(props.selectedNode.value), 1)[0];
nodes.push(node);
}
if (props.draggingNode.value) {
const node = nodes.splice(nodes.indexOf(props.draggingNode.value), 1)[0];
nodes.push(node);
@ -223,7 +238,7 @@ function drag(e: MouseEvent | TouchEvent) {
}
}
function endDragging(node: BoardNode | null) {
function endDragging(node: BoardNode | null, mouseLeave = false) {
if (props.draggingNode.value != null && props.draggingNode.value === node) {
if (props.receivingNode.value == null) {
props.draggingNode.value.position.x += Math.round(dragged.value.x / 25) * 25;
@ -241,7 +256,7 @@ function endDragging(node: BoardNode | null) {
}
props.setDraggingNode.value(null);
} else if (!hasDragged.value) {
} else if (!hasDragged.value && !mouseLeave) {
props.state.value.selectedNode = null;
props.state.value.selectedAction = null;
}

View file

@ -1,7 +1,7 @@
<template>
<line
class="link"
v-bind="link"
v-bind="linkProps"
:class="{ pulsing: link.pulsing }"
:x1="startPosition.x"
:y1="startPosition.y"
@ -11,36 +11,53 @@
</template>
<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";
const _props = defineProps<{
link: BoardNodeLink;
dragging: BoardNode | null;
dragged?: {
x: number;
y: number;
};
}>();
const props = toRefs(_props);
const startPosition = computed(() => {
const position = props.link.value.startNode.position;
const position = { ...props.link.value.startNode.position };
if (props.link.value.offsetStart) {
position.x += unref(props.link.value.offsetStart).x;
position.y += unref(props.link.value.offsetStart).y;
}
if (props.dragging?.value === props.link.value.startNode) {
position.x += props.dragged?.value?.x ?? 0;
position.y += props.dragged?.value?.y ?? 0;
}
return position;
});
const endPosition = computed(() => {
const position = props.link.value.endNode.position;
const position = { ...props.link.value.endNode.position };
if (props.link.value.offsetEnd) {
position.x += unref(props.link.value.offsetEnd).x;
position.y += unref(props.link.value.offsetEnd).y;
}
if (props.dragging?.value === props.link.value.endNode) {
position.x += props.dragged?.value?.x ?? 0;
position.y += props.dragged?.value?.y ?? 0;
}
return position;
});
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
</script>
<style scoped>
.link {
transition-duration: 0s;
pointer-events: none;
}
.link.pulsing {

View file

@ -160,7 +160,7 @@ const _props = defineProps<{
};
hasDragged?: boolean;
receivingNode?: boolean;
selectedNode: BoardNode | null;
isSelected: boolean;
selectedAction: GenericBoardNodeAction | null;
}>();
const props = toRefs(_props);
@ -170,7 +170,6 @@ const emit = defineEmits<{
(e: "clickAction", actionId: string): void;
}>();
const isSelected = computed(() => unref(props.selectedNode) === unref(props.node));
const isDraggable = computed(() =>
getNodeProperty(props.nodeType.value.draggable, unref(props.node))
);
@ -211,7 +210,7 @@ const shape = computed(() => getNodeProperty(props.nodeType.value.shape, unref(p
const title = computed(() => getNodeProperty(props.nodeType.value.title, unref(props.node)));
const label = computed(
() =>
(isSelected.value
(props.isSelected.value
? unref(props.selectedAction) &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getNodeProperty(unref(props.selectedAction)!.tooltip, unref(props.node))

View file

@ -104,7 +104,7 @@ export interface NodeTypeOptions {
shape: NodeComputable<Shape>;
/** Whether the node can accept another node being dropped upon it. */
canAccept?: NodeComputable<boolean, [BoardNode]>;
/** The progress value of the node. */
/** The progress value of the node, from 0 to 1. */
progress?: NodeComputable<number>;
/** How the progress should be displayed on the node. */
progressDisplay?: NodeComputable<ProgressDisplay>;

View file

@ -52,8 +52,6 @@ export interface ChallengeOptions {
reset?: GenericReset;
/** The requirement(s) to complete this challenge. */
requirements: Requirements;
/** Whether or not completing this challenge should grant multiple completions if requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
maximize?: Computable<boolean>;
/** The maximum number of times the challenge can be completed. */
completionLimit?: Computable<DecimalSource>;
/** Shows a marker on the corner of the feature. */
@ -124,7 +122,6 @@ export type Challenge<T extends ChallengeOptions> = Replace<
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
canStart: GetComputableTypeWithDefault<T["canStart"], true>;
requirements: GetComputableType<T["requirements"]>;
maximize: GetComputableType<T["maximize"]>;
completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
classes: GetComputableType<T["classes"]>;
@ -210,10 +207,7 @@ export function createChallenge<T extends ChallengeOptions>(
}
};
challenge.canComplete = computed(() =>
Decimal.max(
maxRequirementsMet((challenge as GenericChallenge).requirements),
unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1
)
maxRequirementsMet((challenge as GenericChallenge).requirements)
);
challenge.complete = function (remainInChallenge?: boolean) {
const genericChallenge = challenge as GenericChallenge;
@ -254,7 +248,6 @@ export function createChallenge<T extends ChallengeOptions>(
processComputable(challenge as T, "canStart");
setDefault(challenge, "canStart", true);
processComputable(challenge as T, "maximize");
processComputable(challenge as T, "completionLimit");
setDefault(challenge, "completionLimit", 1);
processComputable(challenge as T, "mark");

View file

@ -1,4 +1,4 @@
import type { OptionsFunc, Replace } from "features/feature";
import type { CoercableComponent, OptionsFunc, Replace } from "features/feature";
import { setDefault } from "features/feature";
import type { Resource } from "features/resources/resource";
import Formula from "game/formulas/formulas";
@ -12,6 +12,7 @@ import { createLazyProxy } from "util/proxies";
import type { Ref } from "vue";
import { computed, unref } from "vue";
import { GenericDecorator } from "./decorators/common";
import { createBooleanRequirement } from "game/requirements";
/** An object that configures a {@link Conversion}. */
export interface ConversionOptions {
@ -292,3 +293,20 @@ export function setupPassiveGeneration(
}
});
}
/**
* Creates requirement that is met when the conversion hits a specified gain amount
* @param conversion The conversion to check the gain amount of
* @param minGainAmount The minimum gain amount that must be met for the requirement to be met
*/
export function createCanConvertRequirement(
conversion: GenericConversion,
minGainAmount: Computable<DecimalSource> = 1,
display?: CoercableComponent
) {
const computedMinGainAmount = convertComputable(minGainAmount);
return createBooleanRequirement(
() => Decimal.gte(unref(conversion.actualGain), unref(computedMinGainAmount)),
display
);
}

View file

@ -69,14 +69,12 @@ export const bonusAmountDecorator: Decorator<
BaseBonusAmountFeature,
GenericBonusAmountFeature
> = {
preConstruct(feature) {
postConstruct(feature) {
if (feature.amount === undefined) {
console.error(
`Decorated feature ${feature.id} does not contain the required 'amount' property"`
);
}
},
postConstruct(feature) {
processComputable(feature, "bonusAmount");
if (feature.totalAmount === undefined) {
feature.totalAmount = computed(() =>

View file

@ -27,8 +27,8 @@ export type Decorator<
export type GenericDecorator = Decorator<unknown>;
export interface EffectFeatureOptions {
effect: Computable<unknown>;
export interface EffectFeatureOptions<T = unknown> {
effect: Computable<T>;
}
export type EffectFeature<T extends EffectFeatureOptions> = Replace<
@ -36,9 +36,9 @@ export type EffectFeature<T extends EffectFeatureOptions> = Replace<
{ effect: GetComputableType<T["effect"]> }
>;
export type GenericEffectFeature = Replace<
export type GenericEffectFeature<T = unknown> = Replace<
EffectFeature<EffectFeatureOptions>,
{ effect: ProcessedComputable<unknown> }
{ effect: ProcessedComputable<T> }
>;
/**

View file

@ -2,7 +2,7 @@
<line
stroke-width="15px"
stroke="white"
v-bind="link"
v-bind="linkProps"
:x1="startPosition.x"
:y1="startPosition.y"
:x2="endPosition.x"
@ -13,6 +13,7 @@
<script setup lang="ts">
import type { Link } from "features/links/links";
import type { FeatureNode } from "game/layers";
import { kebabifyObject } from "util/vue";
import { computed, toRefs } from "vue";
const _props = defineProps<{
@ -54,4 +55,6 @@ const endPosition = computed(() => {
}
return position;
});
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
</script>

View file

@ -67,8 +67,6 @@ export interface RepeatableOptions {
mark?: Computable<boolean | string>;
/** Toggles a smaller design for the feature. */
small?: Computable<boolean>;
/** Whether or not clicking this repeatable should attempt to maximize amount based on the requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */
maximize?: Computable<boolean>;
/** The display to use for this repeatable. */
display?: Computable<RepeatableDisplay>;
}
@ -87,7 +85,6 @@ export interface BaseRepeatable {
canClick: ProcessedComputable<boolean>;
/**
* How much amount can be increased by, or 1 if unclickable.
* Capped at 1 if {@link RepeatableOptions.maximize} is false.
**/
amountToIncrease: Ref<DecimalSource>;
/** A function that gets called when this repeatable is clicked. */
@ -111,7 +108,6 @@ export type Repeatable<T extends RepeatableOptions> = Replace<
style: GetComputableType<T["style"]>;
mark: GetComputableType<T["mark"]>;
small: GetComputableType<T["small"]>;
maximize: GetComputableType<T["maximize"]>;
display: Ref<CoercableComponent>;
}
>;
@ -162,7 +158,8 @@ export function createRepeatable<T extends RepeatableOptions>(
)
),
requiresPay: false,
visibility: Visibility.None
visibility: Visibility.None,
canMaximize: true
} as const;
const visibilityRequirement = createVisibilityRequirement(repeatable as GenericRepeatable);
if (isArray(repeatable.requirements)) {
@ -194,9 +191,7 @@ export function createRepeatable<T extends RepeatableOptions>(
return currClasses;
});
repeatable.amountToIncrease = computed(() =>
unref((repeatable as GenericRepeatable).maximize)
? maxRequirementsMet(repeatable.requirements)
: 1
Decimal.clampMin(maxRequirementsMet(repeatable.requirements), 1)
);
repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
const onClick = repeatable.onClick;
@ -205,8 +200,12 @@ export function createRepeatable<T extends RepeatableOptions>(
if (!unref(genericRepeatable.canClick)) {
return;
}
payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease));
genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
const amountToIncrease = unref(repeatable.amountToIncrease) ?? 1;
payRequirements(repeatable.requirements, amountToIncrease);
genericRepeatable.amount.value = Decimal.add(
genericRepeatable.amount.value,
amountToIncrease
);
onClick?.(event);
};
processComputable(repeatable as T, "display");
@ -235,12 +234,10 @@ export function createRepeatable<T extends RepeatableOptions>(
{currDisplay.showAmount === false ? null : (
<div>
<br />
joinJSX(
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>,
{unref(genericRepeatable.limit) !== Decimal.dInf ? (
<>Amount: {formatWhole(genericRepeatable.amount.value)}</>
{Decimal.isFinite(unref(genericRepeatable.limit)) ? (
<> / {formatWhole(unref(genericRepeatable.limit))}</>
) : undefined}
)
</div>
)}
{currDisplay.effectDisplay == null ? null : (
@ -271,7 +268,6 @@ export function createRepeatable<T extends RepeatableOptions>(
processComputable(repeatable as T, "style");
processComputable(repeatable as T, "mark");
processComputable(repeatable as T, "small");
processComputable(repeatable as T, "maximize");
for (const decorator of decorators) {
decorator.postConstruct?.(repeatable);

View file

@ -1,8 +1,9 @@
import type { OptionsFunc, Replace } from "features/feature";
import { getUniqueID } from "features/feature";
import { globalBus } from "game/events";
import Formula from "game/formulas/formulas";
import type { BaseLayer } from "game/layers";
import type { NonPersistent, Persistent } from "game/persistence";
import { NonPersistent, Persistent, SkipPersistence } from "game/persistence";
import { DefaultValue, persistent } from "game/persistence";
import type { Unsubscribe } from "nanoevents";
import Decimal from "util/bignum";
@ -19,7 +20,7 @@ export const ResetType = Symbol("Reset");
*/
export interface ResetOptions {
/** List of things to reset. Can include objects which will be recursed over for persistent values. */
thingsToReset: Computable<Record<string, unknown>[]>;
thingsToReset: Computable<unknown[]>;
/** A function that is called when the reset is performed. */
onReset?: VoidFunction;
}
@ -61,7 +62,15 @@ export function createReset<T extends ResetOptions>(
reset.reset = function () {
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) {
const persistent = obj as NonPersistent;
persistent.value = persistent[DefaultValue];

View file

@ -151,8 +151,7 @@ export function createTabFamily<T extends TabFamilyOptions>(
optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
): TabFamily<T> {
if (Object.keys(tabs).length === 0) {
console.warn("Cannot create tab family with 0 tabs");
throw new Error("Cannot create tab family with 0 tabs");
console.error("Cannot create tab family with 0 tabs");
}
const selected = persistent(Object.keys(tabs)[0], false);

View file

@ -52,7 +52,7 @@ export interface BaseTooltip {
export type Tooltip<T extends TooltipOptions> = Replace<
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;
display: GetComputableType<T["display"]>;
classes: GetComputableType<T["classes"]>;
@ -95,18 +95,6 @@ export function addTooltip<T extends TooltipOptions>(
}
nextTick(() => {
if (options.pinnable) {
if ("pinned" in element) {
console.error(
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
);
options.pinnable = false;
deletePersistent(options.pinned as Persistent<boolean>);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(element as any).pinned = options.pinned;
}
}
const elementComponent = element[Component];
element[Component] = TooltipComponent as GenericComponent;
const elementGatherProps = element[GatherProps].bind(element);

View file

@ -137,7 +137,9 @@ export function createUpgrade<T extends UpgradeOptions>(
upgrade.bought = bought;
Object.assign(upgrade, decoratedData);
upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));
upgrade.canPurchase = computed(
() => !bought.value && requirementsMet(upgrade.requirements)
);
upgrade.purchase = function () {
const genericUpgrade = upgrade as GenericUpgrade;
if (!unref(genericUpgrade.canPurchase)) {

View file

@ -2,7 +2,7 @@ import { Resource } from "features/resources/resource";
import { NonPersistent } from "game/persistence";
import Decimal, { DecimalSource, format } from "util/bignum";
import { Computable, ProcessedComputable, convertComputable } from "util/computed";
import { ComputedRef, Ref, computed, ref, unref } from "vue";
import { Ref, computed, ref, unref } from "vue";
import * as ops from "./operations";
import type {
EvaluateFunction,
@ -104,7 +104,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties<T> {
if (inputs.length !== 1) {
throw new Error("Evaluate function is required if inputs is not length 1");
console.error("Evaluate function is required if inputs is not length 1");
}
return {
inputs: inputs as T,
@ -124,11 +124,14 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined;
const invertible = variable?.isInvertible() ?? false;
const integrable = variable?.isIntegrable() ?? false;
return {
inputs,
internalEvaluate: evaluate,
internalInvert: invert,
internalIntegrate: integrate,
internalInvert: invertible ? invert : undefined,
internalIntegrate: integrable ? integrate : undefined,
internalIntegrateInner: integrateInner,
applySubstitution,
innermostVariable,
@ -226,15 +229,16 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
start: Computable<DecimalSource>,
formulaModifier: (value: InvertibleIntegralFormula) => GenericFormula
) {
const lhsRef = ref<DecimalSource>(0);
const formula = formulaModifier(Formula.variable(lhsRef));
const formula = formulaModifier(Formula.variable(0));
const processedStart = convertComputable(start);
function evalStep(lhs: DecimalSource) {
if (Decimal.lt(lhs, unref(processedStart))) {
return lhs;
}
lhsRef.value = Decimal.sub(lhs, unref(processedStart));
return Decimal.add(formula.evaluate(), unref(processedStart));
return Decimal.add(
formula.evaluate(Decimal.sub(lhs, unref(processedStart))),
unref(processedStart)
);
}
function invertStep(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs) && formula.isInvertible()) {
@ -246,7 +250,8 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
}
return lhs.invert(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;
}
return new Formula({
inputs: [value],
@ -290,7 +295,8 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
!formula.isInvertible() ||
(elseFormula != null && !elseFormula.isInvertible())
) {
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;
}
if (unref(processedCondition)) {
return lhs.invert(formula.invert(value));
@ -339,19 +345,35 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
public static sgn = InternalFormula.sign;
public static round(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.round });
return new Formula({
inputs: [value],
evaluate: Decimal.round,
invert: ops.invertPassthrough
});
}
public static floor(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.floor });
return new Formula({
inputs: [value],
evaluate: Decimal.floor,
invert: ops.invertPassthrough
});
}
public static ceil(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.ceil });
return new Formula({
inputs: [value],
evaluate: Decimal.ceil,
invert: ops.invertPassthrough
});
}
public static trunc(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.trunc });
return new Formula({
inputs: [value],
evaluate: Decimal.trunc,
invert: ops.invertPassthrough
});
}
public static add<T extends GenericFormula>(value: T, other: FormulaSource): T;
@ -453,7 +475,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
return new Formula({
inputs: [value, min, max],
evaluate: Decimal.clamp,
invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]>
invert: ops.invertPassthrough
});
}
@ -1256,7 +1278,8 @@ export default class Formula<
} else if (this.inputs.length === 1 && this.hasVariable()) {
return value;
}
throw new Error("Cannot invert non-invertible formula");
console.error("Cannot invert non-invertible formula");
return 0;
}
/**
@ -1266,7 +1289,8 @@ export default class Formula<
*/
evaluateIntegral(variable?: DecimalSource): DecimalSource {
if (!this.isIntegrable()) {
throw new Error("Cannot evaluate integral of formula without integral");
console.error("Cannot evaluate integral of formula without integral");
return 0;
}
return this.getIntegralFormula().evaluate(variable);
}
@ -1278,7 +1302,8 @@ export default class Formula<
*/
invertIntegral(value: DecimalSource): DecimalSource {
if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
throw new Error("Cannot invert integral of formula without invertible integral");
console.error("Cannot invert integral of formula without invertible integral");
return 0;
}
return (this.getIntegralFormula() as InvertibleFormula).invert(value);
}
@ -1305,7 +1330,8 @@ export default class Formula<
// We're the complex operation of this formula
stack = [];
if (this.internalIntegrate == null) {
throw new Error("Cannot integrate formula with non-integrable operation");
console.error("Cannot integrate formula with non-integrable operation");
return Formula.constant(0);
}
let value = this.internalIntegrate.call(this, stack, ...this.inputs);
stack.forEach(func => (value = func(value)));
@ -1325,14 +1351,16 @@ export default class Formula<
) {
this.integralFormula = this;
} else {
throw new Error("Cannot integrate formula without variable");
console.error("Cannot integrate formula without variable");
return Formula.constant(0);
}
}
return this.integralFormula;
} else {
// "Inner" part of the formula
if (this.applySubstitution == null) {
throw new Error("Cannot have two complex operations in an integrable formula");
console.error("Cannot have two complex operations in an integrable formula");
return Formula.constant(0);
}
stack.push((variable: GenericFormula) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -1349,7 +1377,8 @@ export default class Formula<
) {
return this;
} else {
throw new Error("Cannot integrate formula without variable");
console.error("Cannot integrate formula without variable");
return Formula.constant(0);
}
}
}
@ -1396,58 +1425,70 @@ export function printFormula(formula: FormulaSource): string {
}
/**
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link spendResources} is changed to false, the calculation will be much faster with higher numbers.
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
* @param formula The formula to use for calculating buy max from
* @param resource The resource used when purchasing (is only read from)
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
* @param summedPurchases How many of the most expensive purchases should be manually summed for better accuracy. If unspecified uses 10 when spending resources and 0 when not
* @param cumulativeCost Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
* @param directSum How many of the most expensive purchases should be manually summed for better accuracy. If unspecified uses 10 when spending resources and 0 when not
* @param maxBulkAmount Cap on how many can be purchased at once. If equal to 1 or lte to {@link directSum} then the formula does not need to be invertible. Defaults to Infinity.
*/
export function calculateMaxAffordable(
formula: InvertibleFormula,
formula: GenericFormula,
resource: Resource,
spendResources?: true,
summedPurchases?: number
): ComputedRef<DecimalSource>;
export function calculateMaxAffordable(
formula: InvertibleIntegralFormula,
resource: Resource,
spendResources: Computable<boolean>,
summedPurchases?: number
): ComputedRef<DecimalSource>;
export function calculateMaxAffordable(
formula: InvertibleFormula,
resource: Resource,
spendResources: Computable<boolean> = true,
summedPurchases?: number
cumulativeCost: Computable<boolean> = true,
directSum?: Computable<number>,
maxBulkAmount: Computable<DecimalSource> = Decimal.dInf
) {
const computedSpendResources = convertComputable(spendResources);
const computedCumulativeCost = convertComputable(cumulativeCost);
const computedDirectSum = convertComputable(directSum);
const computedmaxBulkAmount = convertComputable(maxBulkAmount);
return computed(() => {
let affordable;
if (unref(computedSpendResources)) {
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
throw new Error(
"Cannot calculate max affordable of formula with non-invertible integral"
);
}
affordable = Decimal.floor(
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
).sub(unref(formula.innermostVariable) ?? 0);
if (summedPurchases == null) {
summedPurchases = 10;
}
} else {
const maxBulkAmount = unref(computedmaxBulkAmount);
if (Decimal.eq(maxBulkAmount, 1)) {
return Decimal.gte(resource.value, formula.evaluate()) ? Decimal.dOne : Decimal.dZero;
}
const cumulativeCost = unref(computedCumulativeCost);
const directSum = unref(computedDirectSum) ?? (cumulativeCost ? 10 : 0);
let affordable: DecimalSource = 0;
if (Decimal.gt(maxBulkAmount, directSum)) {
if (!formula.isInvertible()) {
throw new Error("Cannot calculate max affordable of non-invertible formula");
console.error(
"Cannot calculate max affordable of non-invertible formula with more maxBulkAmount than directSum"
);
return 0;
}
affordable = Decimal.floor(formula.invert(resource.value));
if (summedPurchases == null) {
summedPurchases = 0;
if (cumulativeCost) {
if (!formula.isIntegralInvertible()) {
console.error(
"Cannot calculate max affordable of formula with non-invertible integral"
);
return 0;
}
affordable = Decimal.floor(
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
).sub(unref(formula.innermostVariable) ?? 0);
} else {
affordable = Decimal.floor(formula.invert(resource.value));
}
}
if (summedPurchases > 0 && Decimal.lt(calculateCost(formula, affordable, true, 0), 1e308)) {
affordable = affordable.sub(summedPurchases).clampMin(0);
let summedCost = calculateCost(formula, affordable, true, 0);
while (true) {
affordable = Decimal.clampMax(affordable, maxBulkAmount);
if (directSum > 0) {
const preSumAffordable = affordable;
affordable = Decimal.sub(affordable, directSum).clampMin(0);
let summedCost;
if (cumulativeCost) {
summedCost = calculateCost(formula as InvertibleFormula, affordable, true, 0);
} else {
summedCost = formula.evaluate(
Decimal.add(unref(formula.innermostVariable) ?? 0, affordable)
);
}
while (
Decimal.lt(affordable, maxBulkAmount) &&
Decimal.lt(affordable, Number.MAX_SAFE_INTEGER) &&
Decimal.add(preSumAffordable, 1).gte(affordable)
) {
const nextCost = formula.evaluate(
affordable.add(unref(formula.innermostVariable) ?? 0)
);
@ -1464,65 +1505,78 @@ export function calculateMaxAffordable(
}
/**
* Utility for calculating the cost of a formula for a given amount of purchases. If {@link spendResources} is changed to false, the calculation will be much faster with higher numbers.
* Utility for calculating the cost of a formula for a given amount of purchases. If {@link cumulativeCost} is changed to false, the calculation will be much faster with higher numbers.
* @param formula The formula to use for calculating buy max from
* @param amountToBuy The amount of purchases to calculate the cost for
* @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
* @param summedPurchases How many purchases to manually sum for improved accuracy. If not specified, defaults to 10 when spending resources and 0 when not
* @param cumulativeCost Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
* @param directSum How many purchases to manually sum for improved accuracy. If not specified, defaults to 10 when cost is cumulative and 0 when not
*/
export function calculateCost(
formula: InvertibleFormula,
amountToBuy: DecimalSource,
spendResources?: true,
summedPurchases?: number
cumulativeCost?: true,
directSum?: number
): DecimalSource;
export function calculateCost(
formula: InvertibleIntegralFormula,
amountToBuy: DecimalSource,
spendResources: boolean,
summedPurchases?: number
cumulativeCost: boolean,
directSum?: number
): DecimalSource;
export function calculateCost(
formula: InvertibleFormula,
amountToBuy: DecimalSource,
spendResources = true,
summedPurchases?: number
cumulativeCost = true,
directSum?: number
) {
let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
if (spendResources) {
if (!formula.isIntegrable()) {
throw new Error(
"Cannot calculate cost with spending resources of non-integrable formula"
);
}
const targetValue = newValue;
newValue = newValue
.sub(summedPurchases ?? 10)
.clampMin(unref(formula.innermostVariable) ?? 0);
let cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
if (targetValue.gt(1e308)) {
// Too large of a number for summedPurchases to make a difference,
// just get the cost and multiply by summed purchases
return cost.add(Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue)));
}
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
cost = cost.add(formula.evaluate(i));
}
return cost;
} else {
const targetValue = newValue;
newValue = newValue
.sub(summedPurchases ?? 0)
.clampMin(unref(formula.innermostVariable) ?? 0);
let cost = formula.evaluate(newValue);
if (targetValue.gt(1e308)) {
// Too large of a number for summedPurchases to make a difference,
// just get the cost and multiply by summed purchases
return Decimal.sub(targetValue, newValue).add(1).times(cost);
}
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
cost = Decimal.add(cost, formula.evaluate(i));
}
return cost;
// Single purchase
if (Decimal.eq(amountToBuy, 1)) {
return formula.evaluate();
}
const origValue = unref(formula.innermostVariable) ?? 0;
let newValue = Decimal.add(amountToBuy, origValue);
const targetValue = newValue;
directSum ??= cumulativeCost ? 10 : 0;
newValue = newValue.sub(directSum).clampMin(origValue);
let cost: DecimalSource = 0;
// Indirect sum
if (Decimal.gt(amountToBuy, directSum)) {
if (!formula.isInvertible()) {
console.error("Cannot calculate cost with indirect sum of non-invertible formula");
return 0;
}
if (cumulativeCost) {
if (!formula.isIntegrable()) {
console.error(
"Cannot calculate cost with cumulative cost of non-integrable formula"
);
return 0;
}
cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
if (targetValue.gt(1e308)) {
// Too large of a number for directSum to make a difference,
// just get the cost and multiply by summed purchases
return Decimal.add(
cost,
Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue))
);
}
} else {
cost = formula.evaluate(newValue);
newValue = newValue.add(1);
if (targetValue.gt(1e308)) {
// Too large of a number for directSum to make a difference,
// just get the cost and multiply by summed purchases
return Decimal.sub(targetValue, newValue).add(1).times(cost);
}
}
}
// Direct sum
for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) {
cost = Decimal.add(cost, formula.evaluate(i));
}
return cost;
}

View file

@ -1,6 +1,12 @@
import Decimal, { DecimalSource } from "util/bignum";
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);
@ -8,21 +14,33 @@ export function passthrough<T extends GenericFormula | DecimalSource>(value: T):
return value;
}
export function invertPassthrough(value: DecimalSource, ...inputs: FormulaSource[]) {
const variable = inputs.find(input => hasVariable(input)) as InvertibleFormula | undefined;
if (variable == null) {
console.error("Could not invert due to no input being a variable");
return 0;
}
return variable.invert(value);
}
export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return lhs.invert(Decimal.neg(value));
}
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
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) {
@ -35,24 +53,28 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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(
@ -62,18 +84,21 @@ export function integrateInnerAdd(
) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.add(x, rhs);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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) {
@ -82,24 +107,28 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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(
@ -109,18 +138,21 @@ export function integrateInnerSub(
) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.sub(x, rhs);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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) {
@ -129,24 +161,28 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.times(x, rhs);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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(
@ -159,7 +195,8 @@ export function applySubstitutionMul(
} else if (hasVariable(rhs)) {
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) {
@ -168,24 +205,28 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.div(x, rhs);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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(
@ -198,32 +239,37 @@ export function applySubstitutionDiv(
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
@ -235,13 +281,15 @@ function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
const numerator = ln10.times(value);
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
@ -249,7 +297,8 @@ export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
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) {
@ -258,7 +307,8 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
@ -270,13 +320,15 @@ function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs
const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
}
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
return new Formula({
inputs: [lhs.getIntegralFormula(stack), rhs],
@ -284,14 +336,16 @@ export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs:
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) {
if (hasVariable(lhs)) {
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) {
@ -303,13 +357,15 @@ function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
const numerator = Decimal.ln(2).times(value);
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
@ -317,14 +373,16 @@ export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
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) {
if (hasVariable(lhs)) {
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) {
@ -335,13 +393,15 @@ function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
@ -349,7 +409,8 @@ export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
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) {
@ -358,70 +419,81 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
const pow = Formula.add(rhs, 1);
return Formula.pow(x, pow).div(pow);
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
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)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.pow(rhs, x).div(Formula.ln(rhs));
} else if (hasVariable(rhs)) {
if (!rhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = rhs.getIntegralFormula(stack);
const denominator = Formula.add(lhs, 1);
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) {
@ -430,36 +502,42 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
} else if (hasVariable(rhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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(
@ -481,7 +559,8 @@ export function invertTetrate(
return base.invert(Decimal.ssqrt(value));
}
// 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(
@ -509,7 +588,8 @@ export function invertIteratedExp(
);
}
// 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(
@ -533,7 +613,8 @@ export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: Formul
);
}
// 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) {
@ -556,21 +637,24 @@ export function invertLayeradd(
);
}
// 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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
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) {
@ -582,226 +666,262 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.asin(x)
.times(x)
.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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.acos(x)
.times(x)
.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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.atan(x)
.times(x)
.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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.acosh(x)
.times(x)
.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) {
if (hasVariable(lhs)) {
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) {
if (hasVariable(lhs)) {
if (!lhs.isIntegrable()) {
throw new Error("Could not integrate due to variable not being integrable");
console.error("Could not integrate due to variable not being integrable");
return Formula.constant(0);
}
const x = lhs.getIntegralFormula(stack);
return Formula.atanh(x)
.times(x)
.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(

View file

@ -225,7 +225,9 @@ export function createLayer<T extends LayerOptions>(
addingLayers[addingLayers.length - 1] == null ||
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();

View file

@ -41,11 +41,11 @@ 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).
*/
export type ModifierFromOptionalParams<T, S> = T extends undefined
? S extends undefined
export type ModifierFromOptionalParams<T, S> = undefined extends T
? undefined extends S
? Omit<WithRequired<Modifier, "invert" | "getFormula">, "description" | "enabled">
: Omit<WithRequired<Modifier, "invert" | "enabled" | "getFormula">, "description">
: S extends undefined
: undefined extends S
? Omit<WithRequired<Modifier, "invert" | "description" | "getFormula">, "enabled">
: WithRequired<Modifier, "invert" | "enabled" | "description" | "getFormula">;
@ -297,9 +297,9 @@ export function createSequentialModifier<
getFormula: modifiers.every(m => m.getFormula != null)
? (gain: FormulaSource) =>
modifiers
.filter(m => unref(m.enabled) !== false)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.reduce((acc, curr) => curr.getFormula!(acc), gain)
.reduce((acc, curr) => Formula.if(acc, curr.enabled ?? true,
acc => curr.getFormula!(acc), acc => acc), gain)
: undefined,
enabled: modifiers.some(m => m.enabled != null)
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)

View file

@ -9,6 +9,7 @@ import type { Ref, WritableComputedRef } from "vue";
import { computed, isReactive, isRef, ref } from "vue";
import player from "./player";
import state from "./state";
import Formula from "./formulas/formulas";
/**
* A symbol used in {@link Persistent} objects.
@ -115,12 +116,7 @@ function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T)
state.NaNPath = persistent[SaveDataPath];
state.NaNPersistent = persistent as Persistent<DecimalSource>;
}
console.error(
`Attempted to save NaN value to`,
persistent[SaveDataPath]?.join("."),
persistent
);
throw new Error("Attempted to set NaN value. See above for details");
console.error(`Attempted to save NaN value to ${persistent[SaveDataPath]?.join(".")}`);
}
persistent[PersistentState].value = value;
}
@ -291,8 +287,8 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
"."
)}\` when it's already present at \`${value[SaveDataPath].join(
"."
)}\`. This can cause unexpected behavior when loading saves between updates.`,
value
)}\`.`,
"This can cause unexpected behavior when loading saves between updates."
);
}
value[SaveDataPath] = newPath;
@ -325,6 +321,7 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
}
} else if (
!(value instanceof Decimal) &&
!(value instanceof Formula) &&
!isRef(value) &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
!features.includes(value as { type: typeof Symbol })
@ -366,9 +363,9 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
return;
}
console.error(
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
persistent,
"\nCreated at:\n" + persistent[StackTrace]
`Created persistent ref in ${layer.id} without registering it to the layer!`,
"Make sure to include everything persistent in the returned object.\n\nCreated at:\n" +
persistent[StackTrace]
);
});
persistentRefs[layer.id].clear();

View file

@ -20,7 +20,7 @@ import { createLazyProxy } from "util/proxies";
import { joinJSX, renderJSX } from "util/vue";
import { computed, unref } from "vue";
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";
/**
@ -86,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.
* @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.
* @see {@link cost} for restrictions on maximizing support.
@ -100,7 +108,7 @@ export type CostRequirement = Replace<
cost: ProcessedComputable<DecimalSource> | GenericFormula;
visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
requiresPay: ProcessedComputable<boolean>;
spendResources: ProcessedComputable<boolean>;
cumulativeCost: ProcessedComputable<boolean>;
canMaximize: ProcessedComputable<boolean>;
}
>;
@ -126,7 +134,12 @@ export function createCostRequirement<T extends CostRequirementOptions>(
{displayResource(
req.resource,
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>)
)}{" "}
{req.resource.displayName}
@ -138,7 +151,12 @@ export function createCostRequirement<T extends CostRequirementOptions>(
{displayResource(
req.resource,
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>)
)}{" "}
{req.resource.displayName}
@ -150,54 +168,62 @@ export function createCostRequirement<T extends CostRequirementOptions>(
processComputable(req as T, "cost");
processComputable(req as T, "requiresPay");
setDefault(req, "requiresPay", true);
processComputable(req as T, "spendResources");
setDefault(req, "spendResources", true);
processComputable(req as T, "cumulativeCost");
setDefault(req, "cumulativeCost", true);
processComputable(req as T, "maxBulkAmount");
setDefault(req, "maxBulkAmount", 1);
processComputable(req as T, "directSum");
setDefault(req, "pay", function (amount?: DecimalSource) {
const cost =
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>);
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
});
req.canMaximize = computed(
() =>
req.cost instanceof Formula &&
req.cost.isInvertible() &&
(unref(req.spendResources) === false || req.cost.isIntegrable())
);
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.cost instanceof Formula && req.cost.isInvertible()) {
const maxAffordable = calculateMaxAffordable(
if (req.cost instanceof Formula) {
req.requirementMet = calculateMaxAffordable(
req.cost,
req.resource,
unref(req.spendResources) as boolean
req.cumulativeCost ?? true,
req.directSum,
req.maxBulkAmount
);
req.requirementMet = computed(() => {
if (unref(req.canMaximize)) {
return maxAffordable.value;
} else {
if (req.cost instanceof Formula) {
return Decimal.gte(req.resource.value, req.cost.evaluate());
} else {
return Decimal.gte(
req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>)
);
}
}
});
} else {
req.requirementMet = computed(() => {
if (req.cost instanceof Formula) {
return Decimal.gte(req.resource.value, req.cost.evaluate());
} else {
return Decimal.gte(
req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>)
);
}
});
req.requirementMet = computed(() =>
Decimal.gte(
req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>)
) ? 1 : 0
);
}
return req as CostRequirement;
@ -328,7 +354,7 @@ export function payByDivision(this: CostRequirement, amount?: DecimalSource) {
? calculateCost(
this.cost,
amount ?? 1,
unref(this.spendResources as ProcessedComputable<boolean> | undefined) ?? true
unref(this.cumulativeCost as ProcessedComputable<boolean> | undefined) ?? true
)
: unref(this.cost as ProcessedComputable<DecimalSource>);
this.resource.value = Decimal.div(this.resource.value, cost);

View file

@ -1,5 +1,5 @@
import type { DecimalSource } from "util/bignum";
import { shallowReactive } from "vue";
import { reactive, shallowReactive } from "vue";
import type { Persistent } from "./persistence";
/** An object of global data that is not persistent. */
@ -12,6 +12,8 @@ export interface Transient {
NaNPath?: string[];
/** The ref that was being set to NaN. */
NaNPersistent?: Persistent<DecimalSource>;
/** List of errors that have occurred, to show the user. */
errors: Error[];
}
declare global {
@ -24,5 +26,6 @@ declare global {
export default window.state = shallowReactive<Transient>({
lastTenTicks: [],
hasNaN: false,
NaNPath: []
NaNPath: [],
errors: reactive([])
});

View file

@ -2,6 +2,7 @@ import "@fontsource/material-icons";
import App from "App.vue";
import projInfo from "data/projInfo.json";
import "game/notifications";
import state from "game/state";
import { load } from "util/save";
import { useRegisterSW } from "virtual:pwa-register/vue";
import type { App as VueApp } from "vue";
@ -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;
window.projInfo = projInfo;
if (projInfo.id === "") {
throw new Error(
"Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"
console.error(
"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
const vue = (window.vue = createApp(App));
vue.config.errorHandler = function (err, instance, info) {
console.error(err, info, instance);
};
globalBus.emit("setupVue", vue);
vue.mount("#app");

View file

@ -12,6 +12,11 @@ export function camelToTitle(camel: string): string {
return title;
}
export function camelToKebab(camel: string) {
// Split off first character so function works on upper camel (pascal) case
return (camel[0] + camel.slice(1).replace(/[A-Z]/g, c => `-${c}`)).toLowerCase();
}
export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
functionOrValue: ((...args: S) => T) | R
): functionOrValue is (...args: S) => T {

View file

@ -36,8 +36,13 @@ export function createLazyProxy<T extends object, S extends T>(
): T {
const obj: S & Partial<T> = baseObject;
let calculated = false;
let calculating = false;
function calculateObj(): T {
if (!calculated) {
if (calculating) {
console.error("Cyclical dependency detected. Cannot evaluate lazy proxy.");
}
calculating = true;
Object.assign(obj, objectFunc.call(obj, obj));
calculated = true;
}

View file

@ -21,6 +21,7 @@ import {
unref,
watchEffect
} from "vue";
import { camelToKebab } from "./common";
export function coerceComponent(
component: CoercableComponent,
@ -241,3 +242,10 @@ export function trackHover(element: VueFeature): Ref<boolean> {
return isHovered;
}
export function kebabifyObject(object: Record<string, unknown>) {
return Object.keys(object).reduce((acc, curr) => {
acc[camelToKebab(curr)] = object[curr];
return acc;
}, {} as Record<string, unknown>);
}

View file

@ -47,6 +47,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentGain)).compare_tolerance(0);
});
});
describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion;
@ -69,6 +73,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(100);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.actualGain)).compare_tolerance(0);
});
});
describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion;
@ -95,6 +103,10 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10)
);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentAt)).compare_tolerance(0);
});
});
describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion;
@ -117,6 +129,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.dTen);
});
});
test("Converts correctly", () => {
const conversion = createCumulativeConversion(() => ({
@ -193,6 +209,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentGain)).compare_tolerance(1);
});
});
describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion;
@ -216,6 +236,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(99);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.actualGain)).compare_tolerance(0);
});
});
describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion;
@ -243,6 +267,10 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10)
);
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentAt)).compare_tolerance(Decimal.pow(1, 2).times(10));
});
});
describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion;
@ -266,6 +294,10 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
});
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(2, 2).times(10));
});
});
test("Converts correctly", () => {
const conversion = createIndependentConversion(() => ({

View file

@ -13,9 +13,13 @@ import { InvertibleIntegralFormula } from "game/formulas/types";
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 = [
"round",
"floor",
"ceil",
"trunc",
"neg",
"recip",
"log10",
@ -48,10 +52,6 @@ const invertibleZeroParamFunctionNames = [
const nonInvertibleZeroParamFunctionNames = [
"abs",
"sign",
"round",
"floor",
"ceil",
"trunc",
"pLog10",
"absLog10",
"factorial",
@ -85,6 +85,10 @@ const integrableZeroParamFunctionNames = [
] as const;
const nonIntegrableZeroParamFunctionNames = [
...nonInvertibleZeroParamFunctionNames,
"round",
"floor",
"ceil",
"trunc",
"lambertw",
"ssqrt"
] as const;
@ -151,7 +155,7 @@ describe("Formula Equality Checking", () => {
describe("Formula aliases", () => {
function testAliases<T extends FormulaFunctions>(
aliases: T[],
args: Parameters<(typeof Formula)[T]>
args: Parameters<typeof Formula[T]>
) {
describe(aliases[0], () => {
let formula: GenericFormula;
@ -227,14 +231,15 @@ describe("Creating Formulas", () => {
expect(formula.evaluate()).compare_tolerance(expectedValue));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
test("Invert throws", () => expect(() => formula.invert(25)).toThrow());
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow());
test("Invert integral throws", () =>
test("Invert errors", () => expect(() => formula.invert(25)).toLogError());
test("Integrate errors", () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
expect(() => formula.invertIntegral(25)).toThrow());
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));
@ -245,7 +250,7 @@ describe("Creating Formulas", () => {
function checkFormula<T extends FormulaFunctions>(
functionName: T,
args: Readonly<Parameters<(typeof Formula)[T]>>
args: Readonly<Parameters<typeof Formula[T]>>
) {
let formula: GenericFormula;
beforeAll(() => {
@ -256,10 +261,10 @@ describe("Creating Formulas", () => {
// None of these formulas have variables, so they should all behave the same
test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false));
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
test(`Formula throws if trying to invert`, () =>
test(`Formula errors if trying to invert`, () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
expect(() => formula.invert(10)).toThrow());
expect(() => formula.invert(10)).toLogError());
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
test("Has a non-invertible integral", () =>
expect(formula.isIntegralInvertible()).toBe(false));
@ -269,7 +274,7 @@ describe("Creating Formulas", () => {
// It's a lot of tests, but I'd rather be exhaustive
function testFormulaCall<T extends FormulaFunctions>(
functionName: T,
args: Readonly<Parameters<(typeof Formula)[T]>>
args: Readonly<Parameters<typeof Formula[T]>>
) {
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
// These cases in particular take a long time, so skip them
@ -487,14 +492,21 @@ describe("Inverting", () => {
});
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);
});
test("Inverting with non-invertible sections", () => {
const formula = Formula.add(variable, constant.ceil());
expect(formula.isInvertible()).toBe(true);
expect(formula.invert(10)).compare_tolerance(0);
describe("Inverting with non-invertible sections", () => {
test("Non-invertible constant", () => {
const formula = Formula.add(variable, constant.sign());
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();
});
});
});
@ -617,7 +629,20 @@ describe("Integrating", () => {
test("Integrating nested complex formulas", () => {
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();
});
});
});
@ -637,7 +662,7 @@ describe("Inverting integrals", () => {
describe("Invertible Integral functions marked as such", () => {
function checkFormula(formula: InvertibleIntegralFormula) {
expect(formula.isIntegralInvertible()).toBe(true);
expect(() => formula.invertIntegral(10)).to.not.throw();
expect(() => formula.invertIntegral(10)).not.toLogError();
}
invertibleIntegralZeroPramFunctionNames.forEach(name => {
describe(name, () => {
@ -656,7 +681,7 @@ describe("Inverting integrals", () => {
test(`${name}(var, var) is marked as not having an invertible integral`, () => {
const formula = Formula[name](variable, variable);
expect(formula.isIntegralInvertible()).toBe(false);
expect(() => formula.invertIntegral(10)).to.throw();
expect(() => formula.invertIntegral(10)).toLogError();
});
});
});
@ -712,7 +737,7 @@ describe("Inverting integrals", () => {
test("Inverting integral of nested complex formulas", () => {
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
expect(() => formula.invertIntegral(100)).toThrow();
expect(() => formula.invertIntegral(100)).toLogError();
});
});
@ -745,7 +770,7 @@ describe("Step-wise", () => {
);
expect(() =>
Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral()
).toThrow();
).toLogError();
});
test("Formula never marked as having an invertible integral", () => {
@ -754,7 +779,7 @@ describe("Step-wise", () => {
).toBe(false);
expect(() =>
Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10)
).toThrow();
).toLogError();
});
test("Formula modifiers with variables mark formula as non-invertible", () => {
@ -846,7 +871,7 @@ describe("Conditionals", () => {
);
expect(() =>
Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral()
).toThrow();
).toLogError();
});
test("Formula never marked as having an invertible integral", () => {
@ -855,7 +880,7 @@ describe("Conditionals", () => {
).toBe(false);
expect(() =>
Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10)
).toThrow();
).toLogError();
});
test("Formula modifiers with variables mark formula as non-invertible", () => {
@ -956,7 +981,7 @@ describe("Custom Formulas", () => {
evaluate: () => 6,
invert: value => value
}).invert(10)
).toThrow());
).toLogError());
test("One input inverts correctly", () =>
expect(
new Formula({
@ -983,7 +1008,7 @@ describe("Custom Formulas", () => {
evaluate: () => 0,
integrate: stack => variable
}).evaluateIntegral()
).toThrow());
).toLogError());
test("One input integrates correctly", () =>
expect(
new Formula({
@ -1010,7 +1035,7 @@ describe("Custom Formulas", () => {
evaluate: () => 0,
integrate: stack => variable
}).invertIntegral(20)
).toThrow());
).toLogError());
test("One input inverts integral correctly", () =>
expect(
new Formula({
@ -1053,12 +1078,19 @@ describe("Buy Max", () => {
beforeAll(() => {
resource = createResource(ref(100000));
});
describe("Without spending", () => {
test("Throws on formula with non-invertible integral", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
expect(() => maxAffordable.value).toThrow();
describe("Without cumulative cost", () => {
test("Errors on calculating max affordable of non-invertible formula", () => {
const purchases = ref(1);
const variable = Formula.variable(purchases);
const formula = Formula.abs(variable);
const maxAffordable = calculateMaxAffordable(formula, resource, false);
expect(() => maxAffordable.value).toLogError();
});
test("Errors on calculating cost of non-invertible formula", () => {
const purchases = ref(1);
const variable = Formula.variable(purchases);
const formula = Formula.abs(variable);
expect(() => calculateCost(formula, 5, false, 0)).toLogError();
});
test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0);
@ -1069,11 +1101,32 @@ describe("Buy Max", () => {
Decimal.pow(1.05, 141).times(100)
);
});
test("Calculates max affordable and cost correctly with direct sum", () => {
const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100);
const maxAffordable = calculateMaxAffordable(formula, resource, false, 4);
expect(maxAffordable.value).compare_tolerance(141 - 4);
const actualCost = new Array(4)
.fill(null)
.reduce((acc, _, i) => acc.add(formula.evaluate(133 + i)), new Decimal(0));
const calculatedCost = calculateCost(formula, maxAffordable.value, false, 4);
expect(calculatedCost).compare_tolerance(actualCost);
});
});
describe("With spending", () => {
test("Throws on non-invertible formula", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow();
describe("With cumulative cost", () => {
test("Errors on calculating max affordable of non-invertible formula", () => {
const purchases = ref(1);
const variable = Formula.variable(purchases);
const formula = Formula.abs(variable);
const maxAffordable = calculateMaxAffordable(formula, resource, true);
expect(() => maxAffordable.value).toLogError();
});
test("Errors on calculating cost of non-invertible formula", () => {
const purchases = ref(1);
const variable = Formula.variable(purchases);
const formula = Formula.abs(variable);
expect(() => calculateCost(formula, 5, true, 0)).toLogError();
});
test("Estimates max affordable and cost correctly with 0 purchases", () => {
const purchases = ref(0);
@ -1131,7 +1184,7 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).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 variable = Formula.variable(purchases);
const formula = Formula.pow(1.05, variable).times(100);
@ -1158,7 +1211,7 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.02);
});
test("Handles summing purchases when making few purchases", () => {
test("Handles direct sum when making few purchases", () => {
const purchases = ref(90);
const variable = Formula.variable(purchases);
const formula = Formula.pow(1.05, variable).times(100);
@ -1186,7 +1239,7 @@ describe("Buy Max", () => {
// Since we're summing all the purchases this should be equivalent
expect(calculatedCost).compare_tolerance(actualCost);
});
test("Handles summing purchases when making very few purchases", () => {
test("Handles direct sum when making very few purchases", () => {
const purchases = ref(0);
const variable = Formula.variable(purchases);
const formula = variable.add(1);
@ -1200,11 +1253,11 @@ describe("Buy Max", () => {
(acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
new Decimal(0)
);
const calculatedCost = calculateCost(formula, maxAffordable.value, true);
const calculatedCost = calculateCost(formula, maxAffordable.value);
// Since we're summing all the purchases this should be equivalent
expect(calculatedCost).compare_tolerance(actualCost);
});
test("Handles summing purchases when over e308 purchases", () => {
test("Handles direct sum when over e308 purchases", () => {
resource.value = "1ee308";
const purchases = ref(0);
const variable = Formula.variable(purchases);
@ -1215,5 +1268,10 @@ describe("Buy Max", () => {
expect(Decimal.isFinite(calculatedCost)).toBe(true);
resource.value = 100000;
});
test("Handles direct sum of non-integrable formula", () => {
const purchases = ref(0);
const formula = Formula.variable(purchases).abs();
expect(() => calculateCost(formula, 10)).not.toLogError();
});
});
});

View file

@ -11,6 +11,7 @@ import {
Requirement,
requirementsMet
} from "game/requirements";
import Decimal from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest";
import { isRef, ref, unref } from "vue";
import "../utils";
@ -26,8 +27,7 @@ describe("Creating cost requirement", () => {
beforeAll(() => {
requirement = createCostRequirement(() => ({
resource,
cost: 10,
spendResources: false
cost: 10
}));
});
@ -44,7 +44,7 @@ describe("Creating cost requirement", () => {
});
test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
test("requires pay", () => expect(requirement.requiresPay).toBe(true));
test("does not spend resources", () => expect(requirement.spendResources).toBe(false));
test("does not spend resources", () => expect(requirement.cumulativeCost).toBe(true));
test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
});
@ -56,8 +56,9 @@ describe("Creating cost requirement", () => {
cost: Formula.variable(resource).times(10),
visibility: Visibility.None,
requiresPay: false,
maximize: true,
spendResources: true,
cumulativeCost: false,
maxBulkAmount: Decimal.dInf,
directSum: 5,
// eslint-disable-next-line @typescript-eslint/no-empty-function
pay() {}
}));
@ -69,30 +70,43 @@ describe("Creating cost requirement", () => {
requirement.pay.length === 1);
test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None));
test("does not require pay", () => expect(requirement.requiresPay).toBe(false));
test("spends resources", () => expect(requirement.spendResources).toBe(true));
test("spends resources", () => expect(requirement.cumulativeCost).toBe(false));
test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true));
test("maxBulkAmount is set", () =>
expect(unref(requirement.maxBulkAmount)).compare_tolerance(Decimal.dInf));
test("directSum is set", () => expect(unref(requirement.directSum)).toBe(5));
});
test("Requirement met when meeting the cost", () => {
const requirement = createCostRequirement(() => ({
resource,
cost: 10,
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", () => {
const requirement = createCostRequirement(() => ({
resource,
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(() => ({
@ -101,82 +115,104 @@ describe("Creating cost requirement", () => {
})).canMaximize
)
).toBe(false));
test("Non-invertible formula cannot maximize", () =>
test("Non-invertible formula cannot maximize when max bulk amount is above direct sum", () =>
expect(
unref(
createCostRequirement(() => ({
resource,
cost: Formula.variable(resource).abs()
cost: Formula.variable(resource).abs(),
maxBulkAmount: Decimal.dInf
})).canMaximize
)
).toBe(false));
test("Invertible formula can maximize if spendResources is 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(),
spendResources: false
cumulativeCost: false,
maxBulkAmount: Decimal.dInf
})).canMaximize
)
).toBe(true));
test("Invertible formula cannot maximize if spendResources is true", () =>
test("Invertible formula cannot maximize if cumulativeCost is true", () =>
expect(
unref(
createCostRequirement(() => ({
resource,
cost: Formula.variable(resource).lambertw(),
spendResources: true
cumulativeCost: true,
maxBulkAmount: Decimal.dInf
})).canMaximize
)
).toBe(false));
test("Integrable formula can maximize if spendResources is false", () =>
test("Integrable formula can maximize if cumulativeCost is false", () =>
expect(
unref(
createCostRequirement(() => ({
resource,
cost: Formula.variable(resource).pow(2),
spendResources: false
cumulativeCost: false,
maxBulkAmount: Decimal.dInf
})).canMaximize
)
).toBe(true));
test("Integrable formula can maximize if spendResources is true", () =>
test("Integrable formula can maximize if cumulativeCost is true", () =>
expect(
unref(
createCostRequirement(() => ({
resource,
cost: Formula.variable(resource).pow(2),
spendResources: true
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("Requirement met when visible", () => {
const requirement = createVisibilityRequirement({ visibility: Visibility.Visible });
expect(unref(requirement.requirementMet)).toBe(true);
});
test("Requirement not met when not visible", () => {
let requirement = createVisibilityRequirement({ visibility: Visibility.None });
expect(unref(requirement.requirementMet)).toBe(false);
requirement = createVisibilityRequirement({ visibility: false });
expect(unref(requirement.requirementMet)).toBe(false);
});
test("Creating visibility requirement", () => {
const visibility = ref<Visibility.None | Visibility.Visible | boolean>(Visibility.Visible);
const requirement = createVisibilityRequirement({ visibility });
expect(unref(requirement.requirementMet)).toBe(true);
visibility.value = true;
expect(unref(requirement.requirementMet)).toBe(true);
visibility.value = Visibility.None;
expect(unref(requirement.requirementMet)).toBe(false);
visibility.value = false;
expect(unref(requirement.requirementMet)).toBe(false);
});
describe("Creating boolean requirement", () => {
test("Requirement met when true", () => {
const requirement = createBooleanRequirement(ref(true));
expect(unref(requirement.requirementMet)).toBe(true);
});
test("Requirement not met when false", () => {
const requirement = createBooleanRequirement(ref(false));
expect(unref(requirement.requirementMet)).toBe(false);
});
test("Creating boolean requirement", () => {
const req = ref(true);
const requirement = createBooleanRequirement(req);
expect(unref(requirement.requirementMet)).toBe(true);
req.value = false;
expect(unref(requirement.requirementMet)).toBe(false);
});
describe("Checking all requirements met", () => {
@ -208,7 +244,7 @@ describe("Checking maximum levels of requirements met", () => {
createCostRequirement(() => ({
resource: createResource(ref(10)),
cost: Formula.variable(0),
spendResources: false
cumulativeCost: false
}))
];
expect(maxRequirementsMet(requirements)).compare_tolerance(0);
@ -220,7 +256,8 @@ describe("Checking maximum levels of requirements met", () => {
createCostRequirement(() => ({
resource: createResource(ref(10)),
cost: Formula.variable(0),
spendResources: false
cumulativeCost: false,
maxBulkAmount: Decimal.dInf
}))
];
expect(maxRequirementsMet(requirements)).compare_tolerance(10);
@ -233,12 +270,12 @@ test("Paying requirements", () => {
resource,
cost: 10,
requiresPay: false,
spendResources: false
cumulativeCost: false
}));
const payment = createCostRequirement(() => ({
resource,
cost: 10,
spendResources: false
cumulativeCost: false
}));
payRequirements([noPayment, payment]);
expect(resource.value).compare_tolerance(90);

View file

@ -1,8 +1,9 @@
import Decimal, { DecimalSource, format } from "util/bignum";
import { expect } from "vitest";
import { Mock, expect, vi } from "vitest";
interface CustomMatchers<R = unknown> {
compare_tolerance(expected: DecimalSource, tolerance?: number): R;
toLogError(): R;
}
declare global {
@ -36,5 +37,25 @@ expect.extend({
expected: format(expected),
actual: format(received)
};
},
toLogError(received: () => unknown) {
const { isNot } = this;
console.error = vi.fn();
received();
const calls = (
console.error as unknown as Mock<
Parameters<typeof console.error>,
ReturnType<typeof console.error>
>
).mock.calls.length;
const pass = calls >= 1;
vi.restoreAllMocks();
return {
pass,
message: () =>
`Expected ${received} to ${(isNot as boolean) ? " not" : ""} log an error`,
expected: "1+",
actual: calls
};
}
});