From 3489c83cc46fe8f8389a732706e29c3488e74076 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 27 Feb 2023 22:44:30 -0600
Subject: [PATCH 01/36] Add util for creating formula from a modifier

---
 src/data/common.tsx | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/data/common.tsx b/src/data/common.tsx
index b0becca..ec3317e 100644
--- a/src/data/common.tsx
+++ b/src/data/common.tsx
@@ -8,7 +8,7 @@ import { GenericMilestone } from "features/milestones/milestone";
 import { displayResource, Resource } from "features/resources/resource";
 import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
 import { createTreeNode } from "features/trees/tree";
-import { GenericFormula } from "game/formulas";
+import Formula, { FormulaSource, GenericFormula, InvertibleFormula } from "game/formulas";
 import type { Modifier } from "game/modifiers";
 import type { Persistent } from "game/persistence";
 import { DefaultValue, persistent } from "game/persistence";
@@ -490,3 +490,24 @@ export function createFormulaPreview(
         return formatSmall(formula.evaluate());
     });
 }
+
+export function modifierToFormula<T extends GenericFormula>(
+    modifier: WithRequired<Modifier, "revert">,
+    base: T
+): T;
+export function modifierToFormula(modifier: Modifier, base: FormulaSource): GenericFormula;
+export function modifierToFormula(modifier: Modifier, base: FormulaSource) {
+    return new Formula({
+        inputs: [base],
+        evaluate: val => modifier.apply(val),
+        invert:
+            "revert" in modifier && modifier.revert != null
+                ? (val, lhs) => {
+                      if (lhs instanceof Formula && lhs.hasVariable()) {
+                          return lhs.invert(modifier.revert!(val));
+                      }
+                      throw "Could not invert due to no input being a variable";
+                  }
+                : undefined
+    });
+}

From 1b8755dbfc891866bf4120abfaf733ac7613fbe2 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 27 Feb 2023 23:47:31 -0600
Subject: [PATCH 02/36] Write up changelog thus far

---
 CHANGELOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 60 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b982041..b2afda6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+- **BREAKING** New requirements system
+    - Replaces many features' existing requirements with new generic form
+- Formulas, which can be used to calculate buy max for you
+- Action feature
+- ETA util
+- createCollapsibleMilestones util
+- deleteLowerSaves util
+- Minimized layers can now display a component
+- submitOnBlur property to Text fields
+- showPopups property to Milestones
+- Mouse/touch events to more onClick listeners
+- Example hotkey to starting layer
+- Schema for projInfo.json
+### Changes
+- **BREAKING** Buyables renamed to Repeatables
+    - Renamed purchaseLimit to limit
+    - Renamed buyMax to maximize
+    - Added initialAmount property
+- **BREAKING** Persistent refs no longer have redundancies in save object
+    - Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()`
+- **BREAKING** Visibility properties can now take booleans
+    - Removed showIf util
+- Tweaked settings display
+- setupPassiveGeneration will no longer lower the resource
+- displayResource now floors resource amounts
+- Tweaked modifier displays, incl showing negative modifiers in red
+- Hotkeys now appear on key graphic
+- Mofifier sections now accept computable strings for title and subtitle
+- Updated b_e
+### Fixed
+- NaN detection stopped working
+    - Now specifically only checks persistent refs
+- trackTotal would increase the total when loading the save
+- PWAs wouldn't show updates
+- Board feature no longer working at all
+- Some discord links didn't open in new tab
+- Adjacent grid cells wouldn't merge
+- When fixing old saves, the modVersion would not be updated
+- Default layer would display `Dev Speed: 0x` when paused
+- Fixed hotkeys not working with shift + numbers
+- Fixed console errors about deleted persistent refs not being included in the layer object
+- Modifiers wouldn't display small numbers
+- Conversions' addSoftcap wouldn't affect currentAt or nextAt
+- MainDisplay not respecting style and classes props
+- Tabs could sometimes not update correctly
+- offlineTime not capping properly
+- Tooltips being user-selectable
+- Workflows not working with submodules
+- Various minor typing issues
+### Documented
+- requirements.tsx
+- formulas.tsx
+- repeatables.tsx
+### Tests
+- requirements
+- formulas
+
+Contributors: thepaperpilot, escapee, adsaf, ducdat
+
 ## [0.5.2] - 2022-08-22
 ### Added
 - onLoad event

From 8939ad133a4127d6004beb9201db18d7d2507fce Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Wed, 15 Mar 2023 16:30:29 -0500
Subject: [PATCH 03/36] Update vitest

---
 package-lock.json | 196 ++++++++++++++++++++++------------------------
 package.json      |   2 +-
 2 files changed, 96 insertions(+), 102 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 7082e7c..31bffe8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
         "jsdom": "^20.0.0",
         "prettier": "^2.5.1",
         "typescript": "^4.7.4",
-        "vitest": "^0.28.5",
+        "vitest": "^0.29.3",
         "vue-tsc": "^0.38.1"
       },
       "engines": {
@@ -2718,23 +2718,23 @@
       }
     },
     "node_modules/@vitest/expect": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz",
-      "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.3.tgz",
+      "integrity": "sha512-z/0JqBqqrdtrT/wzxNrWC76EpkOHdl+SvuNGxWulLaoluygntYyG5wJul5u/rQs5875zfFz/F+JaDf90SkLUIg==",
       "dev": true,
       "dependencies": {
-        "@vitest/spy": "0.28.5",
-        "@vitest/utils": "0.28.5",
+        "@vitest/spy": "0.29.3",
+        "@vitest/utils": "0.29.3",
         "chai": "^4.3.7"
       }
     },
     "node_modules/@vitest/runner": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz",
-      "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.3.tgz",
+      "integrity": "sha512-XLi8ctbvOWhUWmuvBUSIBf8POEDH4zCh6bOuVxm/KGfARpgmVF1ku+vVNvyq85va+7qXxtl+MFmzyXQ2xzhAvw==",
       "dev": true,
       "dependencies": {
-        "@vitest/utils": "0.28.5",
+        "@vitest/utils": "0.29.3",
         "p-limit": "^4.0.0",
         "pathe": "^1.1.0"
       }
@@ -2767,24 +2767,23 @@
       }
     },
     "node_modules/@vitest/spy": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz",
-      "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.3.tgz",
+      "integrity": "sha512-LLpCb1oOCOZcBm0/Oxbr1DQTuKLRBsSIHyLYof7z4QVE8/v8NcZKdORjMUq645fcfX55+nLXwU/1AQ+c2rND+w==",
       "dev": true,
       "dependencies": {
         "tinyspy": "^1.0.2"
       }
     },
     "node_modules/@vitest/utils": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz",
-      "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.3.tgz",
+      "integrity": "sha512-hg4Ff8AM1GtUnLpUJlNMxrf9f4lZr/xRJjh3uJ0QFP+vjaW82HAxKrmeBmLnhc8Os2eRf+f+VBu4ts7TafPPkA==",
       "dev": true,
       "dependencies": {
         "cli-truncate": "^3.1.0",
         "diff": "^5.1.0",
         "loupe": "^2.3.6",
-        "picocolors": "^1.0.0",
         "pretty-format": "^27.5.1"
       }
     },
@@ -5819,15 +5818,15 @@
       "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
     },
     "node_modules/mlly": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz",
-      "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz",
+      "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.8.1",
-        "pathe": "^1.0.0",
-        "pkg-types": "^1.0.1",
-        "ufo": "^1.0.1"
+        "acorn": "^8.8.2",
+        "pathe": "^1.1.0",
+        "pkg-types": "^1.0.2",
+        "ufo": "^1.1.1"
       }
     },
     "node_modules/ms": {
@@ -6083,14 +6082,14 @@
       }
     },
     "node_modules/pkg-types": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz",
-      "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz",
+      "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==",
       "dev": true,
       "dependencies": {
         "jsonc-parser": "^3.2.0",
-        "mlly": "^1.0.0",
-        "pathe": "^1.0.0"
+        "mlly": "^1.1.1",
+        "pathe": "^1.1.0"
       }
     },
     "node_modules/postcss": {
@@ -7019,9 +7018,9 @@
       }
     },
     "node_modules/ufo": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz",
-      "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz",
+      "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==",
       "dev": true
     },
     "node_modules/unbox-primitive": {
@@ -7202,9 +7201,9 @@
       }
     },
     "node_modules/vite-node": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz",
-      "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.3.tgz",
+      "integrity": "sha512-QYzYSA4Yt2IiduEjYbccfZQfxKp+T1Do8/HEpSX/G5WIECTFKJADwLs9c94aQH4o0A+UtCKU61lj1m5KvbxxQA==",
       "dev": true,
       "dependencies": {
         "cac": "^6.7.14",
@@ -7212,8 +7211,6 @@
         "mlly": "^1.1.0",
         "pathe": "^1.1.0",
         "picocolors": "^1.0.0",
-        "source-map": "^0.6.1",
-        "source-map-support": "^0.5.21",
         "vite": "^3.0.0 || ^4.0.0"
       },
       "bin": {
@@ -7280,9 +7277,9 @@
       }
     },
     "node_modules/vite-node/node_modules/rollup": {
-      "version": "3.15.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz",
-      "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==",
+      "version": "3.19.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz",
+      "integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==",
       "dev": true,
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -7296,9 +7293,9 @@
       }
     },
     "node_modules/vite-node/node_modules/vite": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz",
-      "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==",
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz",
+      "integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.16.14",
@@ -7380,18 +7377,18 @@
       }
     },
     "node_modules/vitest": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz",
-      "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.3.tgz",
+      "integrity": "sha512-muMsbXnZsrzDGiyqf/09BKQsGeUxxlyLeLK/sFFM4EXdURPQRv8y7dco32DXaRORYP0bvyN19C835dT23mL0ow==",
       "dev": true,
       "dependencies": {
         "@types/chai": "^4.3.4",
         "@types/chai-subset": "^1.3.3",
         "@types/node": "*",
-        "@vitest/expect": "0.28.5",
-        "@vitest/runner": "0.28.5",
-        "@vitest/spy": "0.28.5",
-        "@vitest/utils": "0.28.5",
+        "@vitest/expect": "0.29.3",
+        "@vitest/runner": "0.29.3",
+        "@vitest/spy": "0.29.3",
+        "@vitest/utils": "0.29.3",
         "acorn": "^8.8.1",
         "acorn-walk": "^8.2.0",
         "cac": "^6.7.14",
@@ -7407,7 +7404,7 @@
         "tinypool": "^0.3.1",
         "tinyspy": "^1.0.2",
         "vite": "^3.0.0 || ^4.0.0",
-        "vite-node": "0.28.5",
+        "vite-node": "0.29.3",
         "why-is-node-running": "^2.2.2"
       },
       "bin": {
@@ -9914,23 +9911,23 @@
       }
     },
     "@vitest/expect": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz",
-      "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.3.tgz",
+      "integrity": "sha512-z/0JqBqqrdtrT/wzxNrWC76EpkOHdl+SvuNGxWulLaoluygntYyG5wJul5u/rQs5875zfFz/F+JaDf90SkLUIg==",
       "dev": true,
       "requires": {
-        "@vitest/spy": "0.28.5",
-        "@vitest/utils": "0.28.5",
+        "@vitest/spy": "0.29.3",
+        "@vitest/utils": "0.29.3",
         "chai": "^4.3.7"
       }
     },
     "@vitest/runner": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz",
-      "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.3.tgz",
+      "integrity": "sha512-XLi8ctbvOWhUWmuvBUSIBf8POEDH4zCh6bOuVxm/KGfARpgmVF1ku+vVNvyq85va+7qXxtl+MFmzyXQ2xzhAvw==",
       "dev": true,
       "requires": {
-        "@vitest/utils": "0.28.5",
+        "@vitest/utils": "0.29.3",
         "p-limit": "^4.0.0",
         "pathe": "^1.1.0"
       },
@@ -9953,24 +9950,23 @@
       }
     },
     "@vitest/spy": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz",
-      "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.3.tgz",
+      "integrity": "sha512-LLpCb1oOCOZcBm0/Oxbr1DQTuKLRBsSIHyLYof7z4QVE8/v8NcZKdORjMUq645fcfX55+nLXwU/1AQ+c2rND+w==",
       "dev": true,
       "requires": {
         "tinyspy": "^1.0.2"
       }
     },
     "@vitest/utils": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz",
-      "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.3.tgz",
+      "integrity": "sha512-hg4Ff8AM1GtUnLpUJlNMxrf9f4lZr/xRJjh3uJ0QFP+vjaW82HAxKrmeBmLnhc8Os2eRf+f+VBu4ts7TafPPkA==",
       "dev": true,
       "requires": {
         "cli-truncate": "^3.1.0",
         "diff": "^5.1.0",
         "loupe": "^2.3.6",
-        "picocolors": "^1.0.0",
         "pretty-format": "^27.5.1"
       }
     },
@@ -12129,15 +12125,15 @@
       "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
     },
     "mlly": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz",
-      "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz",
+      "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==",
       "dev": true,
       "requires": {
-        "acorn": "^8.8.1",
-        "pathe": "^1.0.0",
-        "pkg-types": "^1.0.1",
-        "ufo": "^1.0.1"
+        "acorn": "^8.8.2",
+        "pathe": "^1.1.0",
+        "pkg-types": "^1.0.2",
+        "ufo": "^1.1.1"
       }
     },
     "ms": {
@@ -12327,14 +12323,14 @@
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
     },
     "pkg-types": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz",
-      "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz",
+      "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==",
       "dev": true,
       "requires": {
         "jsonc-parser": "^3.2.0",
-        "mlly": "^1.0.0",
-        "pathe": "^1.0.0"
+        "mlly": "^1.1.1",
+        "pathe": "^1.1.0"
       }
     },
     "postcss": {
@@ -12981,9 +12977,9 @@
       "dev": true
     },
     "ufo": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz",
-      "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz",
+      "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==",
       "dev": true
     },
     "unbox-primitive": {
@@ -13101,9 +13097,9 @@
       }
     },
     "vite-node": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz",
-      "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.3.tgz",
+      "integrity": "sha512-QYzYSA4Yt2IiduEjYbccfZQfxKp+T1Do8/HEpSX/G5WIECTFKJADwLs9c94aQH4o0A+UtCKU61lj1m5KvbxxQA==",
       "dev": true,
       "requires": {
         "cac": "^6.7.14",
@@ -13111,8 +13107,6 @@
         "mlly": "^1.1.0",
         "pathe": "^1.1.0",
         "picocolors": "^1.0.0",
-        "source-map": "^0.6.1",
-        "source-map-support": "^0.5.21",
         "vite": "^3.0.0 || ^4.0.0"
       },
       "dependencies": {
@@ -13154,18 +13148,18 @@
           }
         },
         "rollup": {
-          "version": "3.15.0",
-          "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz",
-          "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==",
+          "version": "3.19.1",
+          "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz",
+          "integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==",
           "dev": true,
           "requires": {
             "fsevents": "~2.3.2"
           }
         },
         "vite": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz",
-          "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==",
+          "version": "4.1.4",
+          "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz",
+          "integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==",
           "dev": true,
           "requires": {
             "esbuild": "^0.16.14",
@@ -13202,18 +13196,18 @@
       }
     },
     "vitest": {
-      "version": "0.28.5",
-      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz",
-      "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==",
+      "version": "0.29.3",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.3.tgz",
+      "integrity": "sha512-muMsbXnZsrzDGiyqf/09BKQsGeUxxlyLeLK/sFFM4EXdURPQRv8y7dco32DXaRORYP0bvyN19C835dT23mL0ow==",
       "dev": true,
       "requires": {
         "@types/chai": "^4.3.4",
         "@types/chai-subset": "^1.3.3",
         "@types/node": "*",
-        "@vitest/expect": "0.28.5",
-        "@vitest/runner": "0.28.5",
-        "@vitest/spy": "0.28.5",
-        "@vitest/utils": "0.28.5",
+        "@vitest/expect": "0.29.3",
+        "@vitest/runner": "0.29.3",
+        "@vitest/spy": "0.29.3",
+        "@vitest/utils": "0.29.3",
         "acorn": "^8.8.1",
         "acorn-walk": "^8.2.0",
         "cac": "^6.7.14",
@@ -13229,7 +13223,7 @@
         "tinypool": "^0.3.1",
         "tinyspy": "^1.0.2",
         "vite": "^3.0.0 || ^4.0.0",
-        "vite-node": "0.28.5",
+        "vite-node": "0.29.3",
         "why-is-node-running": "^2.2.2"
       },
       "dependencies": {
diff --git a/package.json b/package.json
index 8403380..0bf023d 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "jsdom": "^20.0.0",
     "prettier": "^2.5.1",
     "typescript": "^4.7.4",
-    "vitest": "^0.28.5",
+    "vitest": "^0.29.3",
     "vue-tsc": "^0.38.1"
   },
   "engines": {

From 5afb691b309292bd01a498b2e4a7f3c3143142b4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 21 Mar 2023 00:15:28 -0500
Subject: [PATCH 04/36] Fix more tests

---
 src/game/formulas.ts            |   4 +-
 src/game/requirements.tsx       |   5 +-
 tests/game/formulas.test.ts     | 145 +++++++++++---------------------
 tests/game/requirements.test.ts |  43 +++++-----
 4 files changed, 78 insertions(+), 119 deletions(-)

diff --git a/src/game/formulas.ts b/src/game/formulas.ts
index e812de3..3c0b379 100644
--- a/src/game/formulas.ts
+++ b/src/game/formulas.ts
@@ -477,9 +477,7 @@ function invertTetrate(
     payload: FormulaSource
 ) {
     if (hasVariable(base)) {
-        return base.invert(
-            Decimal.slog(value, Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber())
-        );
+        return base.invert(Decimal.ssqrt(value));
     }
     // Other params can't be inverted ATM
     throw "Could not invert due to no input being a variable";
diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 9412e22..5372e36 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -82,7 +82,7 @@ export interface CostRequirementOptions {
      * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
      * @see {Formula}
      */
-    spendResources?: Computable<boolean>;
+    spendResources: Computable<boolean>;
     /**
      * Pass-through to {@link Requirement.pay}. May be required for maximizing support.
      * @see {@link cost} for restrictions on maximizing support.
@@ -148,8 +148,9 @@ export function createCostRequirement<T extends CostRequirementOptions>(
         setDefault(req, "visibility", Visibility.Visible);
         processComputable(req as T, "cost");
         processComputable(req as T, "requiresPay");
-        processComputable(req as T, "spendResources");
         setDefault(req, "requiresPay", true);
+        processComputable(req as T, "spendResources");
+        setDefault(req, "spendResources", false);
         setDefault(req, "pay", function (amount?: DecimalSource) {
             const cost =
                 req.cost instanceof Formula
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index ddc0bee..43d876f 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -40,7 +40,10 @@ const invertibleZeroParamFunctionNames = [
     "tanh",
     "asinh",
     "acosh",
-    "atanh"
+    "atanh",
+    "slog",
+    "tetrate",
+    "iteratedexp"
 ] as const;
 const nonInvertibleZeroParamFunctionNames = [
     "abs",
@@ -122,7 +125,7 @@ const invertibleOneParamFunctionNames = [
     "log",
     "pow",
     "root",
-    "slog"
+    "layeradd"
 ] as const;
 const nonInvertibleOneParamFunctionNames = ["layeradd10"] as const;
 const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const;
@@ -130,12 +133,8 @@ const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionName
 const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames;
 const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames;
 
-const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const;
 const nonInvertibleTwoParamFunctionNames = ["iteratedlog", "pentate"] as const;
-const nonIntegrableTwoParamFunctionNames = [
-    ...invertibleTwoParamFunctionNames,
-    ...nonInvertibleZeroParamFunctionNames
-];
+const nonIntegrableTwoParamFunctionNames = nonInvertibleTwoParamFunctionNames;
 const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunctionNames;
 
 describe("Formula Equality Checking", () => {
@@ -201,16 +200,6 @@ describe("Formula Equality Checking", () => {
                 });
             }
         );
-
-        [...invertibleTwoParamFunctionNames, ...nonInvertibleTwoParamFunctionNames].forEach(
-            name => {
-                test(name, () => {
-                    const instanceFormula = formula[name](1, 1);
-                    const staticFormula = Formula[name](formula, 1, 1);
-                    expect(instanceFormula.equals(staticFormula)).toBe(true);
-                });
-            }
-        );
     });
 });
 
@@ -331,13 +320,7 @@ describe("Creating Formulas", () => {
         );
     });
     describe("2-param", () => {
-        (
-            [
-                ...invertibleTwoParamFunctionNames,
-                ...nonInvertibleTwoParamFunctionNames,
-                "clamp"
-            ] as const
-        ).forEach(names =>
+        ([...nonInvertibleTwoParamFunctionNames, "clamp"] as const).forEach(names =>
             describe(names, () => {
                 checkFormula(names, [0, 0, 0] as const);
                 testValues.forEach(i =>
@@ -403,24 +386,6 @@ describe("Inverting", () => {
                     checkFormula(Formula[name](variable, variable), false));
             });
         });
-        invertibleTwoParamFunctionNames.forEach(name => {
-            describe(name, () => {
-                test(`${name}(var, const, const) is marked as invertible and having a variable`, () =>
-                    checkFormula(Formula[name](variable, constant, constant)));
-                test(`${name}(const, var, const) is marked as invertible and having a variable`, () =>
-                    checkFormula(Formula[name](constant, variable, constant)));
-                test(`${name}(const, const, var) is marked as invertible and having a variable`, () =>
-                    checkFormula(Formula[name](constant, constant, variable)));
-                test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () =>
-                    checkFormula(Formula[name](variable, variable, constant), false));
-                test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () =>
-                    checkFormula(Formula[name](variable, constant, variable), false));
-                test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () =>
-                    checkFormula(Formula[name](constant, variable, variable), false));
-                test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () =>
-                    checkFormula(Formula[name](variable, variable, variable), false));
-            });
-        });
     });
 
     describe("Non-invertible formulas marked as such", () => {
@@ -479,30 +444,13 @@ describe("Inverting", () => {
                     const result = formula.evaluate();
                     expect(formula.invert(result)).compare_tolerance(2);
                 });
-                test(`${name}(const, var).invert()`, () => {
-                    const formula = Formula[name](constant, variable);
-                    const result = formula.evaluate();
-                    expect(formula.invert(result)).compare_tolerance(2);
-                });
-            })
-        );
-        invertibleTwoParamFunctionNames.forEach(name =>
-            describe(name, () => {
-                test(`${name}(var, const, const).invert()`, () => {
-                    const formula = Formula[name](variable, constant, constant);
-                    const result = formula.evaluate();
-                    expect(formula.invert(result)).compare_tolerance(2);
-                });
-                test(`${name}(const, var, const).invert()`, () => {
-                    const formula = Formula[name](constant, variable, constant);
-                    const result = formula.evaluate();
-                    expect(formula.invert(result)).compare_tolerance(2);
-                });
-                test(`${name}(const, const, var).invert()`, () => {
-                    const formula = Formula[name](constant, constant, variable);
-                    const result = formula.evaluate();
-                    expect(formula.invert(result)).compare_tolerance(2);
-                });
+                if (name !== "layeradd") {
+                    test(`${name}(const, var).invert()`, () => {
+                        const formula = Formula[name](constant, variable);
+                        const result = formula.evaluate();
+                        expect(formula.invert(result)).compare_tolerance(2);
+                    });
+                }
             })
         );
     });
@@ -530,7 +478,7 @@ describe("Inverting", () => {
     test("Inverting with non-invertible sections", () => {
         const formula = Formula.add(variable, constant.ceil());
         expect(formula.isInvertible()).toBe(true);
-        expect(formula.invert(10)).compare_tolerance(7);
+        expect(formula.invert(10)).compare_tolerance(0);
     });
 });
 
@@ -562,8 +510,10 @@ describe("Integrating", () => {
             describe(name, () => {
                 test(`${name}(var, const) is marked as integrable`, () =>
                     checkFormula(Formula[name](variable, constant)));
-                test(`${name}(const, var) is marked as integrable`, () =>
-                    checkFormula(Formula[name](constant, variable)));
+                if (name !== "log" && name !== "root") {
+                    test(`${name}(const, var) is marked as integrable`, () =>
+                        checkFormula(Formula[name](constant, variable)));
+                }
                 test(`${name}(var, var) is marked as not integrable`, () =>
                     expect(Formula[name](variable, variable).isIntegrable()).toBe(false));
             });
@@ -645,8 +595,10 @@ describe("Inverting integrals", () => {
             describe(name, () => {
                 test(`${name}(var, const) is marked as having an invertible integral`, () =>
                     checkFormula(Formula[name](variable, constant)));
-                test(`${name}(const, var) is marked as having an invertible integral`, () =>
-                    checkFormula(Formula[name](constant, variable)));
+                if (name !== "log" && name !== "root") {
+                    test(`${name}(const, var) is marked as having an invertible integral`, () =>
+                        checkFormula(Formula[name](constant, variable)));
+                }
                 test(`${name}(var, var) is marked as not having an invertible integral`, () => {
                     const formula = Formula[name](variable, variable);
                     expect(formula.isIntegralInvertible()).toBe(false);
@@ -871,6 +823,11 @@ describe("Conditionals", () => {
 });
 
 describe("Custom Formulas", () => {
+    let variable: GenericFormula;
+    beforeAll(() => {
+        variable = Formula.variable(1);
+    });
+
     describe("Formula with evaluate", () => {
         test("Zero input evaluates correctly", () =>
             expect(new Formula({ inputs: [], evaluate: () => 10 }).evaluate()).compare_tolerance(
@@ -887,28 +844,28 @@ describe("Custom Formulas", () => {
     });
 
     describe("Formula with invert", () => {
-        test("Zero input inverts correctly", () =>
-            expect(
+        test("Zero input does not invert", () =>
+            expect(() =>
                 new Formula({
                     inputs: [],
                     evaluate: () => 6,
                     invert: value => value,
                     hasVariable: true
                 }).invert(10)
-            ).compare_tolerance(10));
+            ).toThrow());
         test("One input inverts correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1],
+                    inputs: [variable],
                     evaluate: () => 10,
-                    invert: (value, v1) => v1,
+                    invert: (value, v1) => v1.evaluate(),
                     hasVariable: true
                 }).invert(10)
             ).compare_tolerance(1));
         test("Two inputs inverts correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1, 2],
+                    inputs: [variable, 2],
                     evaluate: () => 10,
                     invert: (value, v1, v2) => v2,
                     hasVariable: true
@@ -918,7 +875,7 @@ describe("Custom Formulas", () => {
 
     describe("Formula with integrate", () => {
         test("Zero input integrates correctly", () =>
-            expect(
+            expect(() =>
                 new Formula({
                     inputs: [],
                     evaluate: () => 10,
@@ -928,7 +885,7 @@ describe("Custom Formulas", () => {
         test("One input integrates correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1],
+                    inputs: [variable],
                     evaluate: () => 10,
                     integrate: (val, v1) => val ?? 20
                 }).evaluateIntegral()
@@ -936,7 +893,7 @@ describe("Custom Formulas", () => {
         test("Two inputs integrates correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1, 2],
+                    inputs: [variable, 2],
                     evaluate: (v1, v2) => 10,
                     integrate: (v1, v2) => 3
                 }).evaluateIntegral()
@@ -944,19 +901,19 @@ describe("Custom Formulas", () => {
     });
 
     describe("Formula with invertIntegral", () => {
-        test("Zero input inverts integral correctly", () =>
-            expect(
+        test("Zero input does not invert integral", () =>
+            expect(() =>
                 new Formula({
                     inputs: [],
                     evaluate: () => 10,
                     invertIntegral: () => 1,
                     hasVariable: true
                 }).invertIntegral(8)
-            ).compare_tolerance(1));
+            ).toThrow());
         test("One input inverts integral correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1],
+                    inputs: [variable],
                     evaluate: () => 10,
                     invertIntegral: (val, v1) => 1,
                     hasVariable: true
@@ -965,7 +922,7 @@ describe("Custom Formulas", () => {
         test("Two inputs inverts integral correctly", () =>
             expect(
                 new Formula({
-                    inputs: [1, 2],
+                    inputs: [variable, 2],
                     evaluate: (v1, v2) => 10,
                     invertIntegral: (v1, v2) => 1,
                     hasVariable: true
@@ -977,36 +934,36 @@ describe("Custom Formulas", () => {
 describe("Buy Max", () => {
     let resource: Resource;
     beforeAll(() => {
-        resource = createResource(ref(10));
+        resource = createResource(ref(1000));
     });
-    describe("With spending", () => {
+    describe("Without spending", () => {
         test("Throws on formula with non-invertible integral", () => {
             const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
             expect(() => maxAffordable.value).toThrow();
         });
         // https://www.desmos.com/calculator/5vgletdc1p
         test("Calculates max affordable and cost correctly", () => {
-            const variable = Formula.variable(10);
-            const formula = Formula.pow(1.05, variable);
+            const variable = Formula.variable(0);
+            const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource, false);
             expect(maxAffordable.value).compare_tolerance(47);
             expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance(
-                Decimal.pow(1.05, 47)
+                Decimal.pow(1.05, 47).times(100)
             );
         });
     });
-    describe("Without spending", () => {
+    describe("With spending", () => {
         test("Throws on non-invertible formula", () => {
             const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
             expect(() => maxAffordable.value).toThrow();
         });
         // https://www.desmos.com/calculator/5vgletdc1p
         test("Calculates max affordable and cost correctly", () => {
-            const variable = Formula.variable(10);
-            const formula = Formula.pow(1.05, variable);
+            const variable = Formula.variable(0);
+            const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource);
             expect(maxAffordable.value).compare_tolerance(7);
-            expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35);
+            expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735);
         });
     });
 });
diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts
index 57e574b..55474b9 100644
--- a/tests/game/requirements.test.ts
+++ b/tests/game/requirements.test.ts
@@ -2,6 +2,7 @@ import { Visibility } from "features/feature";
 import { createResource, Resource } from "features/resources/resource";
 import Formula from "game/formulas";
 import {
+    CostRequirement,
     createBooleanRequirement,
     createCostRequirement,
     createVisibilityRequirement,
@@ -17,19 +18,18 @@ import "../utils";
 describe("Creating cost requirement", () => {
     describe("Minimal requirement", () => {
         let resource: Resource;
-        let requirement: Requirement;
+        let requirement: CostRequirement;
         beforeAll(() => {
             resource = createResource(ref(10));
             requirement = createCostRequirement(() => ({
                 resource,
-                cost: 10
+                cost: 10,
+                spendResources: false
             }));
         });
 
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        test("resource pass-through", () => expect((requirement as any).resource).toBe(resource));
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        test("cost pass-through", () => expect((requirement as any).cost).toBe(10));
+        test("resource pass-through", () => expect(requirement.resource).toBe(resource));
+        test("cost pass-through", () => expect(requirement.cost).toBe(10));
 
         test("partialDisplay exists", () =>
             expect(typeof requirement.partialDisplay).toBe("function"));
@@ -41,23 +41,22 @@ describe("Creating cost requirement", () => {
         });
         test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible));
         test("requires pay", () => expect(requirement.requiresPay).toBe(true));
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        test("spends resources", () => expect((requirement as any).spendResources).toBe(true));
+        test("does not spend resources", () => expect(requirement.spendResources).toBe(false));
         test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false));
     });
 
     describe("Fully customized", () => {
         let resource: Resource;
-        let requirement: Requirement;
+        let requirement: CostRequirement;
         beforeAll(() => {
             resource = createResource(ref(10));
             requirement = createCostRequirement(() => ({
                 resource,
-                cost: 10,
+                cost: Formula.variable(resource).times(10),
                 visibility: Visibility.None,
                 requiresPay: false,
                 maximize: true,
-                spendResources: false,
+                spendResources: true,
                 // eslint-disable-next-line @typescript-eslint/no-empty-function
                 pay() {}
             }));
@@ -69,9 +68,7 @@ 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("does not spend resources", () =>
-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            expect((requirement as any).spendResources).toBe(false));
+        test("spends resources", () => expect(requirement.spendResources).toBe(true));
         test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true));
     });
 
@@ -79,7 +76,8 @@ describe("Creating cost requirement", () => {
         const resource = createResource(ref(10));
         const requirement = createCostRequirement(() => ({
             resource,
-            cost: 10
+            cost: 10,
+            spendResources: false
         }));
         expect(unref(requirement.requirementMet)).toBe(true);
     });
@@ -88,7 +86,8 @@ describe("Creating cost requirement", () => {
         const resource = createResource(ref(10));
         const requirement = createCostRequirement(() => ({
             resource,
-            cost: 100
+            cost: 100,
+            spendResources: false
         }));
         expect(unref(requirement.requirementMet)).toBe(false);
     });
@@ -148,7 +147,8 @@ describe("Checking maximum levels of requirements met", () => {
             createBooleanRequirement(true),
             createCostRequirement(() => ({
                 resource: createResource(ref(10)),
-                cost: Formula.variable(0)
+                cost: Formula.variable(0),
+                spendResources: false
             }))
         ];
         expect(maxRequirementsMet(requirements)).compare_tolerance(0);
@@ -159,7 +159,8 @@ describe("Checking maximum levels of requirements met", () => {
             createBooleanRequirement(true),
             createCostRequirement(() => ({
                 resource: createResource(ref(10)),
-                cost: Formula.variable(0)
+                cost: Formula.variable(0),
+                spendResources: false
             }))
         ];
         expect(maxRequirementsMet(requirements)).compare_tolerance(10);
@@ -171,11 +172,13 @@ test("Paying requirements", () => {
     const noPayment = createCostRequirement(() => ({
         resource,
         cost: 10,
-        requiresPay: false
+        requiresPay: false,
+        spendResources: false
     }));
     const payment = createCostRequirement(() => ({
         resource,
-        cost: 10
+        cost: 10,
+        spendResources: false
     }));
     payRequirements([noPayment, payment]);
     expect(resource.value).compare_tolerance(90);

From 3078584043798ed794d7d65dce028ce5a565ed35 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 11:43:44 -0500
Subject: [PATCH 05/36] Rewrite integration to handle nested formulas properly
 And more clearly defines which formulas are supported

---
 src/game/formulas.ts        | 472 ++++++++++++++++++++++++++++--------
 tests/game/formulas.test.ts |  52 +++-
 2 files changed, 416 insertions(+), 108 deletions(-)

diff --git a/src/game/formulas.ts b/src/game/formulas.ts
index 3c0b379..7fd489b 100644
--- a/src/game/formulas.ts
+++ b/src/game/formulas.ts
@@ -1,7 +1,7 @@
 import { Resource } from "features/resources/resource";
 import Decimal, { DecimalSource } from "util/bignum";
 import { Computable, convertComputable, ProcessedComputable } from "util/computed";
-import { computed, ComputedRef, ref, Ref, unref } from "vue";
+import { computed, ComputedRef, ref, unref } from "vue";
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export type GenericFormula = Formula<any>;
@@ -16,6 +16,8 @@ export type InvertibleIntegralFormula = GenericFormula & {
     invertIntegral: (value: DecimalSource) => DecimalSource;
 };
 
+export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
+
 export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
     | {
           variable: ProcessedComputable<DecimalSource>;
@@ -34,6 +36,18 @@ export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
           integrate?: (
               this: Formula<T>,
               variable: DecimalSource | undefined,
+              stack: SubstitutionStack | undefined,
+              ...inputs: T
+          ) => DecimalSource;
+          integrateInner?: (
+              this: Formula<T>,
+              variable: DecimalSource | undefined,
+              stack: SubstitutionStack | undefined,
+              ...inputs: T
+          ) => DecimalSource;
+          applySubstitution?: (
+              this: Formula<T>,
+              variable: DecimalSource,
               ...inputs: T
           ) => DecimalSource;
           invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
@@ -60,6 +74,17 @@ function passthrough(value: DecimalSource) {
     return value;
 }
 
+function integrateVariable(variable: DecimalSource) {
+    return Decimal.pow(variable, 2).div(2);
+}
+
+function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) {
+    if (variable == null && this.innermostVariable == null) {
+        throw "Cannot integrate non-existent variable";
+    }
+    return variable ?? unref(this.innermostVariable);
+}
+
 function invertNeg(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.neg(value));
@@ -67,8 +92,19 @@ function invertNeg(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) {
-    return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg();
+function integrateNeg(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        return Decimal.neg(lhs.evaluateIntegral(variable, stack));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+function applySubstitutionNeg(value: DecimalSource) {
+    return Decimal.neg(value);
 }
 
 function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -80,17 +116,40 @@ function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAdd(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integrateAdd(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.pow(x, 2)
-            .div(2)
-            .add(Decimal.times(unrefFormulaSource(rhs), x));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(rhs),
+            variable ?? unref(lhs.innermostVariable) ?? 0
+        ).add(x);
     } else if (hasVariable(rhs)) {
-        const x = unrefFormulaSource(rhs, variable);
-        return Decimal.pow(x, 2)
-            .div(2)
-            .add(Decimal.times(unrefFormulaSource(lhs), x));
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(lhs),
+            variable ?? unref(rhs.innermostVariable) ?? 0
+        ).add(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+function integrateInnerAdd(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.add(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.add(x, unrefFormulaSource(lhs));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -115,15 +174,40 @@ function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateSub(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integrateSub(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.pow(x, 2)
-            .div(2)
-            .add(Decimal.times(unrefFormulaSource(rhs), x).neg());
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(
+            x,
+            Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
+        );
     } else if (hasVariable(rhs)) {
-        const x = unrefFormulaSource(rhs, variable);
-        return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x);
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(lhs),
+            variable ?? unref(rhs.innermostVariable) ?? 0
+        ).sub(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+function integrateInnerSub(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(x, unrefFormulaSource(lhs));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -148,17 +232,31 @@ function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateMul(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integrateMul(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.times(x, unrefFormulaSource(rhs));
     } else if (hasVariable(rhs)) {
-        const x = unrefFormulaSource(rhs, variable);
-        return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs));
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(x, unrefFormulaSource(lhs));
     }
     throw "Could not integrate due to no input being a variable";
 }
 
+function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return Decimal.div(value, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        return Decimal.div(value, unrefFormulaSource(lhs));
+    }
+    throw "Could not apply substitution due to no input being a variable";
+}
+
 function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         const b = unrefFormulaSource(rhs);
@@ -179,17 +277,31 @@ function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateDiv(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integrateDiv(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs)));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.div(x, unrefFormulaSource(rhs));
     } else if (hasVariable(rhs)) {
-        const x = unrefFormulaSource(rhs, variable);
-        return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs)));
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.div(unrefFormulaSource(lhs), x);
     }
     throw "Could not integrate due to no input being a variable";
 }
 
+function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return Decimal.mul(value, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        return Decimal.mul(value, unrefFormulaSource(lhs));
+    }
+    throw "Could not apply substitution due to no input being a variable";
+}
+
 function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         const b = unrefFormulaSource(rhs);
@@ -208,9 +320,13 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateRecip(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x);
     }
     throw "Could not integrate due to no input being a variable";
@@ -230,10 +346,14 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateLog10(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10)));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -256,13 +376,18 @@ function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateLog(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integrateLog(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.times(
-            x,
-            Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs)))
-        );
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x)
+            .sub(1)
+            .times(x)
+            .div(Decimal.ln(unrefFormulaSource(rhs)));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -282,10 +407,14 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateLog2(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2)));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -304,10 +433,14 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateLn(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
-        return Decimal.times(x, Decimal.ln(x).sub(1));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x);
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -328,13 +461,18 @@ function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource)
     throw "Could not invert due to no input being a variable";
 }
 
-function integratePow(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) {
+function integratePow(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         const pow = Decimal.add(unrefFormulaSource(rhs), 1);
         return Decimal.pow(x, pow).div(pow);
     } else if (hasVariable(rhs)) {
-        const x = unrefFormulaSource(rhs, variable);
+        const x = rhs.evaluateIntegral(variable, stack);
         const b = unrefFormulaSource(lhs);
         return Decimal.pow(b, x).div(Decimal.ln(b));
     }
@@ -359,9 +497,13 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integratePow10(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
     }
     throw "Could not integrate due to no input being a variable";
@@ -387,15 +529,18 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou
 
 function integratePowBase(
     variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs, variable);
-        return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b));
+        const x = lhs.evaluateIntegral(variable, stack);
+        const b = unrefFormulaSource(rhs);
+        return Decimal.pow(b, x).div(Decimal.ln(b));
     } else if (hasVariable(rhs)) {
-        const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1);
-        return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator);
+        const x = rhs.evaluateIntegral(variable, stack);
+        const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
+        return Decimal.pow(x, denominator).div(denominator);
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -422,14 +567,14 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource
 
 function integrateRoot(
     variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1))
-            .times(b)
-            .div(Decimal.add(b, 1));
+        const x = lhs.evaluateIntegral(variable, stack);
+        const a = unrefFormulaSource(rhs);
+        return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -454,9 +599,14 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateExp(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.exp(unrefFormulaSource(lhs, variable));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.exp(x);
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -580,9 +730,14 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateSin(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.cos(unrefFormulaSource(lhs, variable)).neg();
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cos(x).neg();
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -594,9 +749,14 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateCos(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.sin(unrefFormulaSource(lhs, variable));
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sin(x);
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -608,9 +768,14 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateTan(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg();
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cos(x).ln().neg();
     }
     throw "Could not integrate due to no input being a variable";
 }
@@ -622,9 +787,13 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAsin(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.asin(x)
             .times(x)
             .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
@@ -639,9 +808,13 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAcos(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.acos(x)
             .times(x)
             .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
@@ -656,9 +829,13 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAtan(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.atan(x)
             .times(x)
             .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
@@ -673,9 +850,13 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateSinh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cosh(x);
     }
     throw "Could not integrate due to no input being a variable";
@@ -688,9 +869,13 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateCosh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.sinh(x);
     }
     throw "Could not integrate due to no input being a variable";
@@ -703,9 +888,13 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateTanh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cosh(x).ln();
     }
     throw "Could not integrate due to no input being a variable";
@@ -718,9 +907,13 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAsinh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
     }
     throw "Could not integrate due to no input being a variable";
@@ -733,9 +926,13 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAcosh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.acosh(x)
             .times(x)
             .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
@@ -750,9 +947,13 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
     throw "Could not invert due to no input being a variable";
 }
 
-function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) {
+function integrateAtanh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        const x = unrefFormulaSource(lhs, variable);
+        const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.atanh(x)
             .times(x)
             .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
@@ -776,7 +977,21 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         | ((value: DecimalSource, ...inputs: T) => DecimalSource)
         | undefined;
     private readonly internalIntegrate:
-        | ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource)
+        | ((
+              variable: DecimalSource | undefined,
+              stack: SubstitutionStack | undefined,
+              ...inputs: T
+          ) => DecimalSource)
+        | undefined;
+    private readonly internalIntegrateInner:
+        | ((
+              variable: DecimalSource | undefined,
+              stack: SubstitutionStack | undefined,
+              ...inputs: T
+          ) => DecimalSource)
+        | undefined;
+    private readonly applySubstitution:
+        | ((variable: DecimalSource, ...inputs: T) => DecimalSource)
         | undefined;
     private readonly internalInvertIntegral:
         | ((value: DecimalSource, ...inputs: T) => DecimalSource)
@@ -791,6 +1006,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             this.inputs = [options.variable] as T;
             this.internalHasVariable = true;
             this.innermostVariable = options.variable;
+            this.internalIntegrate =
+                integrateVariable as unknown as Formula<T>["internalIntegrate"];
+            this.internalIntegrateInner =
+                integrateVariableInner as unknown as Formula<T>["internalIntegrateInner"];
+            this.applySubstitution = passthrough as unknown as Formula<T>["applySubstitution"];
             return;
         }
         // Constant case
@@ -803,7 +1023,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             return;
         }
 
-        const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options;
+        const {
+            inputs,
+            evaluate,
+            invert,
+            integrate,
+            integrateInner,
+            applySubstitution,
+            invertIntegral,
+            hasVariable
+        } = options;
         if (invert == null && invertIntegral == null && hasVariable) {
             throw "A formula cannot be marked as having a variable if it is not invertible";
         }
@@ -811,6 +1040,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this.inputs = inputs;
         this.internalEvaluate = evaluate;
         this.internalIntegrate = integrate;
+        this.internalIntegrateInner = integrateInner;
+        this.applySubstitution = applySubstitution;
 
         const numVariables = inputs.filter(
             input => input instanceof Formula && input.hasVariable()
@@ -874,7 +1105,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                     unrefFormulaSource(input, variable)
                 ) as GuardedFormulasToDecimals<T>)
             ) ??
-            variable ??
+            (this.internalHasVariable ? variable : null) ??
             unrefFormulaSource(this.inputs[0])
         );
     }
@@ -894,17 +1125,57 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     }
 
     /**
-     * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable
+     * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/).
      * @param variable Optionally override the value of the variable while evaluating
+     * @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas
      * @see {@link isIntegrable}
      */
-    evaluateIntegral(variable?: DecimalSource): DecimalSource {
-        if (this.internalIntegrate) {
-            return this.internalIntegrate.call(this, variable, ...this.inputs);
-        } else if (this.inputs.length === 1 && this.internalHasVariable) {
-            return variable ?? unrefFormulaSource(this.inputs[0]);
+    evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource {
+        if (stack == null) {
+            // "Outer" part of the formula
+            if (this.applySubstitution == null) {
+                // We're the complex operation of this formula
+                stack = [];
+                if (this.internalIntegrate == null) {
+                    throw "Cannot integrate formula with non-existent operation";
+                }
+                let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
+                stack.forEach(func => (value = func(value)));
+                return value;
+            } else {
+                // Continue digging into the formula
+                if (this.internalIntegrate) {
+                    return this.internalIntegrate.call(this, variable, undefined, ...this.inputs);
+                } else if (this.inputs.length === 1 && this.internalHasVariable) {
+                    return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0]));
+                }
+                throw "Cannot integrate formula without variable";
+            }
+        } else {
+            // "Inner" part of the formula
+            if (this.applySubstitution == null) {
+                throw "Cannot have two complex operations in an integrable formula";
+            }
+            stack.push((variable: DecimalSource) =>
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                this.applySubstitution!.call(this, variable, ...this.inputs)
+            );
+            if (this.internalIntegrateInner) {
+                return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
+            } else if (this.internalIntegrate) {
+                return this.internalIntegrate.call(this, variable, stack, ...this.inputs);
+            } else if (this.inputs.length === 1 && this.internalHasVariable) {
+                return variable ?? unrefFormulaSource(this.inputs[0]);
+            }
+            throw "Cannot integrate formula without variable";
         }
-        throw "Cannot integrate formula without variable";
+    }
+
+    calculateConstantOfIntegration() {
+        // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
+        const integral = this.evaluateIntegral(1);
+        const actualCost = this.evaluate(0);
+        return Decimal.sub(actualCost, integral);
     }
 
     /**
@@ -1070,6 +1341,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.neg,
             invert: invertNeg,
+            applySubstitution: applySubstitutionNeg,
             integrate: integrateNeg
         });
     }
@@ -1116,6 +1388,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.add,
             invert: invertAdd,
             integrate: integrateAdd,
+            integrateInner: integrateInnerAdd,
+            applySubstitution: passthrough,
             invertIntegral: invertIntegrateAdd
         });
     }
@@ -1135,6 +1409,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.sub,
             invert: invertSub,
             integrate: integrateSub,
+            integrateInner: integrateInnerSub,
+            applySubstitution: passthrough,
             invertIntegral: invertIntegrateSub
         });
     }
@@ -1160,6 +1436,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.mul,
             invert: invertMul,
             integrate: integrateMul,
+            applySubstitution: applySubstitutionMul,
             invertIntegral: invertIntegrateMul
         });
     }
@@ -1185,6 +1462,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.div,
             invert: invertDiv,
             integrate: integrateDiv,
+            applySubstitution: applySubstitutionDiv,
             invertIntegral: invertIntegrateDiv
         });
     }
@@ -2180,7 +2458,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
  * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
  * @param formula The formula to use for calculating buy max from
  * @param resource The resource used when purchasing (is only read from)
- * @param spendResources Whether or not to count spent resources on each purchase or not
+ * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases
  */
 export function calculateMaxAffordable(
     formula: InvertibleFormula,
@@ -2219,7 +2497,7 @@ export function calculateMaxAffordable(
  * Utility for calculating the cost of a formula for a given amount of purchases. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
  * @param formula The formula to use for calculating buy max from
  * @param amountToBuy The amount of purchases to calculate the cost for
- * @param spendResources Whether or not to count spent resources on each purchase or not
+ * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost
  */
 export function calculateCost(
     formula: InvertibleFormula,
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 43d876f..b45112d 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -8,7 +8,7 @@ import Formula, {
 } from "game/formulas";
 import Decimal, { DecimalSource } from "util/bignum";
 import { beforeAll, describe, expect, test } from "vitest";
-import { ref } from "vue";
+import { ref, unref } from "vue";
 import "../utils";
 
 type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
@@ -486,7 +486,7 @@ describe("Integrating", () => {
     let variable: GenericFormula;
     let constant: GenericFormula;
     beforeAll(() => {
-        variable = Formula.variable(10);
+        variable = Formula.variable(ref(10));
         constant = Formula.constant(10);
     });
 
@@ -564,8 +564,24 @@ describe("Integrating", () => {
     describe.todo("Integrable formulas integrate correctly");
 
     test("Integrating nested formulas", () => {
-        const formula = Formula.add(variable, constant).times(constant);
-        expect(formula.evaluateIntegral()).compare_tolerance(1500);
+        const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
+        const actualCost = new Array(10)
+            .fill(null)
+            .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
+        const calculatedCost = Decimal.add(
+            formula.evaluateIntegral(),
+            formula.calculateConstantOfIntegration()
+        );
+        // Check if the calculated cost is within 10% of the actual cost,
+        // because this is an approximation
+        expect(
+            Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+        ).toBeLessThan(0.1);
+    });
+
+    test("Integrating nested complex formulas", () => {
+        const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
+        expect(() => formula.evaluateIntegral()).toThrow();
     });
 });
 
@@ -652,8 +668,13 @@ describe("Inverting integrals", () => {
     describe.todo("Invertible Integral formulas invert correctly");
 
     test("Inverting integral of nested formulas", () => {
-        const formula = Formula.add(variable, constant).times(constant);
-        expect(formula.invertIntegral(1500)).compare_tolerance(10);
+        const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
+        expect(formula.invertIntegral(7000000)).compare_tolerance(10);
+    });
+
+    test("Inverting integral of nested complex formulas", () => {
+        const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
+        expect(() => formula.invertIntegral(100)).toThrow();
     });
 
     describe("Inverting integral pass-throughs", () => {
@@ -875,7 +896,7 @@ describe("Custom Formulas", () => {
 
     describe("Formula with integrate", () => {
         test("Zero input integrates correctly", () =>
-            expect(() =>
+            expect(
                 new Formula({
                     inputs: [],
                     evaluate: () => 10,
@@ -887,7 +908,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable],
                     evaluate: () => 10,
-                    integrate: (val, v1) => val ?? 20
+                    integrate: (val, stack, v1) => val ?? 20
                 }).evaluateIntegral()
             ).compare_tolerance(20));
         test("Two inputs integrates correctly", () =>
@@ -941,7 +962,7 @@ describe("Buy Max", () => {
             const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
             expect(() => maxAffordable.value).toThrow();
         });
-        // https://www.desmos.com/calculator/5vgletdc1p
+        // https://www.desmos.com/calculator/7ffthe7wi8
         test("Calculates max affordable and cost correctly", () => {
             const variable = Formula.variable(0);
             const formula = Formula.pow(1.05, variable).times(100);
@@ -957,13 +978,22 @@ describe("Buy Max", () => {
             const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
             expect(() => maxAffordable.value).toThrow();
         });
-        // https://www.desmos.com/calculator/5vgletdc1p
+        // https://www.desmos.com/calculator/7ffthe7wi8
         test("Calculates max affordable and cost correctly", () => {
             const variable = Formula.variable(0);
             const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource);
             expect(maxAffordable.value).compare_tolerance(7);
-            expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735);
+
+            const actualCost = new Array(7)
+                .fill(null)
+                .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            // Check if the calculated cost is within 10% of the actual cost,
+            // because this is an approximation
+            expect(
+                Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+            ).toBeLessThan(0.1);
         });
     });
 });

From bb0f83e75ab3c051a1ddf4e344d7e7c18d71c049 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 11:47:39 -0500
Subject: [PATCH 06/36] Make tests faster

---
 tests/game/formulas.test.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index b45112d..9937d30 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -8,12 +8,12 @@ import Formula, {
 } from "game/formulas";
 import Decimal, { DecimalSource } from "util/bignum";
 import { beforeAll, describe, expect, test } from "vitest";
-import { ref, unref } from "vue";
+import { ref } from "vue";
 import "../utils";
 
 type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
 
-const testValues = ["-1e400", 0, 0.25] as const;
+const testValues = [-1, "0", Decimal.dOne] as const;
 
 const invertibleZeroParamFunctionNames = [
     "neg",

From 6e4c61155a21e126069313187a96aabf5d5cbe4f Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 19:04:07 -0500
Subject: [PATCH 07/36] Re-organize formulas for better
 readability/maintainability

---
 src/data/common.tsx                 |    3 +-
 src/game/{ => formulas}/formulas.ts | 1361 ++++-----------------------
 src/game/formulas/operations.ts     |  904 ++++++++++++++++++
 src/game/formulas/types.d.ts        |   79 ++
 src/game/requirements.tsx           |    8 +-
 tests/game/formulas.test.ts         |    5 +-
 tests/game/requirements.test.ts     |    2 +-
 7 files changed, 1175 insertions(+), 1187 deletions(-)
 rename src/game/{ => formulas}/formulas.ts (56%)
 create mode 100644 src/game/formulas/operations.ts
 create mode 100644 src/game/formulas/types.d.ts

diff --git a/src/data/common.tsx b/src/data/common.tsx
index ec3317e..b1d5398 100644
--- a/src/data/common.tsx
+++ b/src/data/common.tsx
@@ -8,7 +8,8 @@ import { GenericMilestone } from "features/milestones/milestone";
 import { displayResource, Resource } from "features/resources/resource";
 import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
 import { createTreeNode } from "features/trees/tree";
-import Formula, { FormulaSource, GenericFormula, InvertibleFormula } from "game/formulas";
+import Formula from "game/formulas/formulas";
+import type { FormulaSource, GenericFormula } from "game/formulas/types";
 import type { Modifier } from "game/modifiers";
 import type { Persistent } from "game/persistence";
 import { DefaultValue, persistent } from "game/persistence";
diff --git a/src/game/formulas.ts b/src/game/formulas/formulas.ts
similarity index 56%
rename from src/game/formulas.ts
rename to src/game/formulas/formulas.ts
index 7fd489b..885adee 100644
--- a/src/game/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -2,965 +2,44 @@ import { Resource } from "features/resources/resource";
 import Decimal, { DecimalSource } from "util/bignum";
 import { Computable, convertComputable, ProcessedComputable } from "util/computed";
 import { computed, ComputedRef, ref, unref } from "vue";
+import type {
+    EvaluateFunction,
+    FormulaOptions,
+    FormulaSource,
+    GeneralFormulaOptions,
+    GenericFormula,
+    GuardedFormulasToDecimals,
+    IntegrableFormula,
+    IntegrateFunction,
+    InternalFormulaProperties,
+    InvertFunction,
+    InvertibleFormula,
+    InvertibleIntegralFormula,
+    InvertIntegralFunction,
+    SubstitutionFunction,
+    SubstitutionStack
+} from "./types";
+import * as ops from "./operations";
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type GenericFormula = Formula<any>;
-export type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
-export type InvertibleFormula = GenericFormula & {
-    invert: (value: DecimalSource) => DecimalSource;
-};
-export type IntegrableFormula = GenericFormula & {
-    evaluateIntegral: (variable?: DecimalSource) => DecimalSource;
-};
-export type InvertibleIntegralFormula = GenericFormula & {
-    invertIntegral: (value: DecimalSource) => DecimalSource;
-};
-
-export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
-
-export type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
-    | {
-          variable: ProcessedComputable<DecimalSource>;
-      }
-    | {
-          inputs: [FormulaSource];
-      }
-    | {
-          inputs: T;
-          evaluate: (this: Formula<T>, ...inputs: GuardedFormulasToDecimals<T>) => DecimalSource;
-          invert?: (
-              this: Formula<T>,
-              value: DecimalSource,
-              ...inputs: [...T, ...unknown[]]
-          ) => DecimalSource;
-          integrate?: (
-              this: Formula<T>,
-              variable: DecimalSource | undefined,
-              stack: SubstitutionStack | undefined,
-              ...inputs: T
-          ) => DecimalSource;
-          integrateInner?: (
-              this: Formula<T>,
-              variable: DecimalSource | undefined,
-              stack: SubstitutionStack | undefined,
-              ...inputs: T
-          ) => DecimalSource;
-          applySubstitution?: (
-              this: Formula<T>,
-              variable: DecimalSource,
-              ...inputs: T
-          ) => DecimalSource;
-          invertIntegral?: (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
-          hasVariable?: boolean;
-      };
-
-function hasVariable(value: FormulaSource): value is InvertibleFormula {
+export function hasVariable(value: FormulaSource): value is InvertibleFormula {
     return value instanceof Formula && value.hasVariable();
 }
 
-// It's really hard to type mapped tuples, but these classes seem to manage
-type FormulasToDecimals<T extends FormulaSource[]> = {
-    [K in keyof T]: DecimalSource;
-};
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type TupleGuard<T extends any[]> = T extends any[] ? FormulasToDecimals<T> : never;
-type GuardedFormulasToDecimals<T extends FormulaSource[]> = TupleGuard<T>;
-
 export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) {
     return value instanceof Formula ? value.evaluate(variable) : unref(value);
 }
 
-function passthrough(value: DecimalSource) {
-    return value;
-}
-
 function integrateVariable(variable: DecimalSource) {
     return Decimal.pow(variable, 2).div(2);
 }
 
-function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) {
+function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) {
     if (variable == null && this.innermostVariable == null) {
         throw "Cannot integrate non-existent variable";
     }
     return variable ?? unref(this.innermostVariable);
 }
 
-function invertNeg(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.neg(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateNeg(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        return Decimal.neg(lhs.evaluateIntegral(variable, stack));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function applySubstitutionNeg(value: DecimalSource) {
-    return Decimal.neg(value);
-}
-
-function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAdd(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(rhs),
-            variable ?? unref(lhs.innermostVariable) ?? 0
-        ).add(x);
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(lhs),
-            variable ?? unref(rhs.innermostVariable) ?? 0
-        ).add(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function integrateInnerAdd(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.add(x, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.add(x, unrefFormulaSource(lhs));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateSub(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(
-            x,
-            Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
-        );
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(lhs),
-            variable ?? unref(rhs.innermostVariable) ?? 0
-        ).sub(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function integrateInnerSub(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(x, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(x, unrefFormulaSource(lhs));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateMul(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.times(x, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(x, unrefFormulaSource(lhs));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return Decimal.div(value, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        return Decimal.div(value, unrefFormulaSource(lhs));
-    }
-    throw "Could not apply substitution due to no input being a variable";
-}
-
-function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateDiv(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.div(x, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.div(unrefFormulaSource(lhs), x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return Decimal.mul(value, unrefFormulaSource(rhs));
-    } else if (hasVariable(rhs)) {
-        return Decimal.mul(value, unrefFormulaSource(lhs));
-    }
-    throw "Could not apply substitution due to no input being a variable";
-}
-
-function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertRecip(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.recip(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateRecip(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertLog10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.pow10(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateLog10(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1))
-        );
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateLog(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x)
-            .sub(1)
-            .times(x)
-            .div(Decimal.ln(unrefFormulaSource(rhs)));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
-        return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertLog2(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.pow(2, value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateLog2(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertLn(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateLn(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integratePow(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const pow = Decimal.add(unrefFormulaSource(rhs), 1);
-        return Decimal.pow(x, pow).div(pow);
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        const b = unrefFormulaSource(lhs);
-        return Decimal.pow(b, x).div(Decimal.ln(b));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
-    } else if (hasVariable(rhs)) {
-        const denominator = Decimal.ln(unrefFormulaSource(lhs));
-        return rhs.invert(Decimal.times(denominator, value).ln().div(denominator));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertPow10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.root(value, 10));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integratePow10(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp()
-        );
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integratePowBase(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const b = unrefFormulaSource(rhs);
-        return Decimal.pow(b, x).div(Decimal.ln(b));
-    } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
-        return Decimal.pow(x, denominator).div(denominator);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegratePowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs))));
-    } else if (hasVariable(rhs)) {
-        return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateRoot(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const a = unrefFormulaSource(rhs);
-        return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(
-            Decimal.add(b, 1)
-                .times(value)
-                .div(b)
-                .pow(Decimal.div(b, Decimal.add(b, 1)))
-        );
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertExp(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.ln(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateExp(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.exp(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function tetrate(
-    value: DecimalSource,
-    height: DecimalSource = 2,
-    payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
-) {
-    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
-    return Decimal.tetrate(value, heightNumber, payload);
-}
-
-function invertTetrate(
-    value: DecimalSource,
-    base: FormulaSource,
-    height: FormulaSource,
-    payload: FormulaSource
-) {
-    if (hasVariable(base)) {
-        return base.invert(Decimal.ssqrt(value));
-    }
-    // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
-}
-
-function iteratedexp(
-    value: DecimalSource,
-    height: DecimalSource = 2,
-    payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
-) {
-    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
-    return Decimal.iteratedexp(value, heightNumber, new Decimal(payload));
-}
-
-function invertIteratedExp(
-    value: DecimalSource,
-    lhs: FormulaSource,
-    height: FormulaSource,
-    payload: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.iteratedlog(
-                value,
-                Math.E,
-                Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber()
-            )
-        );
-    }
-    // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
-}
-
-function iteratedLog(value: DecimalSource, lhs: DecimalSource = 10, times: DecimalSource = 2) {
-    const timesNumber = Decimal.minabs(times, 1e308).toNumber();
-    return Decimal.iteratedlog(value, lhs, timesNumber);
-}
-
-function slog(value: DecimalSource, lhs: DecimalSource = 10) {
-    const baseNumber = Decimal.minabs(lhs, 1e308).toNumber();
-    return Decimal.slog(value, baseNumber);
-}
-
-function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber())
-        );
-    }
-    // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
-}
-
-function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
-    const diffNumber = Decimal.minabs(diff, 1e308).toNumber();
-    return Decimal.layeradd(value, diffNumber, base);
-}
-
-function invertLayeradd(
-    value: DecimalSource,
-    lhs: FormulaSource,
-    diff: FormulaSource,
-    base: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.layeradd(
-                value,
-                Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber()
-            )
-        );
-    }
-    // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.pow(Math.E, value).times(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.tetrate(value, 2));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
-    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
-    return Decimal.pentate(value, heightNumber, payload);
-}
-
-function invertSin(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.asin(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateSin(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cos(x).neg();
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertCos(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.acos(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateCos(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sin(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertTan(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.atan(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateTan(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cos(x).ln().neg();
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAsin(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.sin(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAsin(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.asin(x)
-            .times(x)
-            .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAcos(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.cos(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAcos(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.acos(x)
-            .times(x)
-            .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAtan(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.tan(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAtan(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.atan(x)
-            .times(x)
-            .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertSinh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.asinh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateSinh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cosh(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertCosh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.acosh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateCosh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sinh(x);
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertTanh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.atanh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateTanh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cosh(x).ln();
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.sinh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAsinh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.cosh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAcosh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.acosh(x)
-            .times(x)
-            .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
-function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.tanh(value));
-    }
-    throw "Could not invert due to no input being a variable";
-}
-
-function integrateAtanh(
-    variable: DecimalSource | undefined,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.atanh(x)
-            .times(x)
-            .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
-    }
-    throw "Could not integrate due to no input being a variable";
-}
-
 /**
  * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
  * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@@ -970,59 +49,59 @@ function integrateAtanh(
 export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     readonly inputs: T;
 
-    private readonly internalEvaluate:
-        | ((...inputs: GuardedFormulasToDecimals<T>) => DecimalSource)
-        | undefined;
-    private readonly internalInvert:
-        | ((value: DecimalSource, ...inputs: T) => DecimalSource)
-        | undefined;
-    private readonly internalIntegrate:
-        | ((
-              variable: DecimalSource | undefined,
-              stack: SubstitutionStack | undefined,
-              ...inputs: T
-          ) => DecimalSource)
-        | undefined;
-    private readonly internalIntegrateInner:
-        | ((
-              variable: DecimalSource | undefined,
-              stack: SubstitutionStack | undefined,
-              ...inputs: T
-          ) => DecimalSource)
-        | undefined;
-    private readonly applySubstitution:
-        | ((variable: DecimalSource, ...inputs: T) => DecimalSource)
-        | undefined;
-    private readonly internalInvertIntegral:
-        | ((value: DecimalSource, ...inputs: T) => DecimalSource)
-        | undefined;
+    private readonly internalEvaluate: EvaluateFunction<T> | undefined;
+    private readonly internalInvert: InvertFunction<T> | undefined;
+    private readonly internalIntegrate: IntegrateFunction<T> | undefined;
+    private readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
+    private readonly applySubstitution: SubstitutionFunction<T> | undefined;
+    private readonly internalInvertIntegral: InvertIntegralFunction<T> | undefined;
     private readonly internalHasVariable: boolean;
 
     public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
 
     constructor(options: FormulaOptions<T>) {
-        // Variable case
+        let readonlyProperties;
         if ("variable" in options) {
-            this.inputs = [options.variable] as T;
-            this.internalHasVariable = true;
-            this.innermostVariable = options.variable;
-            this.internalIntegrate =
-                integrateVariable as unknown as Formula<T>["internalIntegrate"];
-            this.internalIntegrateInner =
-                integrateVariableInner as unknown as Formula<T>["internalIntegrateInner"];
-            this.applySubstitution = passthrough as unknown as Formula<T>["applySubstitution"];
-            return;
-        }
-        // Constant case
-        if (!("evaluate" in options)) {
-            if (options.inputs.length !== 1) {
-                throw "Evaluate function is required if inputs is not length 1";
-            }
-            this.inputs = options.inputs as T;
-            this.internalHasVariable = false;
-            return;
+            readonlyProperties = this.setupVariable(options);
+        } else if (!("evaluate" in options)) {
+            readonlyProperties = this.setupConstant(options);
+        } else {
+            readonlyProperties = this.setupFormula(options);
         }
+        this.inputs = readonlyProperties.inputs;
+        this.internalHasVariable = readonlyProperties.internalHasVariable;
+        this.innermostVariable = readonlyProperties.innermostVariable;
+        this.internalIntegrate = readonlyProperties.internalIntegrate;
+        this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
+        this.applySubstitution = readonlyProperties.applySubstitution;
+    }
 
+    private setupVariable({
+        variable
+    }: {
+        variable: ProcessedComputable<DecimalSource>;
+    }): InternalFormulaProperties<T> {
+        return {
+            inputs: [variable] as T,
+            internalHasVariable: true,
+            innermostVariable: variable,
+            internalIntegrate: integrateVariable as unknown as IntegrateFunction<T>,
+            internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction<T>,
+            applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
+        };
+    }
+
+    private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties<T> {
+        if (inputs.length !== 1) {
+            throw "Evaluate function is required if inputs is not length 1";
+        }
+        return {
+            inputs: inputs as T,
+            internalHasVariable: false
+        };
+    }
+
+    private setupFormula(options: GeneralFormulaOptions<T>): InternalFormulaProperties<T> {
         const {
             inputs,
             evaluate,
@@ -1037,12 +116,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             throw "A formula cannot be marked as having a variable if it is not invertible";
         }
 
-        this.inputs = inputs;
-        this.internalEvaluate = evaluate;
-        this.internalIntegrate = integrate;
-        this.internalIntegrateInner = integrateInner;
-        this.applySubstitution = applySubstitution;
-
         const numVariables = inputs.filter(
             input => input instanceof Formula && input.hasVariable()
         ).length;
@@ -1050,21 +123,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             | GenericFormula
             | undefined;
 
-        this.internalHasVariable =
+        const internalHasVariable =
             numVariables === 1 || (numVariables === 0 && hasVariable === true);
-        if (this.internalHasVariable) {
-            this.innermostVariable = variable?.innermostVariable;
-        }
-        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-        // @ts-ignore
-        this.internalInvert =
-            this.internalHasVariable && variable?.isInvertible() ? invert : undefined;
-        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-        // @ts-ignore
-        this.internalInvertIntegral =
-            this.internalHasVariable && variable?.isIntegralInvertible()
-                ? invertIntegral
-                : undefined;
+        const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined;
+        const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined;
+        const internalInvertIntegral =
+            internalHasVariable && variable?.isIntegralInvertible() ? invertIntegral : undefined;
+
+        return {
+            inputs,
+            internalEvaluate: evaluate,
+            internalIntegrate: integrate,
+            internalIntegrateInner: integrateInner,
+            applySubstitution,
+            innermostVariable,
+            internalHasVariable,
+            internalInvert,
+            internalInvertIntegral
+        };
     }
 
     /** Type predicate that this formula can be inverted. */
@@ -1340,9 +416,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.neg,
-            invert: invertNeg,
-            applySubstitution: applySubstitutionNeg,
-            integrate: integrateNeg
+            invert: ops.invertNeg,
+            applySubstitution: ops.applySubstitutionNeg,
+            integrate: ops.integrateNeg
         });
     }
     public static negate<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
@@ -1386,11 +462,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.add,
-            invert: invertAdd,
-            integrate: integrateAdd,
-            integrateInner: integrateInnerAdd,
-            applySubstitution: passthrough,
-            invertIntegral: invertIntegrateAdd
+            invert: ops.invertAdd,
+            integrate: ops.integrateAdd,
+            integrateInner: ops.integrateInnerAdd,
+            applySubstitution: ops.passthrough,
+            invertIntegral: ops.invertIntegrateAdd
         });
     }
     public static plus<T extends GenericFormula>(value: T, other: FormulaSource): T;
@@ -1407,11 +483,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.sub,
-            invert: invertSub,
-            integrate: integrateSub,
-            integrateInner: integrateInnerSub,
-            applySubstitution: passthrough,
-            invertIntegral: invertIntegrateSub
+            invert: ops.invertSub,
+            integrate: ops.integrateSub,
+            integrateInner: ops.integrateInnerSub,
+            applySubstitution: ops.passthrough,
+            invertIntegral: ops.invertIntegrateSub
         });
     }
     public static subtract<T extends GenericFormula>(value: T, other: FormulaSource): T;
@@ -1434,10 +510,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.mul,
-            invert: invertMul,
-            integrate: integrateMul,
-            applySubstitution: applySubstitutionMul,
-            invertIntegral: invertIntegrateMul
+            invert: ops.invertMul,
+            integrate: ops.integrateMul,
+            applySubstitution: ops.applySubstitutionMul,
+            invertIntegral: ops.invertIntegrateMul
         });
     }
     public static multiply<T extends GenericFormula>(value: T, other: FormulaSource): T;
@@ -1460,10 +536,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.div,
-            invert: invertDiv,
-            integrate: integrateDiv,
-            applySubstitution: applySubstitutionDiv,
-            invertIntegral: invertIntegrateDiv
+            invert: ops.invertDiv,
+            integrate: ops.integrateDiv,
+            applySubstitution: ops.applySubstitutionDiv,
+            invertIntegral: ops.invertIntegrateDiv
         });
     }
     public static divide<T extends GenericFormula>(value: T, other: FormulaSource): T;
@@ -1479,9 +555,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.recip,
-            invert: invertRecip,
-            integrate: integrateRecip,
-            invertIntegral: invertIntegrateRecip
+            invert: ops.invertRecip,
+            integrate: ops.integrateRecip,
+            invertIntegral: ops.invertIntegrateRecip
         });
     }
     public static reciprocal<T extends GenericFormula>(value: T): T;
@@ -1499,61 +575,22 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.max,
-            invert: passthrough as (
+            invert: ops.passthrough as (
                 value: DecimalSource,
                 ...inputs: [FormulaSource, FormulaSource]
             ) => DecimalSource,
-            invertIntegral: passthrough as (
+            invertIntegral: ops.passthrough as (
                 value: DecimalSource,
                 ...inputs: [FormulaSource, FormulaSource]
             ) => DecimalSource
         });
     }
 
-    public static min(value: FormulaSource, other: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, other],
-            evaluate: Decimal.min,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
-
-    public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, other],
-            evaluate: Decimal.minabs,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
-
-    public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, other],
-            evaluate: Decimal.maxabs,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
+    public static min = ops.createPassthroughBinaryFormula(Decimal.min);
+    public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs);
+    public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs);
+    public static clampMin = ops.createPassthroughBinaryFormula(Decimal.clampMin);
+    public static clampMax = ops.createPassthroughBinaryFormula(Decimal.clampMax);
 
     public static clamp(
         value: FormulaSource,
@@ -1563,44 +600,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, min, max],
             evaluate: Decimal.clamp,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
-
-    public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, min],
-            evaluate: Decimal.clampMin,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
-
-    public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, max],
-            evaluate: Decimal.clampMax,
-            invert: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
+            invert: ops.passthrough as InvertFunction<
+                [FormulaSource, FormulaSource, FormulaSource]
+            >,
+            invertIntegral: ops.passthrough as InvertFunction<
+                [FormulaSource, FormulaSource, FormulaSource]
+            >
         });
     }
 
@@ -1618,9 +623,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.log10,
-            invert: invertLog10,
-            integrate: integrateLog10,
-            invertIntegral: invertIntegrateLog10
+            invert: ops.invertLog10,
+            integrate: ops.integrateLog10,
+            invertIntegral: ops.invertIntegrateLog10
         });
     }
 
@@ -1631,9 +636,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, base],
             evaluate: Decimal.log,
-            invert: invertLog,
-            integrate: integrateLog,
-            invertIntegral: invertIntegrateLog
+            invert: ops.invertLog,
+            integrate: ops.integrateLog,
+            invertIntegral: ops.invertIntegrateLog
         });
     }
     public static logarithm<T extends GenericFormula>(value: T, base: FormulaSource): T;
@@ -1649,9 +654,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.log2,
-            invert: invertLog2,
-            integrate: integrateLog2,
-            invertIntegral: invertIntegrateLog2
+            invert: ops.invertLog2,
+            integrate: ops.integrateLog2,
+            invertIntegral: ops.invertIntegrateLog2
         });
     }
 
@@ -1661,9 +666,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.ln,
-            invert: invertLn,
-            integrate: integrateLn,
-            invertIntegral: invertIntegrateLn
+            invert: ops.invertLn,
+            integrate: ops.integrateLn,
+            invertIntegral: ops.invertIntegrateLn
         });
     }
 
@@ -1674,9 +679,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.pow,
-            invert: invertPow,
-            integrate: integratePow,
-            invertIntegral: invertIntegratePow
+            invert: ops.invertPow,
+            integrate: ops.integratePow,
+            invertIntegral: ops.invertIntegratePow
         });
     }
 
@@ -1686,9 +691,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.pow10,
-            invert: invertPow10,
-            integrate: integratePow10,
-            invertIntegral: invertIntegratePow10
+            invert: ops.invertPow10,
+            integrate: ops.integratePow10,
+            invertIntegral: ops.invertIntegratePow10
         });
     }
 
@@ -1699,9 +704,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.pow_base,
-            invert: invertPowBase,
-            integrate: integratePowBase,
-            invertIntegral: invertIntegratePowBase
+            invert: ops.invertPowBase,
+            integrate: ops.integratePowBase,
+            invertIntegral: ops.invertIntegratePowBase
         });
     }
 
@@ -1712,9 +717,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, other],
             evaluate: Decimal.root,
-            invert: invertRoot,
-            integrate: integrateRoot,
-            invertIntegral: invertIntegrateRoot
+            invert: ops.invertRoot,
+            integrate: ops.integrateRoot,
+            invertIntegral: ops.invertIntegrateRoot
         });
     }
 
@@ -1736,8 +741,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.exp,
-            invert: invertExp,
-            integrate: integrateExp
+            invert: ops.invertExp,
+            integrate: ops.integrateExp
         });
     }
 
@@ -1782,8 +787,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     ) {
         return new Formula({
             inputs: [value, height, payload],
-            evaluate: tetrate,
-            invert: invertTetrate
+            evaluate: ops.tetrate,
+            invert: ops.invertTetrate
         });
     }
 
@@ -1804,8 +809,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     ) {
         return new Formula({
             inputs: [value, height, payload],
-            evaluate: iteratedexp,
-            invert: invertIteratedExp
+            evaluate: ops.iteratedexp,
+            invert: ops.invertIteratedExp
         });
     }
 
@@ -1814,7 +819,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         base: FormulaSource = 10,
         times: FormulaSource = 1
     ): GenericFormula {
-        return new Formula({ inputs: [value, base, times], evaluate: iteratedLog });
+        return new Formula({ inputs: [value, base, times], evaluate: ops.iteratedLog });
     }
 
     public static slog<T extends GenericFormula>(
@@ -1823,7 +828,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     ): Omit<T, "integrate" | "invertIntegral">;
     public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula;
     public static slog(value: FormulaSource, base: FormulaSource = 10) {
-        return new Formula({ inputs: [value, base], evaluate: slog, invert: invertSlog });
+        return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog });
     }
 
     public static layeradd10(value: FormulaSource, diff: FormulaSource) {
@@ -1843,8 +848,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) {
         return new Formula({
             inputs: [value, diff, base],
-            evaluate: layeradd,
-            invert: invertLayeradd
+            evaluate: ops.layeradd,
+            invert: ops.invertLayeradd
         });
     }
 
@@ -1853,7 +858,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     ): Omit<T, "integrate" | "invertIntegral">;
     public static lambertw(value: FormulaSource): GenericFormula;
     public static lambertw(value: FormulaSource) {
-        return new Formula({ inputs: [value], evaluate: Decimal.lambertw, invert: invertLambertw });
+        return new Formula({
+            inputs: [value],
+            evaluate: Decimal.lambertw,
+            invert: ops.invertLambertw
+        });
     }
 
     public static ssqrt<T extends GenericFormula>(
@@ -1861,7 +870,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     ): Omit<T, "integrate" | "invertIntegral">;
     public static ssqrt(value: FormulaSource): GenericFormula;
     public static ssqrt(value: FormulaSource) {
-        return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: invertSsqrt });
+        return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt });
     }
 
     public static pentate(
@@ -1869,7 +878,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         height: FormulaSource = 2,
         payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1)
     ): GenericFormula {
-        return new Formula({ inputs: [value, height, payload], evaluate: pentate });
+        return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate });
     }
 
     public static sin<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
@@ -1878,8 +887,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.sin,
-            invert: invertAsin,
-            integrate: integrateSin
+            invert: ops.invertAsin,
+            integrate: ops.integrateSin
         });
     }
 
@@ -1889,8 +898,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.cos,
-            invert: invertAcos,
-            integrate: integrateCos
+            invert: ops.invertAcos,
+            integrate: ops.integrateCos
         });
     }
 
@@ -1900,8 +909,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.tan,
-            invert: invertAtan,
-            integrate: integrateTan
+            invert: ops.invertAtan,
+            integrate: ops.integrateTan
         });
     }
 
@@ -1911,8 +920,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.asin,
-            invert: invertSin,
-            integrate: integrateAsin
+            invert: ops.invertSin,
+            integrate: ops.integrateAsin
         });
     }
 
@@ -1922,8 +931,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.acos,
-            invert: invertCos,
-            integrate: integrateAcos
+            invert: ops.invertCos,
+            integrate: ops.integrateAcos
         });
     }
 
@@ -1933,8 +942,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.atan,
-            invert: invertTan,
-            integrate: integrateAtan
+            invert: ops.invertTan,
+            integrate: ops.integrateAtan
         });
     }
 
@@ -1944,8 +953,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.sinh,
-            invert: invertAsinh,
-            integrate: integrateSinh
+            invert: ops.invertAsinh,
+            integrate: ops.integrateSinh
         });
     }
 
@@ -1955,8 +964,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.cosh,
-            invert: invertAcosh,
-            integrate: integrateCosh
+            invert: ops.invertAcosh,
+            integrate: ops.integrateCosh
         });
     }
 
@@ -1966,8 +975,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.tanh,
-            invert: invertAtanh,
-            integrate: integrateTanh
+            invert: ops.invertAtanh,
+            integrate: ops.integrateTanh
         });
     }
 
@@ -1977,8 +986,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.asinh,
-            invert: invertSinh,
-            integrate: integrateAsinh
+            invert: ops.invertSinh,
+            integrate: ops.integrateAsinh
         });
     }
 
@@ -1988,8 +997,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.acosh,
-            invert: invertCosh,
-            integrate: integrateAcosh
+            invert: ops.invertCosh,
+            integrate: ops.integrateAcosh
         });
     }
 
@@ -1999,8 +1008,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value],
             evaluate: Decimal.atanh,
-            invert: invertTanh,
-            integrate: integrateAtanh
+            invert: ops.invertTanh,
+            integrate: ops.integrateAtanh
         });
     }
 
@@ -2488,7 +1497,7 @@ export function calculateMaxAffordable(
             if (!formula.isInvertible()) {
                 throw "Cannot calculate max affordable of non-invertible formula";
             }
-            return Decimal.floor((formula as InvertibleFormula).invert(resource.value));
+            return Decimal.floor(formula.invert(resource.value));
         }
     });
 }
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
new file mode 100644
index 0000000..fb860d3
--- /dev/null
+++ b/src/game/formulas/operations.ts
@@ -0,0 +1,904 @@
+import Decimal, { DecimalSource } from "util/bignum";
+import { unref } from "vue";
+import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
+import { FormulaSource, InvertFunction, SubstitutionStack } from "./types";
+
+export function passthrough(value: DecimalSource) {
+    return value;
+}
+
+export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.neg(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateNeg(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        return Decimal.neg(lhs.evaluateIntegral(variable, stack));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function applySubstitutionNeg(value: DecimalSource) {
+    return Decimal.neg(value);
+}
+
+export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAdd(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(rhs),
+            variable ?? unref(lhs.innermostVariable) ?? 0
+        ).add(x);
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(lhs),
+            variable ?? unref(rhs.innermostVariable) ?? 0
+        ).add(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function integrateInnerAdd(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.add(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.add(x, unrefFormulaSource(lhs));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
+    } else if (hasVariable(rhs)) {
+        const b = unrefFormulaSource(lhs);
+        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateSub(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(
+            x,
+            Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
+        );
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(
+            unrefFormulaSource(lhs),
+            variable ?? unref(rhs.innermostVariable) ?? 0
+        ).sub(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function integrateInnerSub(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.sub(x, unrefFormulaSource(lhs));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
+    } else if (hasVariable(rhs)) {
+        const b = unrefFormulaSource(lhs);
+        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateMul(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.times(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.times(x, unrefFormulaSource(lhs));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return Decimal.div(value, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        return Decimal.div(value, unrefFormulaSource(lhs));
+    }
+    throw "Could not apply substitution due to no input being a variable";
+}
+
+export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
+    } else if (hasVariable(rhs)) {
+        const b = unrefFormulaSource(lhs);
+        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateDiv(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.div(x, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        return Decimal.div(unrefFormulaSource(lhs), x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return Decimal.mul(value, unrefFormulaSource(rhs));
+    } else if (hasVariable(rhs)) {
+        return Decimal.mul(value, unrefFormulaSource(lhs));
+    }
+    throw "Could not apply substitution due to no input being a variable";
+}
+
+export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
+    } else if (hasVariable(rhs)) {
+        const b = unrefFormulaSource(lhs);
+        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.recip(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateRecip(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.exp(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.pow10(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateLog10(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(
+            Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1))
+        );
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateLog(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x)
+            .sub(1)
+            .times(x)
+            .div(Decimal.ln(unrefFormulaSource(rhs)));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
+        return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.pow(2, value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateLog2(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertLn(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.exp(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateLn(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs))));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integratePow(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        const pow = Decimal.add(unrefFormulaSource(rhs), 1);
+        return Decimal.pow(x, pow).div(pow);
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        const b = unrefFormulaSource(lhs);
+        return Decimal.pow(b, x).div(Decimal.ln(b));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
+    } else if (hasVariable(rhs)) {
+        const denominator = Decimal.ln(unrefFormulaSource(lhs));
+        return rhs.invert(Decimal.times(denominator, value).ln().div(denominator));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.root(value, 10));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integratePow10(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(
+            Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp()
+        );
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integratePowBase(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        const b = unrefFormulaSource(rhs);
+        return Decimal.pow(b, x).div(Decimal.ln(b));
+    } else if (hasVariable(rhs)) {
+        const x = rhs.evaluateIntegral(variable, stack);
+        const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
+        return Decimal.pow(x, denominator).div(denominator);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegratePowBase(
+    value: DecimalSource,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b)));
+    } else if (hasVariable(rhs)) {
+        const b = unrefFormulaSource(lhs);
+        return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs))));
+    } else if (hasVariable(rhs)) {
+        return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value)));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateRoot(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        const a = unrefFormulaSource(rhs);
+        return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        const b = unrefFormulaSource(rhs);
+        return lhs.invert(
+            Decimal.add(b, 1)
+                .times(value)
+                .div(b)
+                .pow(Decimal.div(b, Decimal.add(b, 1)))
+        );
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertExp(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.ln(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateExp(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.exp(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function tetrate(
+    value: DecimalSource,
+    height: DecimalSource = 2,
+    payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
+) {
+    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
+    return Decimal.tetrate(value, heightNumber, payload);
+}
+
+export function invertTetrate(
+    value: DecimalSource,
+    base: FormulaSource,
+    height: FormulaSource,
+    payload: FormulaSource
+) {
+    if (hasVariable(base)) {
+        return base.invert(Decimal.ssqrt(value));
+    }
+    // Other params can't be inverted ATM
+    throw "Could not invert due to no input being a variable";
+}
+
+export function iteratedexp(
+    value: DecimalSource,
+    height: DecimalSource = 2,
+    payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1)
+) {
+    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
+    return Decimal.iteratedexp(value, heightNumber, new Decimal(payload));
+}
+
+export function invertIteratedExp(
+    value: DecimalSource,
+    lhs: FormulaSource,
+    height: FormulaSource,
+    payload: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(
+            Decimal.iteratedlog(
+                value,
+                Math.E,
+                Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber()
+            )
+        );
+    }
+    // Other params can't be inverted ATM
+    throw "Could not invert due to no input being a variable";
+}
+
+export function iteratedLog(
+    value: DecimalSource,
+    lhs: DecimalSource = 10,
+    times: DecimalSource = 2
+) {
+    const timesNumber = Decimal.minabs(times, 1e308).toNumber();
+    return Decimal.iteratedlog(value, lhs, timesNumber);
+}
+
+export function slog(value: DecimalSource, lhs: DecimalSource = 10) {
+    const baseNumber = Decimal.minabs(lhs, 1e308).toNumber();
+    return Decimal.slog(value, baseNumber);
+}
+
+export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(
+            Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber())
+        );
+    }
+    // Other params can't be inverted ATM
+    throw "Could not invert due to no input being a variable";
+}
+
+export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
+    const diffNumber = Decimal.minabs(diff, 1e308).toNumber();
+    return Decimal.layeradd(value, diffNumber, base);
+}
+
+export function invertLayeradd(
+    value: DecimalSource,
+    lhs: FormulaSource,
+    diff: FormulaSource,
+    base: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(
+            Decimal.layeradd(
+                value,
+                Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber()
+            )
+        );
+    }
+    // Other params can't be inverted ATM
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.pow(Math.E, value).times(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.tetrate(value, 2));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
+    const heightNumber = Decimal.minabs(height, 1e308).toNumber();
+    return Decimal.pentate(value, heightNumber, payload);
+}
+
+export function invertSin(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.asin(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateSin(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cos(x).neg();
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertCos(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.acos(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateCos(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sin(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertTan(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.atan(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateTan(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cos(x).ln().neg();
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.sin(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAsin(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.asin(x)
+            .times(x)
+            .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.cos(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAcos(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.acos(x)
+            .times(x)
+            .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.tan(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAtan(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.atan(x)
+            .times(x)
+            .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.asinh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateSinh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cosh(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.acosh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateCosh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.sinh(x);
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.atanh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateTanh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.cosh(x).ln();
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.sinh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAsinh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.cosh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAcosh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.acosh(x)
+            .times(x)
+            .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return lhs.invert(Decimal.tanh(value));
+    }
+    throw "Could not invert due to no input being a variable";
+}
+
+export function integrateAtanh(
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack,
+    lhs: FormulaSource
+) {
+    if (hasVariable(lhs)) {
+        const x = lhs.evaluateIntegral(variable, stack);
+        return Decimal.atanh(x)
+            .times(x)
+            .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
+    }
+    throw "Could not integrate due to no input being a variable";
+}
+
+export function createPassthroughBinaryFormula(
+    operation: (a: DecimalSource, b: DecimalSource) => DecimalSource
+) {
+    return (value: FormulaSource, other: FormulaSource) =>
+        new Formula({
+            inputs: [value, other],
+            evaluate: operation,
+            invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>,
+            invertIntegral: passthrough as InvertFunction<[FormulaSource, FormulaSource]>
+        });
+}
diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts
new file mode 100644
index 0000000..baaf2bb
--- /dev/null
+++ b/src/game/formulas/types.d.ts
@@ -0,0 +1,79 @@
+import Formula from "game/formulas/formulas";
+import { DecimalSource } from "util/bignum";
+import { ProcessedComputable } from "util/computed";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type GenericFormula = Formula<any>;
+type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
+type InvertibleFormula = GenericFormula & {
+    invert: (value: DecimalSource) => DecimalSource;
+};
+type IntegrableFormula = GenericFormula & {
+    evaluateIntegral: (variable?: DecimalSource) => DecimalSource;
+};
+type InvertibleIntegralFormula = GenericFormula & {
+    invertIntegral: (value: DecimalSource) => DecimalSource;
+};
+
+type EvaluateFunction<T> = (
+    this: Formula<T>,
+    ...inputs: GuardedFormulasToDecimals<T>
+) => DecimalSource;
+type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
+type IntegrateFunction<T> = (
+    this: Formula<T>,
+    variable: DecimalSource | undefined,
+    stack: SubstitutionStack | undefined,
+    ...inputs: T
+) => DecimalSource;
+type SubstitutionFunction<T> = (
+    this: Formula<T>,
+    variable: DecimalSource,
+    ...inputs: T
+) => DecimalSource;
+type InvertIntegralFunction<T> = (
+    this: Formula<T>,
+    value: DecimalSource,
+    ...inputs: T
+) => DecimalSource;
+
+type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
+type ConstantFormulaOptions = {
+    inputs: [FormulaSource];
+};
+type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
+    inputs: T;
+    evaluate: EvaluateFunction<T>;
+    invert?: InvertFunction<T>;
+    integrate?: IntegrateFunction<T>;
+    integrateInner?: IntegrateFunction<T>;
+    applySubstitution?: SubstitutionFunction<T>;
+    invertIntegral?: InvertIntegralFunction<T>;
+    hasVariable?: boolean;
+};
+type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
+    | VariableFormulaOptions
+    | ConstantFormulaOptions
+    | GeneralFormulaOptions<T>;
+
+type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
+    inputs: T;
+    internalHasVariable: boolean;
+    internalEvaluate?: EvaluateFunction<T>;
+    internalInvert?: InvertFunction<T>;
+    internalIntegrate?: IntegrateFunction<T>;
+    internalIntegrateInner?: IntegrateFunction<T>;
+    internalInvertIntegral?: InvertIntegralFunction<T>;
+    applySubstitution?: SubstitutionFunction<T>;
+    innermostVariable?: ProcessedComputable<DecimalSource>;
+};
+
+type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
+
+// It's really hard to type mapped tuples, but these classes seem to manage
+type FormulasToDecimals<T extends FormulaSource[]> = {
+    [K in keyof T]: DecimalSource;
+};
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type TupleGuard<T extends any[]> = T extends any[] ? FormulasToDecimals<T> : never;
+type GuardedFormulasToDecimals<T extends FormulaSource[]> = TupleGuard<T>;
diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 5372e36..8cd1cf7 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -11,12 +11,8 @@ import {
 import { createLazyProxy } from "util/proxies";
 import { joinJSX, renderJSX } from "util/vue";
 import { computed, unref } from "vue";
-import Formula, {
-    calculateCost,
-    calculateMaxAffordable,
-    GenericFormula,
-    InvertibleFormula
-} from "./formulas";
+import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas";
+import type { GenericFormula, InvertibleFormula } from "./formulas/types";
 import { DefaultValue, Persistent } from "./persistence";
 
 /**
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 9937d30..a2b4012 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -2,10 +2,9 @@ import { createResource, Resource } from "features/resources/resource";
 import Formula, {
     calculateCost,
     calculateMaxAffordable,
-    GenericFormula,
-    InvertibleFormula,
     unrefFormulaSource
-} from "game/formulas";
+} from "game/formulas/formulas";
+import type { GenericFormula, InvertibleFormula } from "game/formulas/types";
 import Decimal, { DecimalSource } from "util/bignum";
 import { beforeAll, describe, expect, test } from "vitest";
 import { ref } from "vue";
diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts
index 55474b9..13a13e9 100644
--- a/tests/game/requirements.test.ts
+++ b/tests/game/requirements.test.ts
@@ -1,6 +1,6 @@
 import { Visibility } from "features/feature";
 import { createResource, Resource } from "features/resources/resource";
-import Formula from "game/formulas";
+import Formula from "game/formulas/formulas";
 import {
     CostRequirement,
     createBooleanRequirement,

From f589184c9e95ef9a0e844d912c7758756bb83c5d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 19:22:27 -0500
Subject: [PATCH 08/36] Simplify alias definitions

---
 src/game/formulas/formulas.ts | 170 ++++++----------------------------
 1 file changed, 28 insertions(+), 142 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 885adee..6255981 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -421,23 +421,13 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             integrate: ops.integrateNeg
         });
     }
-    public static negate<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
-    public static negate(value: FormulaSource): GenericFormula;
-    public static negate(value: FormulaSource) {
-        return Formula.neg(value);
-    }
-    public static negated<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
-    public static negated(value: FormulaSource): GenericFormula;
-    public static negated(value: FormulaSource) {
-        return Formula.neg(value);
-    }
+    public static negate = Formula.neg;
+    public static negated = Formula.neg;
 
     public static sign(value: FormulaSource): GenericFormula {
         return new Formula({ inputs: [value], evaluate: Decimal.sign });
     }
-    public static sgn(value: FormulaSource) {
-        return Formula.sign(value);
-    }
+    public static sgn = Formula.sign;
 
     public static round(value: FormulaSource): GenericFormula {
         return new Formula({ inputs: [value], evaluate: Decimal.round });
@@ -469,12 +459,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateAdd
         });
     }
-    public static plus<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static plus<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static plus(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static plus(value: FormulaSource, other: FormulaSource) {
-        return Formula.add(value, other);
-    }
+    public static plus = Formula.add;
 
     public static sub<T extends GenericFormula>(value: T, other: FormulaSource): T;
     public static sub<T extends GenericFormula>(value: FormulaSource, other: T): T;
@@ -490,18 +475,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateSub
         });
     }
-    public static subtract<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static subtract<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static subtract(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static subtract(value: FormulaSource, other: FormulaSource) {
-        return Formula.sub(value, other);
-    }
-    public static minus<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static minus<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static minus(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static minus(value: FormulaSource, other: FormulaSource) {
-        return Formula.sub(value, other);
-    }
+    public static subtract = Formula.sub;
+    public static minus = Formula.sub;
 
     public static mul<T extends GenericFormula>(value: T, other: FormulaSource): T;
     public static mul<T extends GenericFormula>(value: FormulaSource, other: T): T;
@@ -516,18 +491,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateMul
         });
     }
-    public static multiply<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static multiply<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static multiply(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static multiply(value: FormulaSource, other: FormulaSource) {
-        return Formula.mul(value, other);
-    }
-    public static times<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static times<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static times(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static times(value: FormulaSource, other: FormulaSource) {
-        return Formula.mul(value, other);
-    }
+    public static multiply = Formula.mul;
+    public static times = Formula.mul;
 
     public static div<T extends GenericFormula>(value: T, other: FormulaSource): T;
     public static div<T extends GenericFormula>(value: FormulaSource, other: T): T;
@@ -542,12 +507,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateDiv
         });
     }
-    public static divide<T extends GenericFormula>(value: T, other: FormulaSource): T;
-    public static divide<T extends GenericFormula>(value: FormulaSource, other: T): T;
-    public static divide(value: FormulaSource, other: FormulaSource): GenericFormula;
-    public static divide(value: FormulaSource, other: FormulaSource) {
-        return Formula.div(value, other);
-    }
+    public static divide = Formula.div;
+    public static divideBy = Formula.div;
+    public static dividedBy = Formula.div;
 
     public static recip<T extends GenericFormula>(value: T): T;
     public static recip(value: FormulaSource): GenericFormula;
@@ -560,16 +522,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateRecip
         });
     }
-    public static reciprocal<T extends GenericFormula>(value: T): T;
-    public static reciprocal(value: FormulaSource): GenericFormula;
-    public static reciprocal(value: FormulaSource): GenericFormula {
-        return Formula.recip(value);
-    }
-    public static reciprocate<T extends GenericFormula>(value: T): T;
-    public static reciprocate(value: FormulaSource): GenericFormula;
-    public static reciprocate(value: FormulaSource) {
-        return Formula.recip(value);
-    }
+    public static reciprocal = Formula.recip;
+    public static reciprocate = Formula.recip;
 
     public static max(value: FormulaSource, other: FormulaSource): GenericFormula {
         return new Formula({
@@ -641,12 +595,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invertIntegral: ops.invertIntegrateLog
         });
     }
-    public static logarithm<T extends GenericFormula>(value: T, base: FormulaSource): T;
-    public static logarithm<T extends GenericFormula>(value: FormulaSource, base: T): T;
-    public static logarithm(value: FormulaSource, base: FormulaSource): GenericFormula;
-    public static logarithm(value: FormulaSource, base: FormulaSource) {
-        return Formula.log(value, base);
-    }
+    public static logarithm = Formula.log;
 
     public static log2<T extends GenericFormula>(value: T): T;
     public static log2(value: FormulaSource): GenericFormula;
@@ -1048,23 +997,13 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public neg(this: GenericFormula) {
         return Formula.neg(this);
     }
-    public negate<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
-    public negate(this: GenericFormula): GenericFormula;
-    public negate(this: GenericFormula) {
-        return Formula.neg(this);
-    }
-    public negated<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
-    public negated(this: GenericFormula): GenericFormula;
-    public negated(this: GenericFormula) {
-        return Formula.neg(this);
-    }
+    public negate = this.neg;
+    public negated = this.neg;
 
     public sign() {
         return Formula.sign(this);
     }
-    public sgn() {
-        return this.sign();
-    }
+    public sgn = this.sign;
 
     public round() {
         return Formula.round(this);
@@ -1088,12 +1027,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public add(this: GenericFormula, value: FormulaSource) {
         return Formula.add(this, value);
     }
-    public plus<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public plus<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public plus(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public plus(value: FormulaSource) {
-        return Formula.add(this, value);
-    }
+    public plus = this.add;
 
     public sub<T extends GenericFormula>(this: T, value: FormulaSource): T;
     public sub<T extends GenericFormula>(this: GenericFormula, value: T): T;
@@ -1101,18 +1035,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public sub(value: FormulaSource) {
         return Formula.sub(this, value);
     }
-    public subtract<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public subtract<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public subtract(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public subtract(value: FormulaSource) {
-        return Formula.sub(this, value);
-    }
-    public minus<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public minus<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public minus(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public minus(value: FormulaSource) {
-        return Formula.sub(this, value);
-    }
+    public subtract = this.sub;
+    public minus = this.sub;
 
     public mul<T extends GenericFormula>(this: T, value: FormulaSource): T;
     public mul<T extends GenericFormula>(this: GenericFormula, value: T): T;
@@ -1120,18 +1044,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public mul(value: FormulaSource) {
         return Formula.mul(this, value);
     }
-    public multiply<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public multiply<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public multiply(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public multiply(value: FormulaSource) {
-        return Formula.mul(this, value);
-    }
-    public times<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public times<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public times(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public times(value: FormulaSource) {
-        return Formula.mul(this, value);
-    }
+    public multiply = this.mul;
+    public times = this.mul;
 
     public div<T extends GenericFormula>(this: T, value: FormulaSource): T;
     public div<T extends GenericFormula>(this: GenericFormula, value: T): T;
@@ -1139,40 +1053,17 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public div(value: FormulaSource) {
         return Formula.div(this, value);
     }
-    public divide<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public divide<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public divide(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public divide(value: FormulaSource) {
-        return Formula.div(this, value);
-    }
-    public divideBy<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public divideBy<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public divideBy(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public divideBy(value: FormulaSource) {
-        return Formula.div(this, value);
-    }
-    public dividedBy<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public dividedBy<T extends GenericFormula>(this: GenericFormula, value: T): T;
-    public dividedBy(this: GenericFormula, value: FormulaSource): GenericFormula;
-    public dividedBy(value: FormulaSource) {
-        return Formula.div(this, value);
-    }
+    public divide = this.div;
+    public divideBy = this.div;
+    public dividedBy = this.div;
 
     public recip<T extends GenericFormula>(this: T): T;
     public recip(this: FormulaSource): GenericFormula;
     public recip() {
         return Formula.recip(this);
     }
-    public reciprocal<T extends GenericFormula>(this: T): T;
-    public reciprocal(this: FormulaSource): GenericFormula;
-    public reciprocal() {
-        return Formula.recip(this);
-    }
-    public reciprocate<T extends GenericFormula>(this: T): T;
-    public reciprocate(this: FormulaSource): GenericFormula;
-    public reciprocate() {
-        return Formula.recip(this);
-    }
+    public reciprocal = this.recip;
+    public reciprocate = this.recip;
 
     public max(value: FormulaSource) {
         return Formula.max(this, value);
@@ -1222,12 +1113,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public log(value: FormulaSource) {
         return Formula.log(this, value);
     }
-    public logarithm<T extends GenericFormula>(this: T, value: FormulaSource): T;
-    public logarithm<T extends GenericFormula>(this: FormulaSource, value: T): T;
-    public logarithm(this: FormulaSource, value: FormulaSource): GenericFormula;
-    public logarithm(value: FormulaSource) {
-        return Formula.log(this, value);
-    }
+    public logarithm = this.log;
 
     public log2<T extends GenericFormula>(this: T): T;
     public log2(this: FormulaSource): GenericFormula;

From 3b4c098f9e9acf330393f805058cf46f9b935e80 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 19:59:45 -0500
Subject: [PATCH 09/36] Fix some formula tests

---
 tests/game/formulas.test.ts | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index a2b4012..967080d 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -138,11 +138,13 @@ const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunction
 
 describe("Formula Equality Checking", () => {
     describe("Equality Checks", () => {
-        test("Equals", () => Formula.add(1, 1).equals(Formula.add(1, 1)));
-        test("Not Equals due to inputs", () => Formula.add(1, 1).equals(Formula.add(1, 0)));
-        test("Not Equals due to functions", () => Formula.add(1, 1).equals(Formula.sub(1, 1)));
+        test("Equals", () => expect(Formula.add(1, 1).equals(Formula.add(1, 1))).toBe(true));
+        test("Not Equals due to inputs", () =>
+            expect(Formula.add(1, 1).equals(Formula.add(1, 0))).toBe(false));
+        test("Not Equals due to functions", () =>
+            expect(Formula.add(1, 1).equals(Formula.sub(1, 1))).toBe(false));
         test("Not Equals due to hasVariable", () =>
-            Formula.constant(1).equals(Formula.variable(1)));
+            expect(Formula.constant(1).equals(Formula.variable(1))).toBe(false));
     });
 
     describe("Formula aliases", () => {

From f94c0c1f68a9e1a7be92ed7312136b8ba10f922b Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 20:00:06 -0500
Subject: [PATCH 10/36] Add some more tests for step-wise and conditional
 formulas

---
 tests/game/formulas.test.ts | 45 +++++++++++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 967080d..13a6e53 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -759,6 +759,17 @@ describe("Step-wise", () => {
             ).compare_tolerance(10));
     });
 
+    describe("Pass-through at boundary", () => {
+        test("Evaluates correctly", () =>
+            expect(
+                Formula.step(constant, 10, value => Formula.sqrt(value)).evaluate()
+            ).compare_tolerance(10));
+        test("Inverts correctly with variable in input", () =>
+            expect(
+                Formula.step(variable, 10, value => Formula.sqrt(value)).invert(10)
+            ).compare_tolerance(10));
+    });
+
     describe("Evaluates correctly beyond start", () => {
         test("Evaluates correctly", () =>
             expect(
@@ -769,6 +780,23 @@ describe("Step-wise", () => {
                 Formula.step(variable, 8, value => Formula.add(value, 2)).invert(12)
             ).compare_tolerance(10));
     });
+
+    describe("Evaluates correctly when nested", () => {
+        test("Evaluates correctly", () =>
+            expect(
+                Formula.add(variable, constant)
+                    .step(10, value => Formula.mul(value, 2))
+                    .sub(10)
+                    .evaluate()
+            ).compare_tolerance(20));
+        test("Inverts correctly", () =>
+            expect(
+                Formula.add(variable, constant)
+                    .step(10, value => Formula.mul(value, 2))
+                    .sub(10)
+                    .invert(30)
+            ).compare_tolerance(15));
+    });
 });
 
 describe("Conditionals", () => {
@@ -842,6 +870,23 @@ describe("Conditionals", () => {
                 Formula.if(variable, true, value => Formula.add(value, 2)).invert(12)
             ).compare_tolerance(10));
     });
+
+    describe("Evaluates correctly when nested", () => {
+        test("Evaluates correctly", () =>
+            expect(
+                Formula.add(variable, constant)
+                    .if(true, value => Formula.add(value, 2))
+                    .div(2)
+                    .evaluate()
+            ).compare_tolerance(11));
+        test("Inverts correctly", () =>
+            expect(
+                Formula.add(variable, constant)
+                    .if(true, value => Formula.add(value, 2))
+                    .div(2)
+                    .invert(12)
+            ).compare_tolerance(12));
+    });
 });
 
 describe("Custom Formulas", () => {

From f47cc23eea29a1160d1ef037381be0522f9124f7 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 20:16:59 -0500
Subject: [PATCH 11/36] Add some test TODOs

---
 tests/game/formulas.test.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 13a6e53..4003051 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -996,6 +996,10 @@ describe("Custom Formulas", () => {
                 }).invertIntegral(8)
             ).compare_tolerance(1));
     });
+
+    describe.todo("Formula as input");
+
+    describe.todo("Determines invertibility etc. correctly");
 });
 
 describe("Buy Max", () => {

From c3b5f2cdad930993dd3aae2a036e6cd48f0d3c63 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 20:22:03 -0500
Subject: [PATCH 12/36] Throw error objects instead of strings

---
 src/data/common.tsx                   |   4 +-
 src/data/projEntry.tsx                |  10 +-
 src/features/challenges/challenge.tsx |   4 +-
 src/features/tabs/tabFamily.ts        |   2 +-
 src/game/formulas/formulas.ts         |  30 ++---
 src/game/formulas/operations.ts       | 154 +++++++++++++-------------
 src/game/persistence.ts               |   2 +-
 src/lib/break_eternity.ts             |   2 +-
 src/main.ts                           |   4 +-
 9 files changed, 114 insertions(+), 98 deletions(-)

diff --git a/src/data/common.tsx b/src/data/common.tsx
index b1d5398..befb408 100644
--- a/src/data/common.tsx
+++ b/src/data/common.tsx
@@ -465,7 +465,7 @@ export function createFormulaPreview(
     const processedShowPreview = convertComputable(showPreview);
     const processedPreviewAmount = convertComputable(previewAmount);
     if (!formula.hasVariable()) {
-        throw "Cannot create formula preview if the formula does not have a variable";
+        throw new Error("Cannot create formula preview if the formula does not have a variable");
     }
     return computed(() => {
         if (unref(processedShowPreview)) {
@@ -507,7 +507,7 @@ export function modifierToFormula(modifier: Modifier, base: FormulaSource) {
                       if (lhs instanceof Formula && lhs.hasVariable()) {
                           return lhs.invert(modifier.revert!(val));
                       }
-                      throw "Could not invert due to no input being a variable";
+                      throw new Error("Could not invert due to no input being a variable");
                   }
                 : undefined
     });
diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index e4640b6..df86d6a 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -4,6 +4,7 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso
 import type { GenericTree } from "features/trees/tree";
 import { branchedResetPropagation, createTree } from "features/trees/tree";
 import { globalBus } from "game/events";
+import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas";
 import type { BaseLayer, GenericLayer } from "game/layers";
 import { createLayer } from "game/layers";
 import type { Player } from "game/player";
@@ -11,9 +12,16 @@ import player from "game/player";
 import type { DecimalSource } from "util/bignum";
 import Decimal, { format, formatTime } from "util/bignum";
 import { render } from "util/vue";
-import { computed, toRaw } from "vue";
+import { computed, ref, toRaw, unref } from "vue";
 import prestige from "./layers/prestige";
 
+window.Formula = Formula;
+window.calculateMaxAffordable = calculateMaxAffordable;
+window.calculateCost = calculateCost;
+window.unref = unref;
+window.ref = ref;
+window.createResource = createResource;
+
 /**
  * @hidden
  */
diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index 58a804c..92dce42 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -113,7 +113,9 @@ export function createChallenge<T extends ChallengeOptions>(
                 "Cannot create challenge without a canComplete property or a resource and goal property",
                 challenge
             );
-            throw "Cannot create challenge without a canComplete property or a resource and goal property";
+            throw new Error(
+                "Cannot create challenge without a canComplete property or a resource and goal property"
+            );
         }
 
         challenge.id = getUniqueID("challenge-");
diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts
index 5f5c666..9652f3d 100644
--- a/src/features/tabs/tabFamily.ts
+++ b/src/features/tabs/tabFamily.ts
@@ -98,7 +98,7 @@ export function createTabFamily<T extends TabFamilyOptions>(
 ): TabFamily<T> {
     if (Object.keys(tabs).length === 0) {
         console.warn("Cannot create tab family with 0 tabs");
-        throw "Cannot create tab family with 0 tabs";
+        throw new Error("Cannot create tab family with 0 tabs");
     }
 
     const selected = persistent(Object.keys(tabs)[0], false);
diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 6255981..30a2b20 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -35,7 +35,7 @@ function integrateVariable(variable: DecimalSource) {
 
 function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) {
     if (variable == null && this.innermostVariable == null) {
-        throw "Cannot integrate non-existent variable";
+        throw new Error("Cannot integrate non-existent variable");
     }
     return variable ?? unref(this.innermostVariable);
 }
@@ -93,7 +93,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
 
     private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties<T> {
         if (inputs.length !== 1) {
-            throw "Evaluate function is required if inputs is not length 1";
+            throw new Error("Evaluate function is required if inputs is not length 1");
         }
         return {
             inputs: inputs as T,
@@ -113,7 +113,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             hasVariable
         } = options;
         if (invert == null && invertIntegral == null && hasVariable) {
-            throw "A formula cannot be marked as having a variable if it is not invertible";
+            throw new Error(
+                "A formula cannot be marked as having a variable if it is not invertible"
+            );
         }
 
         const numVariables = inputs.filter(
@@ -197,7 +199,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         } else if (this.inputs.length === 1 && this.internalHasVariable) {
             return value;
         }
-        throw "Cannot invert non-invertible formula";
+        throw new Error("Cannot invert non-invertible formula");
     }
 
     /**
@@ -213,7 +215,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 // We're the complex operation of this formula
                 stack = [];
                 if (this.internalIntegrate == null) {
-                    throw "Cannot integrate formula with non-existent operation";
+                    throw new Error("Cannot integrate formula with non-existent operation");
                 }
                 let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
                 stack.forEach(func => (value = func(value)));
@@ -225,12 +227,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 } else if (this.inputs.length === 1 && this.internalHasVariable) {
                     return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0]));
                 }
-                throw "Cannot integrate formula without variable";
+                throw new Error("Cannot integrate formula without variable");
             }
         } else {
             // "Inner" part of the formula
             if (this.applySubstitution == null) {
-                throw "Cannot have two complex operations in an integrable formula";
+                throw new Error("Cannot have two complex operations in an integrable formula");
             }
             stack.push((variable: DecimalSource) =>
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -243,7 +245,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             } else if (this.inputs.length === 1 && this.internalHasVariable) {
                 return variable ?? unrefFormulaSource(this.inputs[0]);
             }
-            throw "Cannot integrate formula without variable";
+            throw new Error("Cannot integrate formula without variable");
         }
     }
 
@@ -267,7 +269,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         } else if (this.inputs.length === 1 && this.internalHasVariable) {
             return value;
         }
-        throw "Cannot invert integral of formula without invertible integral";
+        throw new Error("Cannot invert integral of formula without invertible integral");
     }
 
     /**
@@ -346,7 +348,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 }
                 return lhs.invert(value);
             }
-            throw "Could not invert due to no input being a variable";
+            throw new Error("Could not invert due to no input being a variable");
         }
         return new Formula({
             inputs: [value],
@@ -381,7 +383,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         }
         function invertStep(value: DecimalSource, lhs: FormulaSource) {
             if (!hasVariable(lhs)) {
-                throw "Could not invert due to no input being a variable";
+                throw new Error("Could not invert due to no input being a variable");
             }
             if (unref(processedCondition)) {
                 return lhs.invert(formula.invert(value));
@@ -1374,14 +1376,16 @@ export function calculateMaxAffordable(
     return computed(() => {
         if (unref(computedSpendResources)) {
             if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
-                throw "Cannot calculate max affordable of formula with non-invertible integral";
+                throw new Error(
+                    "Cannot calculate max affordable of formula with non-invertible integral"
+                );
             }
             return Decimal.floor(
                 formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
             ).sub(unref(formula.innermostVariable) ?? 0);
         } else {
             if (!formula.isInvertible()) {
-                throw "Cannot calculate max affordable of non-invertible formula";
+                throw new Error("Cannot calculate max affordable of non-invertible formula");
             }
             return Decimal.floor(formula.invert(resource.value));
         }
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index fb860d3..f4dd1f0 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -11,7 +11,7 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.neg(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateNeg(
@@ -22,7 +22,7 @@ export function integrateNeg(
     if (hasVariable(lhs)) {
         return Decimal.neg(lhs.evaluateIntegral(variable, stack));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function applySubstitutionNeg(value: DecimalSource) {
@@ -35,7 +35,7 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAdd(
@@ -57,7 +57,7 @@ export function integrateAdd(
             variable ?? unref(rhs.innermostVariable) ?? 0
         ).add(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function integrateInnerAdd(
@@ -73,7 +73,7 @@ export function integrateInnerAdd(
         const x = rhs.evaluateIntegral(variable, stack);
         return Decimal.add(x, unrefFormulaSource(lhs));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -84,7 +84,7 @@ export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs
         const b = unrefFormulaSource(lhs);
         return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -93,7 +93,7 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateSub(
@@ -115,7 +115,7 @@ export function integrateSub(
             variable ?? unref(rhs.innermostVariable) ?? 0
         ).sub(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function integrateInnerSub(
@@ -131,7 +131,7 @@ export function integrateInnerSub(
         const x = rhs.evaluateIntegral(variable, stack);
         return Decimal.sub(x, unrefFormulaSource(lhs));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -142,7 +142,7 @@ export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs
         const b = unrefFormulaSource(lhs);
         return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -151,7 +151,7 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateMul(
@@ -167,7 +167,7 @@ export function integrateMul(
         const x = rhs.evaluateIntegral(variable, stack);
         return Decimal.times(x, unrefFormulaSource(lhs));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -176,7 +176,7 @@ export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, r
     } else if (hasVariable(rhs)) {
         return Decimal.div(value, unrefFormulaSource(lhs));
     }
-    throw "Could not apply substitution due to no input being a variable";
+    throw new Error("Could not apply substitution due to no input being a variable");
 }
 
 export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -187,7 +187,7 @@ export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs
         const b = unrefFormulaSource(lhs);
         return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -196,7 +196,7 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateDiv(
@@ -212,7 +212,7 @@ export function integrateDiv(
         const x = rhs.evaluateIntegral(variable, stack);
         return Decimal.div(unrefFormulaSource(lhs), x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -221,7 +221,7 @@ export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, r
     } else if (hasVariable(rhs)) {
         return Decimal.mul(value, unrefFormulaSource(lhs));
     }
-    throw "Could not apply substitution due to no input being a variable";
+    throw new Error("Could not apply substitution due to no input being a variable");
 }
 
 export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -232,14 +232,14 @@ export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs
         const b = unrefFormulaSource(lhs);
         return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.recip(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateRecip(
@@ -251,21 +251,21 @@ export function integrateRecip(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.exp(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow10(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateLog10(
@@ -277,7 +277,7 @@ export function integrateLog10(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) {
@@ -286,7 +286,7 @@ export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) {
             Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1))
         );
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -295,7 +295,7 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateLog(
@@ -311,7 +311,7 @@ export function integrateLog(
             .times(x)
             .div(Decimal.ln(unrefFormulaSource(rhs)));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -319,14 +319,14 @@ export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs
         const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value);
         return lhs.invert(numerator.div(numerator.div(Math.E).lambertw()));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow(2, value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateLog2(
@@ -338,21 +338,21 @@ export function integrateLog2(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertLn(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.exp(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateLn(
@@ -364,14 +364,14 @@ export function integrateLn(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x).sub(1).times(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -380,7 +380,7 @@ 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 "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integratePow(
@@ -398,7 +398,7 @@ export function integratePow(
         const b = unrefFormulaSource(lhs);
         return Decimal.pow(b, x).div(Decimal.ln(b));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -409,14 +409,14 @@ export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs
         const denominator = Decimal.ln(unrefFormulaSource(lhs));
         return rhs.invert(Decimal.times(denominator, value).ln().div(denominator));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.root(value, 10));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integratePow10(
@@ -428,7 +428,7 @@ export function integratePow10(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) {
@@ -437,7 +437,7 @@ export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) {
             Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp()
         );
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -446,7 +446,7 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For
     } else if (hasVariable(rhs)) {
         return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integratePowBase(
@@ -464,7 +464,7 @@ export function integratePowBase(
         const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
         return Decimal.pow(x, denominator).div(denominator);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegratePowBase(
@@ -479,7 +479,7 @@ export function invertIntegratePowBase(
         const b = unrefFormulaSource(lhs);
         return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -488,7 +488,7 @@ 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 "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateRoot(
@@ -502,7 +502,7 @@ export function integrateRoot(
         const a = unrefFormulaSource(rhs);
         return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -515,14 +515,14 @@ export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rh
                 .pow(Decimal.div(b, Decimal.add(b, 1)))
         );
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertExp(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.ln(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateExp(
@@ -534,7 +534,7 @@ export function integrateExp(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.exp(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function tetrate(
@@ -556,7 +556,7 @@ export function invertTetrate(
         return base.invert(Decimal.ssqrt(value));
     }
     // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function iteratedexp(
@@ -584,7 +584,7 @@ export function invertIteratedExp(
         );
     }
     // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function iteratedLog(
@@ -608,7 +608,7 @@ export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: Formul
         );
     }
     // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) {
@@ -631,21 +631,21 @@ export function invertLayeradd(
         );
     }
     // Other params can't be inverted ATM
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertLambertw(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow(Math.E, value).times(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.tetrate(value, 2));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) {
@@ -657,7 +657,7 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.asin(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateSin(
@@ -669,14 +669,14 @@ export function integrateSin(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cos(x).neg();
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertCos(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.acos(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateCos(
@@ -688,14 +688,14 @@ export function integrateCos(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.sin(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertTan(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.atan(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateTan(
@@ -707,14 +707,14 @@ export function integrateTan(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cos(x).ln().neg();
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.sin(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAsin(
@@ -728,14 +728,14 @@ export function integrateAsin(
             .times(x)
             .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.cos(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAcos(
@@ -749,14 +749,14 @@ export function integrateAcos(
             .times(x)
             .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.tan(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAtan(
@@ -770,14 +770,14 @@ export function integrateAtan(
             .times(x)
             .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.asinh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateSinh(
@@ -789,14 +789,14 @@ export function integrateSinh(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cosh(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.acosh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateCosh(
@@ -808,14 +808,14 @@ export function integrateCosh(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.sinh(x);
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.atanh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateTanh(
@@ -827,14 +827,14 @@ export function integrateTanh(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.cosh(x).ln();
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.sinh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAsinh(
@@ -846,14 +846,14 @@ export function integrateAsinh(
         const x = lhs.evaluateIntegral(variable, stack);
         return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.cosh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAcosh(
@@ -867,14 +867,14 @@ export function integrateAcosh(
             .times(x)
             .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.tanh(value));
     }
-    throw "Could not invert due to no input being a variable";
+    throw new Error("Could not invert due to no input being a variable");
 }
 
 export function integrateAtanh(
@@ -888,7 +888,7 @@ export function integrateAtanh(
             .times(x)
             .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
     }
-    throw "Could not integrate due to no input being a variable";
+    throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function createPassthroughBinaryFormula(
diff --git a/src/game/persistence.ts b/src/game/persistence.ts
index bf3afa9..b56f52f 100644
--- a/src/game/persistence.ts
+++ b/src/game/persistence.ts
@@ -115,7 +115,7 @@ function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T)
             persistent[SaveDataPath]?.join("."),
             persistent
         );
-        throw "Attempted to set NaN value. See above for details";
+        throw new Error("Attempted to set NaN value. See above for details");
     }
     persistent[PersistentState].value = value;
 }
diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts
index 8d39f22..ec0203b 100644
--- a/src/lib/break_eternity.ts
+++ b/src/lib/break_eternity.ts
@@ -2788,7 +2788,7 @@ export default class Decimal {
             return FC_NN(this.sign, this.layer - 1, this.mag);
         }
 
-        throw "Unhandled behavior in lambertw()";
+        throw new Error("Unhandled behavior in lambertw()");
     }
 
     //The super square-root function - what number, tetrated to height 2, equals this?
diff --git a/src/main.ts b/src/main.ts
index 74893b2..38c9ac3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -26,7 +26,9 @@ declare global {
 document.title = projInfo.title;
 window.projInfo = projInfo;
 if (projInfo.id === "") {
-    throw "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json";
+    throw new Error(
+        "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"
+    );
 }
 
 requestAnimationFrame(async () => {

From d7e26583045bbb2dcac71956667543c793ed9672 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 23 Mar 2023 20:59:41 -0500
Subject: [PATCH 13/36] Fix some tests

---
 src/game/formulas/formulas.ts | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 30a2b20..d6eabcd 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -71,8 +71,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this.inputs = readonlyProperties.inputs;
         this.internalHasVariable = readonlyProperties.internalHasVariable;
         this.innermostVariable = readonlyProperties.innermostVariable;
+        this.internalEvaluate = readonlyProperties.internalEvaluate;
+        this.internalInvert = readonlyProperties.internalInvert;
         this.internalIntegrate = readonlyProperties.internalIntegrate;
         this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
+        this.internalInvertIntegral = readonlyProperties.internalInvertIntegral;
         this.applySubstitution = readonlyProperties.applySubstitution;
     }
 
@@ -135,13 +138,13 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return {
             inputs,
             internalEvaluate: evaluate,
+            internalInvert,
             internalIntegrate: integrate,
             internalIntegrateInner: integrateInner,
+            internalInvertIntegral,
             applySubstitution,
             innermostVariable,
-            internalHasVariable,
-            internalInvert,
-            internalInvertIntegral
+            internalHasVariable
         };
     }
 

From a91efffd5c1bc37513f88bb5b55344b2f5eab6cb Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 1 Apr 2023 15:55:17 -0500
Subject: [PATCH 14/36] WIP integral rework

---
 src/game/formulas/formulas.ts   | 268 ++++++++++-----------
 src/game/formulas/operations.ts | 399 +++++++++++---------------------
 src/game/formulas/types.d.ts    |  17 +-
 tests/game/formulas.test.ts     |  30 ++-
 4 files changed, 283 insertions(+), 431 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index d6eabcd..305ad5b 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -1,7 +1,7 @@
 import { Resource } from "features/resources/resource";
 import Decimal, { DecimalSource } from "util/bignum";
 import { Computable, convertComputable, ProcessedComputable } from "util/computed";
-import { computed, ComputedRef, ref, unref } from "vue";
+import { computed, ComputedRef, Ref, ref, unref } from "vue";
 import type {
     EvaluateFunction,
     FormulaOptions,
@@ -15,7 +15,6 @@ import type {
     InvertFunction,
     InvertibleFormula,
     InvertibleIntegralFormula,
-    InvertIntegralFunction,
     SubstitutionFunction,
     SubstitutionStack
 } from "./types";
@@ -29,8 +28,8 @@ export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSourc
     return value instanceof Formula ? value.evaluate(variable) : unref(value);
 }
 
-function integrateVariable(variable: DecimalSource) {
-    return Decimal.pow(variable, 2).div(2);
+function integrateVariable(this: GenericFormula) {
+    return Formula.pow(this, 2).div(2);
 }
 
 function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) {
@@ -54,11 +53,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     private readonly internalIntegrate: IntegrateFunction<T> | undefined;
     private readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
     private readonly applySubstitution: SubstitutionFunction<T> | undefined;
-    private readonly internalInvertIntegral: InvertIntegralFunction<T> | undefined;
     private readonly internalHasVariable: boolean;
 
     public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
 
+    private integralFormula: GenericFormula | undefined;
+
     constructor(options: FormulaOptions<T>) {
         let readonlyProperties;
         if ("variable" in options) {
@@ -75,7 +75,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this.internalInvert = readonlyProperties.internalInvert;
         this.internalIntegrate = readonlyProperties.internalIntegrate;
         this.internalIntegrateInner = readonlyProperties.internalIntegrateInner;
-        this.internalInvertIntegral = readonlyProperties.internalInvertIntegral;
         this.applySubstitution = readonlyProperties.applySubstitution;
     }
 
@@ -112,10 +111,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             integrate,
             integrateInner,
             applySubstitution,
-            invertIntegral,
             hasVariable
         } = options;
-        if (invert == null && invertIntegral == null && hasVariable) {
+        if (invert == null && hasVariable) {
             throw new Error(
                 "A formula cannot be marked as having a variable if it is not invertible"
             );
@@ -132,8 +130,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             numVariables === 1 || (numVariables === 0 && hasVariable === true);
         const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined;
         const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined;
-        const internalInvertIntegral =
-            internalHasVariable && variable?.isIntegralInvertible() ? invertIntegral : undefined;
 
         return {
             inputs,
@@ -141,13 +137,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             internalInvert,
             internalIntegrate: integrate,
             internalIntegrateInner: integrateInner,
-            internalInvertIntegral,
             applySubstitution,
             innermostVariable,
             internalHasVariable
         };
     }
 
+    private calculateConstantOfIntegration() {
+        // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
+        const integral = this.getIntegralFormula().evaluate(1);
+        const actualCost = this.evaluate(0);
+        return Decimal.sub(actualCost, integral);
+    }
+
     /** Type predicate that this formula can be inverted. */
     isInvertible(): this is InvertibleFormula {
         return (
@@ -163,10 +165,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
 
     /** Type predicate that this formula has an integral function that can be inverted. */
     isIntegralInvertible(): this is InvertibleIntegralFormula {
-        return (
-            this.internalHasVariable &&
-            (this.internalInvertIntegral != null || this.internalEvaluate == null)
-        );
+        if (!this.isIntegrable()) {
+            return false;
+        }
+        return this.getIntegralFormula().isInvertible();
     }
 
     /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */
@@ -208,71 +210,101 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     /**
      * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/).
      * @param variable Optionally override the value of the variable while evaluating
-     * @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas
      * @see {@link isIntegrable}
      */
-    evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource {
+    evaluateIntegral(variable?: DecimalSource): DecimalSource {
+        if (!this.isIntegrable()) {
+            throw new Error("Cannot evaluate integral of formula without integral");
+        }
+        return Decimal.add(
+            this.getIntegralFormula().evaluate(variable),
+            this.calculateConstantOfIntegration()
+        );
+    }
+
+    /**
+     * Given the potential result of the formula's integral (and the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible.
+     * @param value The result of the integral.
+     * @see {@link isIntegralInvertible}
+     */
+    invertIntegral(value: DecimalSource): DecimalSource {
+        if (this.integralFormula?.isInvertible()) {
+            throw new Error("Cannot invert integral of formula without invertible integral");
+        }
+        return this.getIntegralFormula().invert(value);
+    }
+
+    /**
+     * Get a formula that will evaluate to the integral of this formula. May also be invertible.
+     * @param variable The variable that will be used to evaluate this integral at a given x value
+     * @param stack For nested formulas, a stack of operations that occur outside the complex operation
+     */
+    getIntegralFormula(
+        variable?: ProcessedComputable<DecimalSource>,
+        stack?: SubstitutionStack
+    ): GenericFormula {
+        if (variable == null && this.integralFormula != null) {
+            return this.integralFormula;
+        }
+        let formula;
+        const variablePresent = variable != null;
+        if (variable == null) {
+            variable = this.innermostVariable;
+            if (variable == null) {
+                throw new Error("Cannot integrate formula without variable");
+            }
+        }
         if (stack == null) {
             // "Outer" part of the formula
             if (this.applySubstitution == null) {
                 // We're the complex operation of this formula
                 stack = [];
                 if (this.internalIntegrate == null) {
-                    throw new Error("Cannot integrate formula with non-existent operation");
+                    throw new Error("Cannot integrate formula with non-integrable operation");
                 }
                 let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
                 stack.forEach(func => (value = func(value)));
-                return value;
+                formula = value;
             } else {
                 // Continue digging into the formula
                 if (this.internalIntegrate) {
-                    return this.internalIntegrate.call(this, variable, undefined, ...this.inputs);
+                    formula = this.internalIntegrate.call(
+                        this,
+                        variable,
+                        undefined,
+                        ...this.inputs
+                    );
                 } else if (this.inputs.length === 1 && this.internalHasVariable) {
-                    return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0]));
+                    // eslint-disable-next-line @typescript-eslint/no-this-alias
+                    formula = this;
+                } else {
+                    throw new Error("Cannot integrate formula without variable");
                 }
-                throw new Error("Cannot integrate formula without variable");
             }
         } else {
             // "Inner" part of the formula
             if (this.applySubstitution == null) {
                 throw new Error("Cannot have two complex operations in an integrable formula");
             }
-            stack.push((variable: DecimalSource) =>
+            stack.push((variable: GenericFormula) =>
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 this.applySubstitution!.call(this, variable, ...this.inputs)
             );
             if (this.internalIntegrateInner) {
-                return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
+                formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
             } else if (this.internalIntegrate) {
-                return this.internalIntegrate.call(this, variable, stack, ...this.inputs);
+                formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
             } else if (this.inputs.length === 1 && this.internalHasVariable) {
-                return variable ?? unrefFormulaSource(this.inputs[0]);
+                // eslint-disable-next-line @typescript-eslint/no-this-alias
+                formula = this;
+            } else {
+                throw new Error("Cannot integrate formula without variable");
             }
-            throw new Error("Cannot integrate formula without variable");
         }
-    }
-
-    calculateConstantOfIntegration() {
-        // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
-        const integral = this.evaluateIntegral(1);
-        const actualCost = this.evaluate(0);
-        return Decimal.sub(actualCost, integral);
-    }
-
-    /**
-     * Given the potential result of the formula's integral (sand the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible.
-     * @param value The result of the integral.
-     * @see {@link isIntegralInvertible}
-     */
-    invertIntegral(value: DecimalSource): DecimalSource {
-        // This is nearly completely non-functional
-        // Proper nesting will require somehow using integration by substitution or integration by parts
-        if (this.internalInvertIntegral) {
-            return this.internalInvertIntegral.call(this, value, ...this.inputs);
-        } else if (this.inputs.length === 1 && this.internalHasVariable) {
-            return value;
+        if (!variablePresent) {
+            this.integralFormula = formula;
         }
-        throw new Error("Cannot invert integral of formula without invertible integral");
+        return formula;
     }
 
     /**
@@ -292,7 +324,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             this.internalEvaluate === other.internalEvaluate &&
             this.internalInvert === other.internalInvert &&
             this.internalIntegrate === other.internalIntegrate &&
-            this.internalInvertIntegral === other.internalInvertIntegral &&
             this.internalHasVariable === other.internalHasVariable
         );
     }
@@ -415,7 +446,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({ inputs: [value], evaluate: Decimal.abs });
     }
 
-    public static neg<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static neg<T extends GenericFormula>(value: T): T;
     public static neg(value: FormulaSource): GenericFormula;
     public static neg(value: FormulaSource) {
         return new Formula({
@@ -460,8 +491,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invert: ops.invertAdd,
             integrate: ops.integrateAdd,
             integrateInner: ops.integrateInnerAdd,
-            applySubstitution: ops.passthrough,
-            invertIntegral: ops.invertIntegrateAdd
+            applySubstitution: ops.passthrough
         });
     }
     public static plus = Formula.add;
@@ -476,8 +506,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invert: ops.invertSub,
             integrate: ops.integrateSub,
             integrateInner: ops.integrateInnerSub,
-            applySubstitution: ops.passthrough,
-            invertIntegral: ops.invertIntegrateSub
+            applySubstitution: ops.passthrough
         });
     }
     public static subtract = Formula.sub;
@@ -492,8 +521,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.mul,
             invert: ops.invertMul,
             integrate: ops.integrateMul,
-            applySubstitution: ops.applySubstitutionMul,
-            invertIntegral: ops.invertIntegrateMul
+            applySubstitution: ops.applySubstitutionMul
         });
     }
     public static multiply = Formula.mul;
@@ -508,8 +536,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             evaluate: Decimal.div,
             invert: ops.invertDiv,
             integrate: ops.integrateDiv,
-            applySubstitution: ops.applySubstitutionDiv,
-            invertIntegral: ops.invertIntegrateDiv
+            applySubstitution: ops.applySubstitutionDiv
         });
     }
     public static divide = Formula.div;
@@ -523,8 +550,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.recip,
             invert: ops.invertRecip,
-            integrate: ops.integrateRecip,
-            invertIntegral: ops.invertIntegrateRecip
+            integrate: ops.integrateRecip
         });
     }
     public static reciprocal = Formula.recip;
@@ -537,10 +563,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             invert: ops.passthrough as (
                 value: DecimalSource,
                 ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource,
-            invertIntegral: ops.passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
             ) => DecimalSource
         });
     }
@@ -559,12 +581,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({
             inputs: [value, min, max],
             evaluate: Decimal.clamp,
-            invert: ops.passthrough as InvertFunction<
-                [FormulaSource, FormulaSource, FormulaSource]
-            >,
-            invertIntegral: ops.passthrough as InvertFunction<
-                [FormulaSource, FormulaSource, FormulaSource]
-            >
+            invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]>
         });
     }
 
@@ -583,8 +600,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.log10,
             invert: ops.invertLog10,
-            integrate: ops.integrateLog10,
-            invertIntegral: ops.invertIntegrateLog10
+            integrate: ops.integrateLog10
         });
     }
 
@@ -596,8 +612,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value, base],
             evaluate: Decimal.log,
             invert: ops.invertLog,
-            integrate: ops.integrateLog,
-            invertIntegral: ops.invertIntegrateLog
+            integrate: ops.integrateLog
         });
     }
     public static logarithm = Formula.log;
@@ -609,8 +624,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.log2,
             invert: ops.invertLog2,
-            integrate: ops.integrateLog2,
-            invertIntegral: ops.invertIntegrateLog2
+            integrate: ops.integrateLog2
         });
     }
 
@@ -621,8 +635,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.ln,
             invert: ops.invertLn,
-            integrate: ops.integrateLn,
-            invertIntegral: ops.invertIntegrateLn
+            integrate: ops.integrateLn
         });
     }
 
@@ -634,8 +647,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value, other],
             evaluate: Decimal.pow,
             invert: ops.invertPow,
-            integrate: ops.integratePow,
-            invertIntegral: ops.invertIntegratePow
+            integrate: ops.integratePow
         });
     }
 
@@ -646,8 +658,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value],
             evaluate: Decimal.pow10,
             invert: ops.invertPow10,
-            integrate: ops.integratePow10,
-            invertIntegral: ops.invertIntegratePow10
+            integrate: ops.integratePow10
         });
     }
 
@@ -659,8 +670,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value, other],
             evaluate: Decimal.pow_base,
             invert: ops.invertPowBase,
-            integrate: ops.integratePowBase,
-            invertIntegral: ops.invertIntegratePowBase
+            integrate: ops.integratePowBase
         });
     }
 
@@ -672,8 +682,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             inputs: [value, other],
             evaluate: Decimal.root,
             invert: ops.invertRoot,
-            integrate: ops.integrateRoot,
-            invertIntegral: ops.invertIntegrateRoot
+            integrate: ops.integrateRoot
         });
     }
 
@@ -689,7 +698,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({ inputs: [value], evaluate: Decimal.lngamma });
     }
 
-    public static exp<T extends GenericFormula>(value: T): Omit<T, "invertsIntegral">;
+    public static exp<T extends GenericFormula>(value: T): T;
     public static exp(value: FormulaSource): GenericFormula;
     public static exp(value: FormulaSource) {
         return new Formula({
@@ -728,7 +737,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         value: T,
         height?: FormulaSource,
         payload?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public static tetrate(
         value: FormulaSource,
         height?: FormulaSource,
@@ -750,7 +759,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         value: T,
         height?: FormulaSource,
         payload?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public static iteratedexp(
         value: FormulaSource,
         height?: FormulaSource,
@@ -779,7 +788,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public static slog<T extends GenericFormula>(
         value: T,
         base?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula;
     public static slog(value: FormulaSource, base: FormulaSource = 10) {
         return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog });
@@ -793,7 +802,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         value: T,
         diff: FormulaSource,
         base?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public static layeradd(
         value: FormulaSource,
         diff: FormulaSource,
@@ -807,9 +816,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static lambertw<T extends GenericFormula>(
-        value: T
-    ): Omit<T, "integrate" | "invertIntegral">;
+    public static lambertw<T extends GenericFormula>(value: T): Omit<T, "integrate">;
     public static lambertw(value: FormulaSource): GenericFormula;
     public static lambertw(value: FormulaSource) {
         return new Formula({
@@ -819,9 +826,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static ssqrt<T extends GenericFormula>(
-        value: T
-    ): Omit<T, "integrate" | "invertIntegral">;
+    public static ssqrt<T extends GenericFormula>(value: T): Omit<T, "integrate">;
     public static ssqrt(value: FormulaSource): GenericFormula;
     public static ssqrt(value: FormulaSource) {
         return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt });
@@ -835,7 +840,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate });
     }
 
-    public static sin<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static sin<T extends GenericFormula>(value: T): T;
     public static sin(value: FormulaSource): GenericFormula;
     public static sin(value: FormulaSource) {
         return new Formula({
@@ -846,7 +851,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static cos<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static cos<T extends GenericFormula>(value: T): T;
     public static cos(value: FormulaSource): GenericFormula;
     public static cos(value: FormulaSource) {
         return new Formula({
@@ -857,7 +862,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static tan<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static tan<T extends GenericFormula>(value: T): T;
     public static tan(value: FormulaSource): GenericFormula;
     public static tan(value: FormulaSource) {
         return new Formula({
@@ -868,7 +873,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static asin<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static asin<T extends GenericFormula>(value: T): T;
     public static asin(value: FormulaSource): GenericFormula;
     public static asin(value: FormulaSource) {
         return new Formula({
@@ -879,7 +884,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static acos<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static acos<T extends GenericFormula>(value: T): T;
     public static acos(value: FormulaSource): GenericFormula;
     public static acos(value: FormulaSource) {
         return new Formula({
@@ -890,7 +895,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static atan<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static atan<T extends GenericFormula>(value: T): T;
     public static atan(value: FormulaSource): GenericFormula;
     public static atan(value: FormulaSource) {
         return new Formula({
@@ -901,7 +906,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static sinh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static sinh<T extends GenericFormula>(value: T): T;
     public static sinh(value: FormulaSource): GenericFormula;
     public static sinh(value: FormulaSource) {
         return new Formula({
@@ -912,7 +917,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static cosh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static cosh<T extends GenericFormula>(value: T): T;
     public static cosh(value: FormulaSource): GenericFormula;
     public static cosh(value: FormulaSource) {
         return new Formula({
@@ -923,7 +928,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static tanh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static tanh<T extends GenericFormula>(value: T): T;
     public static tanh(value: FormulaSource): GenericFormula;
     public static tanh(value: FormulaSource) {
         return new Formula({
@@ -934,7 +939,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static asinh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static asinh<T extends GenericFormula>(value: T): T;
     public static asinh(value: FormulaSource): GenericFormula;
     public static asinh(value: FormulaSource) {
         return new Formula({
@@ -945,7 +950,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static acosh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static acosh<T extends GenericFormula>(value: T): T;
     public static acosh(value: FormulaSource): GenericFormula;
     public static acosh(value: FormulaSource) {
         return new Formula({
@@ -956,7 +961,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         });
     }
 
-    public static atanh<T extends GenericFormula>(value: T): Omit<T, "invertIntegral">;
+    public static atanh<T extends GenericFormula>(value: T): T;
     public static atanh(value: FormulaSource): GenericFormula;
     public static atanh(value: FormulaSource) {
         return new Formula({
@@ -997,7 +1002,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return Formula.abs(this);
     }
 
-    public neg<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public neg<T extends GenericFormula>(this: T): T;
     public neg(this: GenericFormula): GenericFormula;
     public neg(this: GenericFormula) {
         return Formula.neg(this);
@@ -1170,7 +1175,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return Formula.lngamma(this);
     }
 
-    public exp<T extends GenericFormula>(this: T): Omit<T, "invertsIntegral">;
+    public exp<T extends GenericFormula>(this: T): T;
     public exp(this: FormulaSource): GenericFormula;
     public exp(this: FormulaSource) {
         return Formula.exp(this);
@@ -1203,7 +1208,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this: T,
         height?: FormulaSource,
         payload?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public tetrate(
         this: FormulaSource,
         height?: FormulaSource,
@@ -1221,7 +1226,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this: T,
         height?: FormulaSource,
         payload?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public iteratedexp(
         this: FormulaSource,
         height?: FormulaSource,
@@ -1239,10 +1244,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return Formula.iteratedlog(this, base, times);
     }
 
-    public slog<T extends GenericFormula>(
-        this: T,
-        base?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    public slog<T extends GenericFormula>(this: T, base?: FormulaSource): Omit<T, "integrate">;
     public slog(this: FormulaSource, base?: FormulaSource): GenericFormula;
     public slog(this: FormulaSource, base: FormulaSource = 10) {
         return Formula.slog(this, base);
@@ -1256,19 +1258,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         this: T,
         diff: FormulaSource,
         base?: FormulaSource
-    ): Omit<T, "integrate" | "invertIntegral">;
+    ): Omit<T, "integrate">;
     public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula;
     public layeradd(this: FormulaSource, diff: FormulaSource, base: FormulaSource) {
         return Formula.layeradd(this, diff, base);
     }
 
-    public lambertw<T extends GenericFormula>(this: T): Omit<T, "integrate" | "invertIntegral">;
+    public lambertw<T extends GenericFormula>(this: T): Omit<T, "integrate">;
     public lambertw(this: FormulaSource): GenericFormula;
     public lambertw(this: FormulaSource) {
         return Formula.lambertw(this);
     }
 
-    public ssqrt<T extends GenericFormula>(this: T): Omit<T, "integrate" | "invertIntegral">;
+    public ssqrt<T extends GenericFormula>(this: T): Omit<T, "integrate">;
     public ssqrt(this: FormulaSource): GenericFormula;
     public ssqrt(this: FormulaSource) {
         return Formula.ssqrt(this);
@@ -1281,73 +1283,73 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return Formula.pentate(this, height, payload);
     }
 
-    public sin<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public sin<T extends GenericFormula>(this: T): T;
     public sin(this: FormulaSource): GenericFormula;
     public sin(this: FormulaSource) {
         return Formula.sin(this);
     }
 
-    public cos<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public cos<T extends GenericFormula>(this: T): T;
     public cos(this: FormulaSource): GenericFormula;
     public cos(this: FormulaSource) {
         return Formula.cos(this);
     }
 
-    public tan<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public tan<T extends GenericFormula>(this: T): T;
     public tan(this: FormulaSource): GenericFormula;
     public tan(this: FormulaSource) {
         return Formula.tan(this);
     }
 
-    public asin<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public asin<T extends GenericFormula>(this: T): T;
     public asin(this: FormulaSource): GenericFormula;
     public asin(this: FormulaSource) {
         return Formula.asin(this);
     }
 
-    public acos<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public acos<T extends GenericFormula>(this: T): T;
     public acos(this: FormulaSource): GenericFormula;
     public acos(this: FormulaSource) {
         return Formula.acos(this);
     }
 
-    public atan<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public atan<T extends GenericFormula>(this: T): T;
     public atan(this: FormulaSource): GenericFormula;
     public atan(this: FormulaSource) {
         return Formula.atan(this);
     }
 
-    public sinh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public sinh<T extends GenericFormula>(this: T): T;
     public sinh(this: FormulaSource): GenericFormula;
     public sinh(this: FormulaSource) {
         return Formula.sinh(this);
     }
 
-    public cosh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public cosh<T extends GenericFormula>(this: T): T;
     public cosh(this: FormulaSource): GenericFormula;
     public cosh(this: FormulaSource) {
         return Formula.cosh(this);
     }
 
-    public tanh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public tanh<T extends GenericFormula>(this: T): T;
     public tanh(this: FormulaSource): GenericFormula;
     public tanh(this: FormulaSource) {
         return Formula.tanh(this);
     }
 
-    public asinh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public asinh<T extends GenericFormula>(this: T): T;
     public asinh(this: FormulaSource): GenericFormula;
     public asinh(this: FormulaSource) {
         return Formula.asinh(this);
     }
 
-    public acosh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public acosh<T extends GenericFormula>(this: T): T;
     public acosh(this: FormulaSource): GenericFormula;
     public acosh(this: FormulaSource) {
         return Formula.acosh(this);
     }
 
-    public atanh<T extends GenericFormula>(this: T): Omit<T, "invertIntegral">;
+    public atanh<T extends GenericFormula>(this: T): T;
     public atanh(this: FormulaSource): GenericFormula;
     public atanh(this: FormulaSource) {
         return Formula.atanh(this);
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index f4dd1f0..4512f55 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -1,9 +1,9 @@
 import Decimal, { DecimalSource } from "util/bignum";
-import { unref } from "vue";
+import { Ref } from "vue";
 import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
-import { FormulaSource, InvertFunction, SubstitutionStack } from "./types";
+import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
 
-export function passthrough(value: DecimalSource) {
+export function passthrough<T extends GenericFormula | DecimalSource>(value: T): T {
     return value;
 }
 
@@ -15,18 +15,18 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateNeg(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        return Decimal.neg(lhs.evaluateIntegral(variable, stack));
+        return Formula.neg(lhs.getIntegralFormula(variable, stack));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function applySubstitutionNeg(value: DecimalSource) {
-    return Decimal.neg(value);
+export function applySubstitutionNeg(value: GenericFormula) {
+    return Formula.neg(value);
 }
 
 export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
@@ -39,54 +39,37 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integrateAdd(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(rhs),
-            variable ?? unref(lhs.innermostVariable) ?? 0
-        ).add(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(lhs),
-            variable ?? unref(rhs.innermostVariable) ?? 0
-        ).add(x);
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function integrateInnerAdd(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.add(x, unrefFormulaSource(rhs));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.add(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.add(x, unrefFormulaSource(lhs));
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.add(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs)));
@@ -97,54 +80,37 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integrateSub(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(
-            x,
-            Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0)
-        );
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0));
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(
-            unrefFormulaSource(lhs),
-            variable ?? unref(rhs.innermostVariable) ?? 0
-        ).sub(x);
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
 export function integrateInnerSub(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(x, unrefFormulaSource(rhs));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.sub(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.sub(x, unrefFormulaSource(lhs));
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.sub(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs)));
@@ -155,41 +121,34 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integrateMul(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.times(x, unrefFormulaSource(rhs));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.times(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.times(x, unrefFormulaSource(lhs));
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.times(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+export function applySubstitutionMul(
+    value: GenericFormula,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.div(value, unrefFormulaSource(rhs));
+        return Formula.div(value, rhs);
     } else if (hasVariable(rhs)) {
-        return Decimal.div(value, unrefFormulaSource(lhs));
+        return Formula.div(value, lhs);
     }
     throw new Error("Could not apply substitution due to no input being a variable");
 }
 
-export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b)));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs)));
@@ -200,41 +159,34 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integrateDiv(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.div(x, unrefFormulaSource(rhs));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.div(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        return Decimal.div(unrefFormulaSource(lhs), x);
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.div(lhs, x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
+export function applySubstitutionDiv(
+    value: GenericFormula,
+    lhs: FormulaSource,
+    rhs: FormulaSource
+) {
     if (hasVariable(lhs)) {
-        return Decimal.mul(value, unrefFormulaSource(rhs));
+        return Formula.mul(value, rhs);
     } else if (hasVariable(rhs)) {
-        return Decimal.mul(value, unrefFormulaSource(lhs));
+        return Formula.mul(value, lhs);
     }
     throw new Error("Could not apply substitution due to no input being a variable");
 }
 
-export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b)));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.recip(value));
@@ -243,24 +195,17 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateRecip(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateRecip(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");
-}
-
 export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow10(value));
@@ -269,26 +214,17 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateLog10(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x).sub(1).times(x).div(Formula.ln(10));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1))
-        );
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value));
@@ -299,29 +235,18 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integrateLog(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x)
-            .sub(1)
-            .times(x)
-            .div(Decimal.ln(unrefFormulaSource(rhs)));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x).sub(1).times(x).div(Formula.ln(rhs));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        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");
-}
-
 export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.pow(2, value));
@@ -330,24 +255,17 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateLog2(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x).sub(1).times(x).div(Formula.ln(2));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1)));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertLn(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.exp(value));
@@ -356,24 +274,17 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateLn(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x).sub(1).times(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1)));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs)));
@@ -384,34 +295,22 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
 }
 
 export function integratePow(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const pow = Decimal.add(unrefFormulaSource(rhs), 1);
-        return Decimal.pow(x, pow).div(pow);
+        const x = lhs.getIntegralFormula(variable, stack);
+        const pow = Formula.add(rhs, 1);
+        return Formula.pow(x, pow).div(pow);
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        const b = unrefFormulaSource(lhs);
-        return Decimal.pow(b, x).div(Decimal.ln(b));
+        const x = rhs.getIntegralFormula(variable, stack);
+        return Formula.pow(lhs, x).div(Formula.ln(lhs));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
-    } else if (hasVariable(rhs)) {
-        const denominator = Decimal.ln(unrefFormulaSource(lhs));
-        return rhs.invert(Decimal.times(denominator, value).ln().div(denominator));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.root(value, 10));
@@ -420,26 +319,17 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integratePow10(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
 
-export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(
-            Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp()
-        );
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs)));
@@ -450,38 +340,22 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For
 }
 
 export function integratePowBase(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const b = unrefFormulaSource(rhs);
-        return Decimal.pow(b, x).div(Decimal.ln(b));
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.pow(rhs, x).div(Formula.ln(rhs));
     } else if (hasVariable(rhs)) {
-        const x = rhs.evaluateIntegral(variable, stack);
-        const denominator = Decimal.add(unrefFormulaSource(lhs), 1);
-        return Decimal.pow(x, denominator).div(denominator);
+        const x = rhs.getIntegralFormula(variable, 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");
 }
 
-export function invertIntegratePowBase(
-    value: DecimalSource,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b)));
-    } else if (hasVariable(rhs)) {
-        const b = unrefFormulaSource(lhs);
-        return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1)));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs))));
@@ -492,32 +366,18 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
 }
 
 export function integrateRoot(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        const a = unrefFormulaSource(rhs);
-        return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1));
+        const x = lhs.getIntegralFormula(variable, 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");
 }
 
-export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        const b = unrefFormulaSource(rhs);
-        return lhs.invert(
-            Decimal.add(b, 1)
-                .times(value)
-                .div(b)
-                .pow(Decimal.div(b, Decimal.add(b, 1)))
-        );
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function invertExp(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
         return lhs.invert(Decimal.ln(value));
@@ -526,13 +386,13 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateExp(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.exp(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.exp(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -661,13 +521,13 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateSin(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cos(x).neg();
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.cos(x).neg();
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -680,13 +540,13 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateCos(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sin(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.sin(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -699,13 +559,13 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateTan(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cos(x).ln().neg();
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.cos(x).ln().neg();
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -718,15 +578,15 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAsin(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.asin(x)
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.asin(x)
             .times(x)
-            .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
+            .add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -739,15 +599,15 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAcos(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.acos(x)
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.acos(x)
             .times(x)
-            .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2))));
+            .sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -760,15 +620,15 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAtan(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.atan(x)
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.atan(x)
             .times(x)
-            .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2));
+            .sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -781,13 +641,13 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateSinh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cosh(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.cosh(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -800,13 +660,13 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateCosh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.sinh(x);
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.sinh(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -819,13 +679,13 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateTanh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.cosh(x).ln();
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.cosh(x).ln();
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -838,13 +698,13 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAsinh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt());
+        const x = lhs.getIntegralFormula(variable, 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");
 }
@@ -857,15 +717,15 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAcosh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.acosh(x)
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.acosh(x)
             .times(x)
-            .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt()));
+            .sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -878,15 +738,15 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
 }
 
 export function integrateAtanh(
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.evaluateIntegral(variable, stack);
-        return Decimal.atanh(x)
+        const x = lhs.getIntegralFormula(variable, stack);
+        return Formula.atanh(x)
             .times(x)
-            .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2));
+            .add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -898,7 +758,6 @@ export function createPassthroughBinaryFormula(
         new Formula({
             inputs: [value, other],
             evaluate: operation,
-            invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>,
-            invertIntegral: passthrough as InvertFunction<[FormulaSource, FormulaSource]>
+            invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>
         });
 }
diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts
index baaf2bb..3d3bc83 100644
--- a/src/game/formulas/types.d.ts
+++ b/src/game/formulas/types.d.ts
@@ -22,20 +22,15 @@ type EvaluateFunction<T> = (
 type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
 type IntegrateFunction<T> = (
     this: Formula<T>,
-    variable: DecimalSource | undefined,
+    variable: Ref<DecimalSource>,
     stack: SubstitutionStack | undefined,
     ...inputs: T
-) => DecimalSource;
+) => GenericFormula;
 type SubstitutionFunction<T> = (
     this: Formula<T>,
-    variable: DecimalSource,
+    variable: GenericFormula,
     ...inputs: T
-) => DecimalSource;
-type InvertIntegralFunction<T> = (
-    this: Formula<T>,
-    value: DecimalSource,
-    ...inputs: T
-) => DecimalSource;
+) => GenericFormula;
 
 type VariableFormulaOptions = { variable: ProcessedComputable<DecimalSource> };
 type ConstantFormulaOptions = {
@@ -48,7 +43,6 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
     integrate?: IntegrateFunction<T>;
     integrateInner?: IntegrateFunction<T>;
     applySubstitution?: SubstitutionFunction<T>;
-    invertIntegral?: InvertIntegralFunction<T>;
     hasVariable?: boolean;
 };
 type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
@@ -63,12 +57,11 @@ type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
     internalInvert?: InvertFunction<T>;
     internalIntegrate?: IntegrateFunction<T>;
     internalIntegrateInner?: IntegrateFunction<T>;
-    internalInvertIntegral?: InvertIntegralFunction<T>;
     applySubstitution?: SubstitutionFunction<T>;
     innermostVariable?: ProcessedComputable<DecimalSource>;
 };
 
-type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined;
+type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined;
 
 // It's really hard to type mapped tuples, but these classes seem to manage
 type FormulasToDecimals<T extends FormulaSource[]> = {
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 4003051..ce0120b 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -491,8 +491,8 @@ describe("Integrating", () => {
         constant = Formula.constant(10);
     });
 
-    test("evaluateIntegral() returns variable's value", () =>
-        expect(variable.evaluate()).compare_tolerance(10));
+    test("variable.evaluateIntegral() calculates correctly", () =>
+        expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2)));
     test("evaluateIntegral(variable) overrides variable value", () =>
         expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400));
 
@@ -569,14 +569,10 @@ describe("Integrating", () => {
         const actualCost = new Array(10)
             .fill(null)
             .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
-        const calculatedCost = Decimal.add(
-            formula.evaluateIntegral(),
-            formula.calculateConstantOfIntegration()
-        );
         // Check if the calculated cost is within 10% of the actual cost,
         // because this is an approximation
         expect(
-            Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+            Decimal.sub(actualCost, formula.evaluateIntegral()).abs().div(actualCost).toNumber()
         ).toBeLessThan(0.1);
     });
 
@@ -594,8 +590,10 @@ describe("Inverting integrals", () => {
         constant = Formula.constant(10);
     });
 
-    test("variable.invertIntegral() is pass-through", () =>
-        expect(variable.invertIntegral(20)).compare_tolerance(20));
+    test("variable.invertIntegral() calculates correctly", () =>
+        expect(variable.invertIntegral(20)).compare_tolerance(
+            Decimal.sqrt(20).times(Decimal.sqrt(2))
+        ));
 
     describe("Invertible Integral functions marked as such", () => {
         function checkFormula(formula: GenericFormula) {
@@ -670,7 +668,7 @@ describe("Inverting integrals", () => {
 
     test("Inverting integral of nested formulas", () => {
         const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
-        expect(formula.invertIntegral(7000000)).compare_tolerance(10);
+        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
     });
 
     test("Inverting integral of nested complex formulas", () => {
@@ -946,7 +944,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [],
                     evaluate: () => 10,
-                    integrate: () => 20
+                    integrate: variable => variable
                 }).evaluateIntegral()
             ).compare_tolerance(20));
         test("One input integrates correctly", () =>
@@ -954,7 +952,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable],
                     evaluate: () => 10,
-                    integrate: (val, stack, v1) => val ?? 20
+                    integrate: (variable, stack, v1) => Formula.add(variable, v1)
                 }).evaluateIntegral()
             ).compare_tolerance(20));
         test("Two inputs integrates correctly", () =>
@@ -962,7 +960,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable, 2],
                     evaluate: (v1, v2) => 10,
-                    integrate: (v1, v2) => 3
+                    integrate: (variable, v1, v2) => variable
                 }).evaluateIntegral()
             ).compare_tolerance(3));
     });
@@ -973,7 +971,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [],
                     evaluate: () => 10,
-                    invertIntegral: () => 1,
+                    integrate: variable => variable,
                     hasVariable: true
                 }).invertIntegral(8)
             ).toThrow());
@@ -982,7 +980,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable],
                     evaluate: () => 10,
-                    invertIntegral: (val, v1) => 1,
+                    integrate: (variable, stack, v1) => variable,
                     hasVariable: true
                 }).invertIntegral(8)
             ).compare_tolerance(1));
@@ -991,7 +989,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable, 2],
                     evaluate: (v1, v2) => 10,
-                    invertIntegral: (v1, v2) => 1,
+                    integrate: (variable, v1, v2) => variable,
                     hasVariable: true
                 }).invertIntegral(8)
             ).compare_tolerance(1));

From 6115b6687dfe325afb746b21fce5044845d1f027 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 1 Apr 2023 23:42:12 -0500
Subject: [PATCH 15/36] Fixing more tests  with integral rework

---
 src/data/projEntry.tsx          |   7 +-
 src/game/formulas/formulas.ts   | 162 +++++++--------
 src/game/formulas/operations.ts | 340 ++++++++++++++------------------
 src/game/formulas/types.d.ts    |   4 +-
 tests/game/formulas.test.ts     |  97 ++++-----
 tests/utils.ts                  |   6 +-
 6 files changed, 264 insertions(+), 352 deletions(-)

diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index df86d6a..55e8500 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -4,7 +4,11 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso
 import type { GenericTree } from "features/trees/tree";
 import { branchedResetPropagation, createTree } from "features/trees/tree";
 import { globalBus } from "game/events";
-import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas";
+import Formula, {
+    calculateCost,
+    calculateMaxAffordable,
+    findNonInvertible
+} from "game/formulas/formulas";
 import type { BaseLayer, GenericLayer } from "game/layers";
 import { createLayer } from "game/layers";
 import type { Player } from "game/player";
@@ -18,6 +22,7 @@ import prestige from "./layers/prestige";
 window.Formula = Formula;
 window.calculateMaxAffordable = calculateMaxAffordable;
 window.calculateCost = calculateCost;
+window.findNonInvertible = findNonInvertible;
 window.unref = unref;
 window.ref = ref;
 window.createResource = createResource;
diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 305ad5b..8161903 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -1,7 +1,8 @@
 import { Resource } from "features/resources/resource";
 import Decimal, { DecimalSource } from "util/bignum";
 import { Computable, convertComputable, ProcessedComputable } from "util/computed";
-import { computed, ComputedRef, Ref, ref, unref } from "vue";
+import { computed, ComputedRef, ref, unref } from "vue";
+import * as ops from "./operations";
 import type {
     EvaluateFunction,
     FormulaOptions,
@@ -18,7 +19,6 @@ import type {
     SubstitutionFunction,
     SubstitutionStack
 } from "./types";
-import * as ops from "./operations";
 
 export function hasVariable(value: FormulaSource): value is InvertibleFormula {
     return value instanceof Formula && value.hasVariable();
@@ -32,13 +32,6 @@ function integrateVariable(this: GenericFormula) {
     return Formula.pow(this, 2).div(2);
 }
 
-function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) {
-    if (variable == null && this.innermostVariable == null) {
-        throw new Error("Cannot integrate non-existent variable");
-    }
-    return variable ?? unref(this.innermostVariable);
-}
-
 /**
  * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
  * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@@ -53,7 +46,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     private readonly internalIntegrate: IntegrateFunction<T> | undefined;
     private readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
     private readonly applySubstitution: SubstitutionFunction<T> | undefined;
-    private readonly internalHasVariable: boolean;
+    private readonly internalVariables: number;
 
     public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
 
@@ -69,7 +62,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             readonlyProperties = this.setupFormula(options);
         }
         this.inputs = readonlyProperties.inputs;
-        this.internalHasVariable = readonlyProperties.internalHasVariable;
+        this.internalVariables = readonlyProperties.internalVariables;
         this.innermostVariable = readonlyProperties.innermostVariable;
         this.internalEvaluate = readonlyProperties.internalEvaluate;
         this.internalInvert = readonlyProperties.internalInvert;
@@ -85,10 +78,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     }): InternalFormulaProperties<T> {
         return {
             inputs: [variable] as T,
-            internalHasVariable: true,
+            internalVariables: 1,
             innermostVariable: variable,
-            internalIntegrate: integrateVariable as unknown as IntegrateFunction<T>,
-            internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction<T>,
+            internalIntegrate: integrateVariable,
             applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
         };
     }
@@ -99,68 +91,49 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         }
         return {
             inputs: inputs as T,
-            internalHasVariable: false
+            internalVariables: 0
         };
     }
 
     private setupFormula(options: GeneralFormulaOptions<T>): InternalFormulaProperties<T> {
-        const {
-            inputs,
-            evaluate,
-            invert,
-            integrate,
-            integrateInner,
-            applySubstitution,
-            hasVariable
-        } = options;
-        if (invert == null && hasVariable) {
-            throw new Error(
-                "A formula cannot be marked as having a variable if it is not invertible"
-            );
-        }
-
-        const numVariables = inputs.filter(
-            input => input instanceof Formula && input.hasVariable()
-        ).length;
+        const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options;
+        const numVariables = inputs.reduce<number>(
+            (acc, input) => acc + (input instanceof Formula ? input.internalVariables : 0),
+            0
+        );
         const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as
             | GenericFormula
             | undefined;
 
-        const internalHasVariable =
-            numVariables === 1 || (numVariables === 0 && hasVariable === true);
-        const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined;
-        const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined;
+        const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined;
 
         return {
             inputs,
             internalEvaluate: evaluate,
-            internalInvert,
+            internalInvert: invert,
             internalIntegrate: integrate,
             internalIntegrateInner: integrateInner,
             applySubstitution,
             innermostVariable,
-            internalHasVariable
+            internalVariables: numVariables
         };
     }
 
     private calculateConstantOfIntegration() {
-        // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase
+        // Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
         const integral = this.getIntegralFormula().evaluate(1);
-        const actualCost = this.evaluate(0);
+        const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
         return Decimal.sub(actualCost, integral);
     }
 
     /** Type predicate that this formula can be inverted. */
     isInvertible(): this is InvertibleFormula {
-        return (
-            this.internalHasVariable &&
-            (this.internalInvert != null || this.internalEvaluate == null)
-        );
+        return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null);
     }
 
     /** Type predicate that this formula can be integrated. */
     isIntegrable(): this is IntegrableFormula {
-        return this.internalHasVariable && this.internalIntegrate != null;
+        return this.hasVariable() && this.internalIntegrate != null;
     }
 
     /** Type predicate that this formula has an integral function that can be inverted. */
@@ -173,7 +146,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
 
     /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */
     hasVariable(): boolean {
-        return this.internalHasVariable;
+        return this.internalVariables === 1;
     }
 
     /**
@@ -188,7 +161,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                     unrefFormulaSource(input, variable)
                 ) as GuardedFormulasToDecimals<T>)
             ) ??
-            (this.internalHasVariable ? variable : null) ??
+            (this.hasVariable() ? variable : null) ??
             unrefFormulaSource(this.inputs[0])
         );
     }
@@ -199,9 +172,9 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
      * @see {@link isInvertible}
      */
     invert(value: DecimalSource): DecimalSource {
-        if (this.internalInvert) {
+        if (this.internalInvert && this.hasVariable()) {
             return this.internalInvert.call(this, value, ...this.inputs);
-        } else if (this.inputs.length === 1 && this.internalHasVariable) {
+        } else if (this.inputs.length === 1 && this.hasVariable()) {
             return value;
         }
         throw new Error("Cannot invert non-invertible formula");
@@ -228,7 +201,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
      * @see {@link isIntegralInvertible}
      */
     invertIntegral(value: DecimalSource): DecimalSource {
-        if (this.integralFormula?.isInvertible()) {
+        if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
             throw new Error("Cannot invert integral of formula without invertible integral");
         }
         return this.getIntegralFormula().invert(value);
@@ -236,24 +209,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
 
     /**
      * Get a formula that will evaluate to the integral of this formula. May also be invertible.
-     * @param variable The variable that will be used to evaluate this integral at a given x value
-     * @param stack For nested formulas, a stack of operations that occur outside the complex operation
+     * @param stack For nested formulas, a stack of operations that occur outside the complex operation.
      */
-    getIntegralFormula(
-        variable?: ProcessedComputable<DecimalSource>,
-        stack?: SubstitutionStack
-    ): GenericFormula {
-        if (variable == null && this.integralFormula != null) {
+    getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
+        if (this.integralFormula != null) {
             return this.integralFormula;
         }
-        let formula;
-        const variablePresent = variable != null;
-        if (variable == null) {
-            variable = this.innermostVariable;
-            if (variable == null) {
-                throw new Error("Cannot integrate formula without variable");
-            }
-        }
         if (stack == null) {
             // "Outer" part of the formula
             if (this.applySubstitution == null) {
@@ -262,21 +223,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 if (this.internalIntegrate == null) {
                     throw new Error("Cannot integrate formula with non-integrable operation");
                 }
-                let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
+                let value = this.internalIntegrate.call(this, stack, ...this.inputs);
                 stack.forEach(func => (value = func(value)));
-                formula = value;
+                this.integralFormula = value;
             } else {
                 // Continue digging into the formula
                 if (this.internalIntegrate) {
-                    formula = this.internalIntegrate.call(
+                    this.integralFormula = this.internalIntegrate.call(
                         this,
-                        variable,
                         undefined,
                         ...this.inputs
                     );
-                } else if (this.inputs.length === 1 && this.internalHasVariable) {
+                } else if (
+                    this.inputs.length === 1 &&
+                    this.internalEvaluate == null &&
+                    this.hasVariable()
+                ) {
                     // eslint-disable-next-line @typescript-eslint/no-this-alias
-                    formula = this;
+                    this.integralFormula = this;
                 } else {
                     throw new Error("Cannot integrate formula without variable");
                 }
@@ -291,20 +255,25 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 this.applySubstitution!.call(this, variable, ...this.inputs)
             );
             if (this.internalIntegrateInner) {
-                formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs);
+                this.integralFormula = this.internalIntegrateInner.call(
+                    this,
+                    stack,
+                    ...this.inputs
+                );
             } else if (this.internalIntegrate) {
-                formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs);
-            } else if (this.inputs.length === 1 && this.internalHasVariable) {
+                this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs);
+            } else if (
+                this.inputs.length === 1 &&
+                this.internalEvaluate == null &&
+                this.hasVariable()
+            ) {
                 // eslint-disable-next-line @typescript-eslint/no-this-alias
-                formula = this;
+                this.integralFormula = this;
             } else {
                 throw new Error("Cannot integrate formula without variable");
             }
         }
-        if (!variablePresent) {
-            this.integralFormula = formula;
-        }
-        return formula;
+        return this.integralFormula;
     }
 
     /**
@@ -324,7 +293,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             this.internalEvaluate === other.internalEvaluate &&
             this.internalInvert === other.internalInvert &&
             this.internalIntegrate === other.internalIntegrate &&
-            this.internalHasVariable === other.internalHasVariable
+            this.internalVariables === other.internalVariables
         );
     }
 
@@ -556,17 +525,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     public static reciprocal = Formula.recip;
     public static reciprocate = Formula.recip;
 
-    public static max(value: FormulaSource, other: FormulaSource): GenericFormula {
-        return new Formula({
-            inputs: [value, other],
-            evaluate: Decimal.max,
-            invert: ops.passthrough as (
-                value: DecimalSource,
-                ...inputs: [FormulaSource, FormulaSource]
-            ) => DecimalSource
-        });
-    }
-
+    // TODO these functions should ostensibly be integrable, and the integrals should be invertible
+    public static max = ops.createPassthroughBinaryFormula(Decimal.max);
     public static min = ops.createPassthroughBinaryFormula(Decimal.min);
     public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs);
     public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs);
@@ -1356,6 +1316,24 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
     }
 }
 
+/**
+ * Utility for recursively searching through a formula for the cause of non-invertibility.
+ * @param formula The formula to search for a non-invertible formula within
+ */
+export function findNonInvertible(formula: GenericFormula): GenericFormula | null {
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    if (formula.internalInvert == null && formula.internalEvaluate != null) {
+        return formula;
+    }
+    for (const input of formula.inputs) {
+        if (hasVariable(input)) {
+            return findNonInvertible(input);
+        }
+    }
+    return null;
+}
+
 /**
  * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
  * @param formula The formula to use for calculating buy max from
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index 4512f55..becaec3 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -1,8 +1,9 @@
 import Decimal, { DecimalSource } from "util/bignum";
-import { Ref } from "vue";
 import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
 import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
 
+const ln10 = Decimal.ln(10);
+
 export function passthrough<T extends GenericFormula | DecimalSource>(value: T): T {
     return value;
 }
@@ -14,13 +15,9 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateNeg(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        return Formula.neg(lhs.getIntegralFormula(variable, stack));
+        return Formula.neg(lhs.getIntegralFormula(stack));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -38,33 +35,27 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAdd(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x);
+        const x = lhs.getIntegralFormula(stack);
+        return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
-        return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x);
+        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");
 }
 
 export function integrateInnerAdd(
-    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.add(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        const x = rhs.getIntegralFormula(stack);
         return Formula.add(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -79,33 +70,27 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateSub(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0));
+        const x = lhs.getIntegralFormula(stack);
+        return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0));
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
-        return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x);
+        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");
 }
 
 export function integrateInnerSub(
-    variable: Ref<DecimalSource>,
     stack: SubstitutionStack,
     lhs: FormulaSource,
     rhs: FormulaSource
 ) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.sub(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        const x = rhs.getIntegralFormula(stack);
         return Formula.sub(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -120,17 +105,12 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateMul(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.times(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        const x = rhs.getIntegralFormula(stack);
         return Formula.times(x, lhs);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -158,17 +138,12 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateDiv(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.div(x, rhs);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        const x = rhs.getIntegralFormula(stack);
         return Formula.div(lhs, x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -194,13 +169,9 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateRecip(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.ln(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -213,14 +184,25 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateLog10(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+function internalIntegrateLog10(lhs: DecimalSource) {
+    return Decimal.ln(lhs).sub(1).times(lhs).div(ln10);
+}
+
+function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.ln(x).sub(1).times(x).div(Formula.ln(10));
+        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");
+}
+
+export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return new Formula({
+            inputs: [lhs.getIntegralFormula(stack)],
+            evaluate: internalIntegrateLog10,
+            invert: internalInvertIntegralLog10
+        });
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -234,15 +216,25 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateLog(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) {
+    return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(rhs));
+}
+
+function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.ln(x).sub(1).times(x).div(Formula.ln(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");
+}
+
+export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return new Formula({
+            inputs: [lhs.getIntegralFormula(stack), rhs],
+            evaluate: internalIntegrateLog,
+            invert: internalInvertIntegralLog
+        });
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -254,14 +246,25 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateLog2(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+function internalIntegrateLog2(lhs: DecimalSource) {
+    return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(2));
+}
+
+function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.ln(x).sub(1).times(x).div(Formula.ln(2));
+        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");
+}
+
+export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return new Formula({
+            inputs: [lhs.getIntegralFormula(stack)],
+            evaluate: internalIntegrateLog2,
+            invert: internalInvertIntegralLog2
+        });
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -273,14 +276,24 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateLn(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+function internalIntegrateLn(lhs: DecimalSource) {
+    return Decimal.ln(lhs).sub(1).times(lhs);
+}
+
+function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.ln(x).sub(1).times(x);
+        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");
+}
+
+export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return new Formula({
+            inputs: [lhs.getIntegralFormula(stack)],
+            evaluate: internalIntegrateLn,
+            invert: internalInvertIntegralLn
+        });
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -294,18 +307,13 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integratePow(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         const pow = Formula.add(rhs, 1);
         return Formula.pow(x, pow).div(pow);
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        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");
@@ -318,14 +326,24 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integratePow10(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+function internalIntegratePow10(lhs: DecimalSource) {
+    return Decimal.pow10(lhs).div(Decimal.ln(lhs));
+}
+
+function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
-        return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10));
+        return lhs.invert(ln10.times(value).ln().div(ln10));
+    }
+    throw new Error("Could not invert due to no input being a variable");
+}
+
+export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
+    if (hasVariable(lhs)) {
+        return new Formula({
+            inputs: [lhs.getIntegralFormula(stack)],
+            evaluate: internalIntegratePow10,
+            invert: internalInvertIntegralPow10
+        });
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
@@ -339,17 +357,12 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integratePowBase(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.pow(rhs, x).div(Formula.ln(rhs));
     } else if (hasVariable(rhs)) {
-        const x = rhs.getIntegralFormula(variable, stack);
+        const x = rhs.getIntegralFormula(stack);
         const denominator = Formula.add(lhs, 1);
         return Formula.pow(x, denominator).div(denominator);
     }
@@ -365,14 +378,9 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateRoot(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource,
-    rhs: FormulaSource
-) {
+export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        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");
@@ -385,13 +393,9 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateExp(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.exp(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -520,13 +524,9 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateSin(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.cos(x).neg();
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -539,13 +539,9 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateCos(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.sin(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -558,13 +554,9 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateTan(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.cos(x).ln().neg();
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -577,13 +569,9 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAsin(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.asin(x)
             .times(x)
             .add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
@@ -598,13 +586,9 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAcos(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.acos(x)
             .times(x)
             .sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2))));
@@ -619,13 +603,9 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAtan(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.atan(x)
             .times(x)
             .sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2));
@@ -640,13 +620,9 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateSinh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.cosh(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -659,13 +635,9 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateCosh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.sinh(x);
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -678,13 +650,9 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateTanh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.cosh(x).ln();
     }
     throw new Error("Could not integrate due to no input being a variable");
@@ -697,13 +665,9 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAsinh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        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");
@@ -716,13 +680,9 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAcosh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.acosh(x)
             .times(x)
             .sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt()));
@@ -737,13 +697,9 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-export function integrateAtanh(
-    variable: Ref<DecimalSource>,
-    stack: SubstitutionStack,
-    lhs: FormulaSource
-) {
+export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        const x = lhs.getIntegralFormula(variable, stack);
+        const x = lhs.getIntegralFormula(stack);
         return Formula.atanh(x)
             .times(x)
             .add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2));
diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts
index 3d3bc83..96d7757 100644
--- a/src/game/formulas/types.d.ts
+++ b/src/game/formulas/types.d.ts
@@ -22,7 +22,6 @@ type EvaluateFunction<T> = (
 type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource;
 type IntegrateFunction<T> = (
     this: Formula<T>,
-    variable: Ref<DecimalSource>,
     stack: SubstitutionStack | undefined,
     ...inputs: T
 ) => GenericFormula;
@@ -43,7 +42,6 @@ type GeneralFormulaOptions<T extends [FormulaSource] | FormulaSource[]> = {
     integrate?: IntegrateFunction<T>;
     integrateInner?: IntegrateFunction<T>;
     applySubstitution?: SubstitutionFunction<T>;
-    hasVariable?: boolean;
 };
 type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
     | VariableFormulaOptions
@@ -52,7 +50,7 @@ type FormulaOptions<T extends [FormulaSource] | FormulaSource[]> =
 
 type InternalFormulaProperties<T extends [FormulaSource] | FormulaSource[]> = {
     inputs: T;
-    internalHasVariable: boolean;
+    internalVariables: number;
     internalEvaluate?: EvaluateFunction<T>;
     internalInvert?: InvertFunction<T>;
     internalIntegrate?: IntegrateFunction<T>;
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index ce0120b..ccaa485 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -96,21 +96,21 @@ const invertibleIntegralZeroPramFunctionNames = [
     "sqr",
     "sqrt",
     "cube",
-    "cbrt"
-] as const;
-const nonInvertibleIntegralZeroPramFunctionNames = [
-    ...nonIntegrableZeroParamFunctionNames,
+    "cbrt",
     "neg",
     "exp",
     "sin",
     "cos",
     "tan",
+    "sinh",
+    "cosh",
+    "tanh"
+] as const;
+const nonInvertibleIntegralZeroPramFunctionNames = [
+    ...nonIntegrableZeroParamFunctionNames,
     "asin",
     "acos",
     "atan",
-    "sinh",
-    "cosh",
-    "tanh",
     "asinh",
     "acosh",
     "atanh"
@@ -493,8 +493,8 @@ describe("Integrating", () => {
 
     test("variable.evaluateIntegral() calculates correctly", () =>
         expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2)));
-    test("evaluateIntegral(variable) overrides variable value", () =>
-        expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400));
+    test("variable.evaluateIntegral(variable) overrides variable value", () =>
+        expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
 
     describe("Integrable functions marked as such", () => {
         function checkFormula(formula: GenericFormula) {
@@ -668,32 +668,13 @@ describe("Inverting integrals", () => {
 
     test("Inverting integral of nested formulas", () => {
         const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
-        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
+        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01);
     });
 
     test("Inverting integral of nested complex formulas", () => {
         const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
         expect(() => formula.invertIntegral(100)).toThrow();
     });
-
-    describe("Inverting integral pass-throughs", () => {
-        test("max", () =>
-            expect(Formula.max(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("min", () =>
-            expect(Formula.min(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("minabs", () =>
-            expect(Formula.minabs(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("maxabs", () =>
-            expect(Formula.maxabs(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("clampMax", () =>
-            expect(Formula.clampMax(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("clampMin", () =>
-            expect(Formula.clampMin(variable, constant).invertIntegral(10)).compare_tolerance(10));
-        test("clamp", () =>
-            expect(
-                Formula.clamp(variable, constant, constant).invertIntegral(10)
-            ).compare_tolerance(10));
-    });
 });
 
 describe("Step-wise", () => {
@@ -914,8 +895,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [],
                     evaluate: () => 6,
-                    invert: value => value,
-                    hasVariable: true
+                    invert: value => value
                 }).invert(10)
             ).toThrow());
         test("One input inverts correctly", () =>
@@ -923,8 +903,7 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable],
                     evaluate: () => 10,
-                    invert: (value, v1) => v1.evaluate(),
-                    hasVariable: true
+                    invert: (value, v1) => v1.evaluate()
                 }).invert(10)
             ).compare_tolerance(1));
         test("Two inputs inverts correctly", () =>
@@ -932,37 +911,36 @@ describe("Custom Formulas", () => {
                 new Formula({
                     inputs: [variable, 2],
                     evaluate: () => 10,
-                    invert: (value, v1, v2) => v2,
-                    hasVariable: true
+                    invert: (value, v1, v2) => v2
                 }).invert(10)
             ).compare_tolerance(2));
     });
 
     describe("Formula with integrate", () => {
-        test("Zero input integrates correctly", () =>
-            expect(
+        test("Zero input cannot integrate", () =>
+            expect(() =>
                 new Formula({
                     inputs: [],
-                    evaluate: () => 10,
-                    integrate: variable => variable
+                    evaluate: () => 0,
+                    integrate: stack => variable
                 }).evaluateIntegral()
-            ).compare_tolerance(20));
+            ).toThrow());
         test("One input integrates correctly", () =>
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: () => 10,
-                    integrate: (variable, stack, v1) => Formula.add(variable, v1)
+                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    integrate: (stack, v1) => Formula.add(v1, 10)
                 }).evaluateIntegral()
             ).compare_tolerance(20));
         test("Two inputs integrates correctly", () =>
             expect(
                 new Formula({
-                    inputs: [variable, 2],
-                    evaluate: (v1, v2) => 10,
-                    integrate: (variable, v1, v2) => variable
+                    inputs: [variable, 10],
+                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    integrate: (stack, v1, v2) => Formula.add(v1, v2)
                 }).evaluateIntegral()
-            ).compare_tolerance(3));
+            ).compare_tolerance(20));
     });
 
     describe("Formula with invertIntegral", () => {
@@ -970,29 +948,26 @@ describe("Custom Formulas", () => {
             expect(() =>
                 new Formula({
                     inputs: [],
-                    evaluate: () => 10,
-                    integrate: variable => variable,
-                    hasVariable: true
-                }).invertIntegral(8)
+                    evaluate: () => 0,
+                    integrate: stack => variable
+                }).invertIntegral(20)
             ).toThrow());
         test("One input inverts integral correctly", () =>
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: () => 10,
-                    integrate: (variable, stack, v1) => variable,
-                    hasVariable: true
-                }).invertIntegral(8)
-            ).compare_tolerance(1));
+                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    integrate: (stack, v1) => Formula.add(v1, 10)
+                }).invertIntegral(20)
+            ).compare_tolerance(10));
         test("Two inputs inverts integral correctly", () =>
             expect(
                 new Formula({
-                    inputs: [variable, 2],
-                    evaluate: (v1, v2) => 10,
-                    integrate: (variable, v1, v2) => variable,
-                    hasVariable: true
-                }).invertIntegral(8)
-            ).compare_tolerance(1));
+                    inputs: [variable, 10],
+                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    integrate: (stack, v1, v2) => Formula.add(v1, v2)
+                }).invertIntegral(20)
+            ).compare_tolerance(10));
     });
 
     describe.todo("Formula as input");
diff --git a/tests/utils.ts b/tests/utils.ts
index 0e58e19..a1e6084 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -2,7 +2,7 @@ import Decimal, { DecimalSource, format } from "util/bignum";
 import { expect } from "vitest";
 
 interface CustomMatchers<R = unknown> {
-    compare_tolerance(expected: DecimalSource): R;
+    compare_tolerance(expected: DecimalSource, tolerance?: number): R;
 }
 
 declare global {
@@ -16,7 +16,7 @@ declare global {
 }
 
 expect.extend({
-    compare_tolerance(received: DecimalSource, expected: DecimalSource) {
+    compare_tolerance(received: DecimalSource, expected: DecimalSource, tolerance?: number) {
         const { isNot } = this;
         let pass = false;
         if (!Decimal.isFinite(expected)) {
@@ -24,7 +24,7 @@ expect.extend({
         } else if (Decimal.isNaN(expected)) {
             pass = Decimal.isNaN(received);
         } else {
-            pass = Decimal.eq_tolerance(received, expected);
+            pass = Decimal.eq_tolerance(received, expected, tolerance);
         }
         return {
             // do not alter your "pass" based on isNot. Vitest does it for you

From 6f9b73d0e87b96fc7314c3ddd5e8d96b88fdb606 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 00:48:48 -0500
Subject: [PATCH 16/36] Add printFormula util

---
 src/data/projEntry.tsx        |  4 +++-
 src/game/formulas/formulas.ts | 24 +++++++++++++++++++++++-
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index 55e8500..5c8d456 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -7,7 +7,8 @@ import { globalBus } from "game/events";
 import Formula, {
     calculateCost,
     calculateMaxAffordable,
-    findNonInvertible
+    findNonInvertible,
+    printFormula
 } from "game/formulas/formulas";
 import type { BaseLayer, GenericLayer } from "game/layers";
 import { createLayer } from "game/layers";
@@ -23,6 +24,7 @@ window.Formula = Formula;
 window.calculateMaxAffordable = calculateMaxAffordable;
 window.calculateCost = calculateCost;
 window.findNonInvertible = findNonInvertible;
+window.printFormula = printFormula;
 window.unref = unref;
 window.ref = ref;
 window.createResource = createResource;
diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 8161903..030e604 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -1,5 +1,5 @@
 import { Resource } from "features/resources/resource";
-import Decimal, { DecimalSource } from "util/bignum";
+import Decimal, { DecimalSource, format } from "util/bignum";
 import { Computable, convertComputable, ProcessedComputable } from "util/computed";
 import { computed, ComputedRef, ref, unref } from "vue";
 import * as ops from "./operations";
@@ -1334,6 +1334,28 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
     return null;
 }
 
+/**
+ * Stringifies a formula so it's more easy to read in the console
+ * @param formula The formula to print
+ */
+export function printFormula(formula: FormulaSource): string {
+    if (formula instanceof Formula) {
+        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+        // @ts-ignore
+        return formula.internalEvaluate == null
+            ? formula.hasVariable()
+                ? "x"
+                : formula.inputs[0] ?? 0
+            : // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+              // @ts-ignore
+              formula.internalEvaluate.name +
+                  "(" +
+                  formula.inputs.map(printFormula).join(", ") +
+                  ")";
+    }
+    return format(unref(formula));
+}
+
 /**
  * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
  * @param formula The formula to use for calculating buy max from

From 8dc0c6c55cf24d5cb241103d1ba83d777758978d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 15:02:43 -0500
Subject: [PATCH 17/36] All tests pass now

---
 src/game/formulas/formulas.ts   | 28 ++++++-------
 src/game/formulas/operations.ts | 18 +-------
 tests/game/formulas.test.ts     | 74 ++++++++++++++++++++++++++-------
 3 files changed, 75 insertions(+), 45 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 030e604..09f17b9 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -32,6 +32,10 @@ function integrateVariable(this: GenericFormula) {
     return Formula.pow(this, 2).div(2);
 }
 
+function integrateVariableInner(this: GenericFormula) {
+    return this;
+}
+
 /**
  * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
  * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@@ -81,6 +85,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             internalVariables: 1,
             innermostVariable: variable,
             internalIntegrate: integrateVariable,
+            internalIntegrateInner: integrateVariableInner,
             applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
         };
     }
@@ -119,7 +124,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         };
     }
 
-    private calculateConstantOfIntegration() {
+    /** Calculates C for the implementation of the integral formula for this formula. */
+    calculateConstantOfIntegration() {
         // Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
         const integral = this.getIntegralFormula().evaluate(1);
         const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
@@ -189,10 +195,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         if (!this.isIntegrable()) {
             throw new Error("Cannot evaluate integral of formula without integral");
         }
-        return Decimal.add(
-            this.getIntegralFormula().evaluate(variable),
-            this.calculateConstantOfIntegration()
-        );
+        return this.getIntegralFormula().evaluate(variable);
     }
 
     /**
@@ -212,7 +215,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
      * @param stack For nested formulas, a stack of operations that occur outside the complex operation.
      */
     getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
-        if (this.integralFormula != null) {
+        if (this.integralFormula != null && stack == null) {
             return this.integralFormula;
         }
         if (stack == null) {
@@ -245,6 +248,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                     throw new Error("Cannot integrate formula without variable");
                 }
             }
+            return this.integralFormula;
         } else {
             // "Inner" part of the formula
             if (this.applySubstitution == null) {
@@ -255,25 +259,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 this.applySubstitution!.call(this, variable, ...this.inputs)
             );
             if (this.internalIntegrateInner) {
-                this.integralFormula = this.internalIntegrateInner.call(
-                    this,
-                    stack,
-                    ...this.inputs
-                );
+                return this.internalIntegrateInner.call(this, stack, ...this.inputs);
             } else if (this.internalIntegrate) {
-                this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs);
+                return this.internalIntegrate.call(this, stack, ...this.inputs);
             } else if (
                 this.inputs.length === 1 &&
                 this.internalEvaluate == null &&
                 this.hasVariable()
             ) {
-                // eslint-disable-next-line @typescript-eslint/no-this-alias
-                this.integralFormula = this;
+                return this;
             } else {
                 throw new Error("Cannot integrate formula without variable");
             }
         }
-        return this.integralFormula;
     }
 
     /**
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index becaec3..c6cbd05 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -326,24 +326,10 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-function internalIntegratePow10(lhs: DecimalSource) {
-    return Decimal.pow10(lhs).div(Decimal.ln(lhs));
-}
-
-function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(ln10.times(value).ln().div(ln10));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        return new Formula({
-            inputs: [lhs.getIntegralFormula(stack)],
-            evaluate: internalIntegratePow10,
-            invert: internalInvertIntegralPow10
-        });
+        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");
 }
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index ccaa485..9c77d86 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -2,10 +2,11 @@ import { createResource, Resource } from "features/resources/resource";
 import Formula, {
     calculateCost,
     calculateMaxAffordable,
+    printFormula,
     unrefFormulaSource
 } from "game/formulas/formulas";
 import type { GenericFormula, InvertibleFormula } from "game/formulas/types";
-import Decimal, { DecimalSource } from "util/bignum";
+import Decimal, { DecimalSource, format } from "util/bignum";
 import { beforeAll, describe, expect, test } from "vitest";
 import { ref } from "vue";
 import "../utils";
@@ -572,7 +573,13 @@ describe("Integrating", () => {
         // Check if the calculated cost is within 10% of the actual cost,
         // because this is an approximation
         expect(
-            Decimal.sub(actualCost, formula.evaluateIntegral()).abs().div(actualCost).toNumber()
+            Decimal.sub(
+                actualCost,
+                Decimal.add(formula.evaluateIntegral(), formula.calculateConstantOfIntegration())
+            )
+                .abs()
+                .div(actualCost)
+                .toNumber()
         ).toBeLessThan(0.1);
     });
 
@@ -668,7 +675,7 @@ describe("Inverting integrals", () => {
 
     test("Inverting integral of nested formulas", () => {
         const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
-        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01);
+        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
     });
 
     test("Inverting integral of nested complex formulas", () => {
@@ -929,18 +936,18 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: v1 => Decimal.add(v1, 10),
                     integrate: (stack, v1) => Formula.add(v1, 10)
                 }).evaluateIntegral()
-            ).compare_tolerance(20));
+            ).compare_tolerance(11));
         test("Two inputs integrates correctly", () =>
             expect(
                 new Formula({
                     inputs: [variable, 10],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: (v1, v2) => Decimal.add(v1, v2),
                     integrate: (stack, v1, v2) => Formula.add(v1, v2)
                 }).evaluateIntegral()
-            ).compare_tolerance(20));
+            ).compare_tolerance(11));
     });
 
     describe("Formula with invertIntegral", () => {
@@ -956,7 +963,7 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: v1 => Decimal.add(v1, 10),
                     integrate: (stack, v1) => Formula.add(v1, 10)
                 }).invertIntegral(20)
             ).compare_tolerance(10));
@@ -964,7 +971,7 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable, 10],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: (v1, v2) => Decimal.add(v1, v2),
                     integrate: (stack, v1, v2) => Formula.add(v1, v2)
                 }).invertIntegral(20)
             ).compare_tolerance(10));
@@ -1001,14 +1008,25 @@ describe("Buy Max", () => {
             const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
             expect(() => maxAffordable.value).toThrow();
         });
-        // https://www.desmos.com/calculator/7ffthe7wi8
-        test("Calculates max affordable and cost correctly", () => {
-            const variable = Formula.variable(0);
+        test("Calculates max affordable and cost correctly with 0 purchases", () => {
+            const purchases = ref(0);
+            const variable = Formula.variable(purchases);
             const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource);
-            expect(maxAffordable.value).compare_tolerance(7);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(actualAffordable);
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
 
-            const actualCost = new Array(7)
+            const actualCost = new Array(actualAffordable)
                 .fill(null)
                 .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
             const calculatedCost = calculateCost(formula, maxAffordable.value);
@@ -1018,5 +1036,33 @@ describe("Buy Max", () => {
                 Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
             ).toBeLessThan(0.1);
         });
+        test("Calculates max affordable and cost correctly with 1 purchase", () => {
+            const purchases = ref(1);
+            const variable = Formula.variable(purchases);
+            const formula = Formula.pow(1.05, variable).times(100);
+            const maxAffordable = calculateMaxAffordable(formula, resource);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(Decimal.add(actualAffordable, 1));
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
+
+            const actualCost = new Array(actualAffordable)
+                .fill(null)
+                .reduce((acc, _, i) => acc.add(formula.evaluate(i + 1)), new Decimal(0));
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            // Check if the calculated cost is within 10% of the actual cost,
+            // because this is an approximation
+            expect(
+                Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+            ).toBeLessThan(0.1);
+        });
     });
 });

From 0c1410a1c12e3e029c99863e5b37b4121ed36558 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 15:04:31 -0500
Subject: [PATCH 18/36] Remove a handful of long-running tests

---
 tests/game/formulas.test.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 9c77d86..95370f6 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -263,6 +263,11 @@ describe("Creating Formulas", () => {
         functionName: 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
+            // We still have plenty of coverage
+            return;
+        }
         let testName = functionName + "(";
         for (let i = 0; i < args.length; i++) {
             if (i !== 0) {

From fb360c72c5836f90b341128c2c3a19a630b23309 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 15:16:47 -0500
Subject: [PATCH 19/36] Add some more tests

---
 tests/game/formulas.test.ts | 40 +++++++++++++++++++++++++++++++++----
 1 file changed, 36 insertions(+), 4 deletions(-)

diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index 95370f6..fbaa378 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -571,7 +571,7 @@ describe("Integrating", () => {
     describe.todo("Integrable formulas integrate correctly");
 
     test("Integrating nested formulas", () => {
-        const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
+        const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10);
         const actualCost = new Array(10)
             .fill(null)
             .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
@@ -588,6 +588,24 @@ describe("Integrating", () => {
         ).toBeLessThan(0.1);
     });
 
+    test("Integrating nested formulas with overidden variable", () => {
+        const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10);
+        const actualCost = new Array(20)
+            .fill(null)
+            .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
+        // Check if the calculated cost is within 10% of the actual cost,
+        // because this is an approximation
+        expect(
+            Decimal.sub(
+                actualCost,
+                Decimal.add(formula.evaluateIntegral(20), formula.calculateConstantOfIntegration())
+            )
+                .abs()
+                .div(actualCost)
+                .toNumber()
+        ).toBeLessThan(0.1);
+    });
+
     test("Integrating nested complex formulas", () => {
         const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
         expect(() => formula.evaluateIntegral()).toThrow();
@@ -982,9 +1000,23 @@ describe("Custom Formulas", () => {
             ).compare_tolerance(10));
     });
 
-    describe.todo("Formula as input");
-
-    describe.todo("Determines invertibility etc. correctly");
+    describe("Formula as input", () => {
+        let customFormula: GenericFormula;
+        beforeAll(() => {
+            customFormula = new Formula({
+                inputs: [variable],
+                evaluate: v1 => v1,
+                invert: value => value,
+                integrate: (stack, v1) => v1.getIntegralFormula(stack)
+            });
+        });
+        test("Evaluate correctly", () =>
+            expect(customFormula.add(10).evaluate()).compare_tolerance(11));
+        test("Invert correctly", () =>
+            expect(customFormula.add(10).invert(20)).compare_tolerance(10));
+        test("Integrate correctly", () =>
+            expect(customFormula.add(10).evaluateIntegral(10)).compare_tolerance(20));
+    });
 });
 
 describe("Buy Max", () => {

From 7e7a36bb7821a939234a042ba0c9bb1957747dde Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 16:41:39 -0500
Subject: [PATCH 20/36] Add summedPurchases param for buy max utilities

---
 src/game/formulas/formulas.ts | 79 +++++++++++++++++++++++++++++-----
 tests/game/formulas.test.ts   | 81 +++++++++++++++++++++++++++++++----
 2 files changed, 141 insertions(+), 19 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 09f17b9..12ea545 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -1359,39 +1359,66 @@ export function printFormula(formula: FormulaSource): string {
  * @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
  */
 export function calculateMaxAffordable(
     formula: InvertibleFormula,
     resource: Resource,
-    spendResources?: true
+    spendResources?: true,
+    summedPurchases?: number
 ): ComputedRef<DecimalSource>;
 export function calculateMaxAffordable(
     formula: InvertibleIntegralFormula,
     resource: Resource,
-    spendResources: Computable<boolean>
+    spendResources: Computable<boolean>,
+    summedPurchases?: number
 ): ComputedRef<DecimalSource>;
 export function calculateMaxAffordable(
     formula: InvertibleFormula,
     resource: Resource,
-    spendResources: Computable<boolean> = true
+    spendResources: Computable<boolean> = true,
+    summedPurchases?: number
 ) {
     const computedSpendResources = convertComputable(spendResources);
     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"
                 );
             }
-            return Decimal.floor(
+            affordable = Decimal.floor(
                 formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
             ).sub(unref(formula.innermostVariable) ?? 0);
+            if (summedPurchases == null) {
+                summedPurchases = 10;
+            }
         } else {
             if (!formula.isInvertible()) {
                 throw new Error("Cannot calculate max affordable of non-invertible formula");
             }
-            return Decimal.floor(formula.invert(resource.value));
+            affordable = Decimal.floor(formula.invert(resource.value));
+            if (summedPurchases == null) {
+                summedPurchases = 0;
+            }
         }
+        if (summedPurchases > 0) {
+            affordable = affordable.sub(summedPurchases).clampMin(0);
+            let summedCost = calculateCost(formula, affordable, true, 0);
+            while (true) {
+                const nextCost = formula.evaluate(
+                    affordable.add(unref(formula.innermostVariable) ?? 0)
+                );
+                if (Decimal.add(summedCost, nextCost).lt(resource.value)) {
+                    affordable = affordable.add(1);
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+        }
+        return affordable;
     });
 }
 
@@ -1400,26 +1427,56 @@ export function calculateMaxAffordable(
  * @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
  */
 export function calculateCost(
     formula: InvertibleFormula,
     amountToBuy: DecimalSource,
-    spendResources?: true
+    spendResources?: true,
+    summedPurchases?: number
 ): DecimalSource;
 export function calculateCost(
     formula: InvertibleIntegralFormula,
     amountToBuy: DecimalSource,
-    spendResources: boolean
+    spendResources: boolean,
+    summedPurchases?: number
 ): DecimalSource;
 export function calculateCost(
     formula: InvertibleFormula,
     amountToBuy: DecimalSource,
-    spendResources = true
+    spendResources = true,
+    summedPurchases?: number
 ) {
-    const newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
+    let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
     if (spendResources) {
-        return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
+        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 {
-        return formula.evaluate(newValue);
+        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;
     }
 }
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index fbaa378..ed5adb2 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -1022,21 +1022,20 @@ describe("Custom Formulas", () => {
 describe("Buy Max", () => {
     let resource: Resource;
     beforeAll(() => {
-        resource = createResource(ref(1000));
+        resource = createResource(ref(100000));
     });
     describe("Without spending", () => {
         test("Throws on formula with non-invertible integral", () => {
             const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
             expect(() => maxAffordable.value).toThrow();
         });
-        // https://www.desmos.com/calculator/7ffthe7wi8
         test("Calculates max affordable and cost correctly", () => {
             const variable = Formula.variable(0);
             const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource, false);
-            expect(maxAffordable.value).compare_tolerance(47);
+            expect(maxAffordable.value).compare_tolerance(141);
             expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance(
-                Decimal.pow(1.05, 47).times(100)
+                Decimal.pow(1.05, 141).times(100)
             );
         });
     });
@@ -1045,11 +1044,11 @@ describe("Buy Max", () => {
             const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
             expect(() => maxAffordable.value).toThrow();
         });
-        test("Calculates max affordable and cost correctly with 0 purchases", () => {
+        test("Estimates max affordable and cost correctly with 0 purchases", () => {
             const purchases = ref(0);
             const variable = Formula.variable(purchases);
             const formula = Formula.pow(1.05, variable).times(100);
-            const maxAffordable = calculateMaxAffordable(formula, resource);
+            const maxAffordable = calculateMaxAffordable(formula, resource, true, 0);
             let actualAffordable = 0;
             let summedCost = Decimal.dZero;
             while (true) {
@@ -1073,11 +1072,11 @@ describe("Buy Max", () => {
                 Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
             ).toBeLessThan(0.1);
         });
-        test("Calculates max affordable and cost correctly with 1 purchase", () => {
+        test("Estimates max affordable and cost with 1 purchase", () => {
             const purchases = ref(1);
             const variable = Formula.variable(purchases);
             const formula = Formula.pow(1.05, variable).times(100);
-            const maxAffordable = calculateMaxAffordable(formula, resource);
+            const maxAffordable = calculateMaxAffordable(formula, resource, true, 0);
             let actualAffordable = 0;
             let summedCost = Decimal.dZero;
             while (true) {
@@ -1101,5 +1100,71 @@ 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", () => {
+            const purchases = ref(1);
+            const variable = Formula.variable(purchases);
+            const formula = Formula.pow(1.05, variable).times(100);
+            const maxAffordable = calculateMaxAffordable(formula, resource);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(Decimal.add(actualAffordable, 1));
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
+
+            const actualCost = new Array(actualAffordable)
+                .fill(null)
+                .reduce((acc, _, i) => acc.add(formula.evaluate(i + 1)), new Decimal(0));
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            // Since we're summing the last few purchases, this has a tighter deviation allowed
+            expect(
+                Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+            ).toBeLessThan(0.02);
+        });
+        test("Handles summing purchases when making few purchases", () => {
+            const purchases = ref(90);
+            const variable = Formula.variable(purchases);
+            const formula = Formula.pow(1.05, variable).times(100);
+            const maxAffordable = calculateMaxAffordable(formula, resource);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(Decimal.add(actualAffordable, purchases.value));
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
+
+            const actualCost = new Array(actualAffordable)
+                .fill(null)
+                .reduce(
+                    (acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
+                    new Decimal(0)
+                );
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            // Since we're summing all the purchases this should be equivalent
+            expect(calculatedCost).compare_tolerance(actualCost);
+        });
+        test("Handles summing purchases when over e308 purchases", () => {
+            resource.value = "1ee308";
+            const purchases = ref(0);
+            const variable = Formula.variable(purchases);
+            const formula = variable;
+            const maxAffordable = calculateMaxAffordable(formula, resource);
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            expect(Decimal.isNaN(calculatedCost)).toBe(false);
+            expect(Decimal.isFinite(calculatedCost)).toBe(true);
+            resource.value = 100000;
+        });
     });
 });

From eff5852b04a4d9051e665e07a374d711dba0f451 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 16:42:03 -0500
Subject: [PATCH 21/36] Remove some debugging code

---
 src/data/projEntry.tsx | 17 +----------------
 1 file changed, 1 insertion(+), 16 deletions(-)

diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx
index 5c8d456..e4640b6 100644
--- a/src/data/projEntry.tsx
+++ b/src/data/projEntry.tsx
@@ -4,12 +4,6 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso
 import type { GenericTree } from "features/trees/tree";
 import { branchedResetPropagation, createTree } from "features/trees/tree";
 import { globalBus } from "game/events";
-import Formula, {
-    calculateCost,
-    calculateMaxAffordable,
-    findNonInvertible,
-    printFormula
-} from "game/formulas/formulas";
 import type { BaseLayer, GenericLayer } from "game/layers";
 import { createLayer } from "game/layers";
 import type { Player } from "game/player";
@@ -17,18 +11,9 @@ import player from "game/player";
 import type { DecimalSource } from "util/bignum";
 import Decimal, { format, formatTime } from "util/bignum";
 import { render } from "util/vue";
-import { computed, ref, toRaw, unref } from "vue";
+import { computed, toRaw } from "vue";
 import prestige from "./layers/prestige";
 
-window.Formula = Formula;
-window.calculateMaxAffordable = calculateMaxAffordable;
-window.calculateCost = calculateCost;
-window.findNonInvertible = findNonInvertible;
-window.printFormula = printFormula;
-window.unref = unref;
-window.ref = ref;
-window.createResource = createResource;
-
 /**
  * @hidden
  */

From cc1a2998e073df08d6e18330c3b0a0708ff2c3d4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 19:22:23 -0500
Subject: [PATCH 22/36] Make challenges use the requirements system

---
 src/features/challenges/challenge.tsx | 67 ++++++++-------------------
 src/features/repeatable.tsx           | 21 +++++----
 src/game/requirements.tsx             |  2 +-
 3 files changed, 32 insertions(+), 58 deletions(-)

diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index 92dce42..34f850c 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -12,10 +12,10 @@ import {
     Visibility
 } from "features/feature";
 import type { GenericReset } from "features/reset";
-import type { Resource } from "features/resources/resource";
 import { globalBus } from "game/events";
 import type { Persistent } from "game/persistence";
 import { persistent } from "game/persistence";
+import { maxRequirementsMet, Requirements } from "game/requirements";
 import settings, { registerSettingField } from "game/settings";
 import type { DecimalSource } from "util/bignum";
 import Decimal from "util/bignum";
@@ -36,11 +36,10 @@ export interface ChallengeOptions {
     visibility?: Computable<Visibility | boolean>;
     canStart?: Computable<boolean>;
     reset?: GenericReset;
-    canComplete?: Computable<boolean | DecimalSource>;
+    requirements: Requirements;
+    maximize?: Computable<boolean>;
     completionLimit?: Computable<DecimalSource>;
     mark?: Computable<boolean | string>;
-    resource?: Resource;
-    goal?: Computable<DecimalSource>;
     classes?: Computable<Record<string, boolean>>;
     style?: Computable<StyleValue>;
     display?: Computable<
@@ -60,6 +59,7 @@ export interface ChallengeOptions {
 
 export interface BaseChallenge {
     id: string;
+    canComplete: Ref<DecimalSource>;
     completions: Persistent<DecimalSource>;
     completed: Ref<boolean>;
     maxed: Ref<boolean>;
@@ -76,10 +76,10 @@ export type Challenge<T extends ChallengeOptions> = Replace<
     {
         visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
         canStart: GetComputableTypeWithDefault<T["canStart"], true>;
-        canComplete: GetComputableTypeWithDefault<T["canComplete"], Ref<boolean>>;
+        requirements: GetComputableType<T["requirements"]>;
+        maximize: GetComputableType<T["maximize"]>;
         completionLimit: GetComputableTypeWithDefault<T["completionLimit"], 1>;
         mark: GetComputableTypeWithDefault<T["mark"], Ref<boolean>>;
-        goal: GetComputableType<T["goal"]>;
         classes: GetComputableType<T["classes"]>;
         style: GetComputableType<T["style"]>;
         display: GetComputableType<T["display"]>;
@@ -91,7 +91,6 @@ export type GenericChallenge = Replace<
     {
         visibility: ProcessedComputable<Visibility | boolean>;
         canStart: ProcessedComputable<boolean>;
-        canComplete: ProcessedComputable<boolean | DecimalSource>;
         completionLimit: ProcessedComputable<DecimalSource>;
         mark: ProcessedComputable<boolean>;
     }
@@ -105,19 +104,6 @@ export function createChallenge<T extends ChallengeOptions>(
     return createLazyProxy(() => {
         const challenge = optionsFunc();
 
-        if (
-            challenge.canComplete == null &&
-            (challenge.resource == null || challenge.goal == null)
-        ) {
-            console.warn(
-                "Cannot create challenge without a canComplete property or a resource and goal property",
-                challenge
-            );
-            throw new Error(
-                "Cannot create challenge without a canComplete property or a resource and goal property"
-            );
-        }
-
         challenge.id = getUniqueID("challenge-");
         challenge.type = ChallengeType;
         challenge[Component] = ChallengeComponent;
@@ -137,13 +123,10 @@ export function createChallenge<T extends ChallengeOptions>(
             const genericChallenge = challenge as GenericChallenge;
             if (genericChallenge.active.value) {
                 if (
-                    unref(genericChallenge.canComplete) !== false &&
+                    Decimal.gt(unref(genericChallenge.canComplete), 0) &&
                     !genericChallenge.maxed.value
                 ) {
-                    let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
-                    if (typeof completions === "boolean") {
-                        completions = 1;
-                    }
+                    const completions = unref(genericChallenge.canComplete);
                     genericChallenge.completions.value = Decimal.min(
                         Decimal.add(genericChallenge.completions.value, completions),
                         unref(genericChallenge.completionLimit)
@@ -163,18 +146,20 @@ export function createChallenge<T extends ChallengeOptions>(
                 genericChallenge.onEnter?.();
             }
         };
+        challenge.canComplete = computed(() =>
+            Decimal.max(
+                maxRequirementsMet((challenge as GenericChallenge).requirements),
+                unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1
+            )
+        );
         challenge.complete = function (remainInChallenge?: boolean) {
             const genericChallenge = challenge as GenericChallenge;
-            let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
+            const completions = unref(genericChallenge.canComplete);
             if (
                 genericChallenge.active.value &&
-                completions !== false &&
-                (completions === true || Decimal.neq(0, completions)) &&
+                Decimal.gt(completions, 0) &&
                 !genericChallenge.maxed.value
             ) {
-                if (typeof completions === "boolean") {
-                    completions = 1;
-                }
                 genericChallenge.completions.value = Decimal.min(
                     Decimal.add(genericChallenge.completions.value, completions),
                     unref(genericChallenge.completionLimit)
@@ -196,19 +181,6 @@ export function createChallenge<T extends ChallengeOptions>(
             }
             return unref(visibility);
         });
-        if (challenge.canComplete == null) {
-            challenge.canComplete = computed(() => {
-                const genericChallenge = challenge as GenericChallenge;
-                if (
-                    !genericChallenge.active.value ||
-                    genericChallenge.resource == null ||
-                    genericChallenge.goal == null
-                ) {
-                    return false;
-                }
-                return Decimal.gte(genericChallenge.resource.value, unref(genericChallenge.goal));
-            });
-        }
         if (challenge.mark == null) {
             challenge.mark = computed(
                 () =>
@@ -219,11 +191,10 @@ export function createChallenge<T extends ChallengeOptions>(
 
         processComputable(challenge as T, "canStart");
         setDefault(challenge, "canStart", true);
-        processComputable(challenge as T, "canComplete");
+        processComputable(challenge as T, "maximize");
         processComputable(challenge as T, "completionLimit");
         setDefault(challenge, "completionLimit", 1);
         processComputable(challenge as T, "mark");
-        processComputable(challenge as T, "goal");
         processComputable(challenge as T, "classes");
         processComputable(challenge as T, "style");
         processComputable(challenge as T, "display");
@@ -278,9 +249,9 @@ export function setupAutoComplete(
 ): WatchStopHandle {
     const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
     return watch(
-        [challenge.canComplete as Ref<boolean>, isActive as Ref<boolean>],
+        [challenge.canComplete as Ref<DecimalSource>, isActive as Ref<boolean>],
         ([canComplete, isActive]) => {
-            if (canComplete && isActive) {
+            if (Decimal.gt(canComplete, 0) && isActive) {
                 challenge.complete(!exitOnComplete);
             }
         }
diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx
index 96bea11..b442068 100644
--- a/src/features/repeatable.tsx
+++ b/src/features/repeatable.tsx
@@ -78,6 +78,11 @@ export interface BaseRepeatable {
     maxed: Ref<boolean>;
     /** Whether or not this repeatable can be clicked. */
     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. */
     onClick: (event?: MouseEvent | TouchEvent) => void;
     /** A symbol that helps identify features of the same type. */
@@ -170,6 +175,11 @@ export function createRepeatable<T extends RepeatableOptions>(
             }
             return currClasses;
         });
+        repeatable.amountToIncrease = computed(() =>
+            unref((repeatable as GenericRepeatable).maximize)
+                ? maxRequirementsMet(repeatable.requirements)
+                : 1
+        );
         repeatable.canClick = computed(() => requirementsMet(repeatable.requirements));
         const onClick = repeatable.onClick;
         repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) {
@@ -177,12 +187,7 @@ export function createRepeatable<T extends RepeatableOptions>(
             if (!unref(genericRepeatable.canClick)) {
                 return;
             }
-            payRequirements(
-                repeatable.requirements,
-                unref(genericRepeatable.maximize)
-                    ? maxRequirementsMet(genericRepeatable.requirements)
-                    : 1
-            );
+            payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease));
             genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1);
             onClick?.(event);
         };
@@ -233,9 +238,7 @@ export function createRepeatable<T extends RepeatableOptions>(
                                 <br />
                                 {displayRequirements(
                                     genericRepeatable.requirements,
-                                    unref(genericRepeatable.maximize)
-                                        ? maxRequirementsMet(genericRepeatable.requirements)
-                                        : 1
+                                    unref(repeatable.amountToIncrease)
                                 )}
                             </div>
                         )}
diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 8cd1cf7..9e775de 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -239,7 +239,7 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource {
     }
     const reqsMet = unref(requirements.requirementMet);
     if (typeof reqsMet === "boolean") {
-        return reqsMet ? Infinity : 0;
+        return reqsMet ? Decimal.dInf : 0;
     } else if (Decimal.gt(reqsMet, 1) && unref(requirements.canMaximize) !== true) {
         return 1;
     }

From 3e0aafcf02350becbf1d717525af93db5d041f3d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 20:30:08 -0500
Subject: [PATCH 23/36] Make conversions use formulas

---
 src/features/conversion.ts    | 296 ++--------------------------------
 src/game/formulas/formulas.ts |   1 +
 2 files changed, 15 insertions(+), 282 deletions(-)

diff --git a/src/features/conversion.ts b/src/features/conversion.ts
index 97637fa..f5146ca 100644
--- a/src/features/conversion.ts
+++ b/src/features/conversion.ts
@@ -1,11 +1,10 @@
 import type { OptionsFunc, Replace } from "features/feature";
 import { setDefault } from "features/feature";
 import type { Resource } from "features/resources/resource";
+import { InvertibleFormula } from "game/formulas/types";
 import type { BaseLayer } from "game/layers";
-import type { Modifier } from "game/modifiers";
 import type { DecimalSource } from "util/bignum";
 import Decimal from "util/bignum";
-import type { WithRequired } from "util/common";
 import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed";
 import { convertComputable, processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
@@ -15,9 +14,10 @@ import { computed, unref } from "vue";
 /** An object that configures a {@link Conversion}. */
 export interface ConversionOptions {
     /**
-     * The scaling function that is used to determine the rate of conversion from one {@link features/resources/resource.Resource} to the other.
+     * The formula used to determine how much {@link gainResource} should be earned by this converting.
+     * When evaluating, the variable will always be overidden to the amount of {@link baseResource}.
      */
-    scaling: ScalingFunction;
+    formula: InvertibleFormula;
     /**
      * How much of the output resource the conversion can currently convert for.
      * Typically this will be set for you in a conversion constructor.
@@ -53,10 +53,6 @@ export interface ConversionOptions {
      * Defaults to true.
      */
     buyMax?: Computable<boolean>;
-    /**
-     * Whether or not to round up the cost to generate a given amount of the output resource.
-     */
-    roundUpCost?: Computable<boolean>;
     /**
      * The function that performs the actual conversion from {@link baseResource} to {@link gainResource}.
      * Typically this will be set for you in a conversion constructor.
@@ -73,20 +69,6 @@ export interface ConversionOptions {
      * This will not be called whenever using currentGain without calling convert (e.g. passive generation)
      */
     onConvert?: (amountGained: DecimalSource) => void;
-    /**
-     * An additional modifier that will be applied to the gain amounts.
-     * Must be reversible in order to correctly calculate {@link nextAt}.
-     * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
-     */
-    gainModifier?: WithRequired<Modifier, "revert">;
-    /**
-     * A modifier that will be applied to the cost amounts.
-     * That is to say, this modifier will be applied to the amount of baseResource before going into the scaling function.
-     * A cost modifier of x0.5 would give gain amounts equal to the player having half the baseResource they actually have.
-     * Must be reversible in order to correctly calculate {@link nextAt}.
-     * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers.
-     */
-    costModifier?: WithRequired<Modifier, "revert">;
 }
 
 /**
@@ -109,7 +91,6 @@ export type Conversion<T extends ConversionOptions> = Replace<
         nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
         buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
         spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"];
-        roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
     }
 >;
 
@@ -123,7 +104,6 @@ export type GenericConversion = Replace<
         nextAt: ProcessedComputable<DecimalSource>;
         buyMax: ProcessedComputable<boolean>;
         spend: (amountGained: DecimalSource) => void;
-        roundUpCost: ProcessedComputable<boolean>;
     }
 >;
 
@@ -142,11 +122,7 @@ export function createConversion<T extends ConversionOptions>(
 
         if (conversion.currentGain == null) {
             conversion.currentGain = computed(() => {
-                let gain = conversion.gainModifier
-                    ? conversion.gainModifier.apply(
-                          conversion.scaling.currentGain(conversion as GenericConversion)
-                      )
-                    : conversion.scaling.currentGain(conversion as GenericConversion);
+                let gain = conversion.formula.evaluate(conversion.baseResource.value);
                 gain = Decimal.floor(gain).max(0);
 
                 if (unref(conversion.buyMax) === false) {
@@ -160,17 +136,16 @@ export function createConversion<T extends ConversionOptions>(
         }
         if (conversion.currentAt == null) {
             conversion.currentAt = computed(() => {
-                let current = conversion.scaling.currentAt(conversion as GenericConversion);
-                if (unref((conversion as GenericConversion).roundUpCost))
-                    current = Decimal.ceil(current);
-                return current;
+                return conversion.formula.invert(
+                    Decimal.floor(unref((conversion as GenericConversion).currentGain))
+                );
             });
         }
         if (conversion.nextAt == null) {
             conversion.nextAt = computed(() => {
-                let next = conversion.scaling.nextAt(conversion as GenericConversion);
-                if (unref((conversion as GenericConversion).roundUpCost)) next = Decimal.ceil(next);
-                return next;
+                return conversion.formula.invert(
+                    Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1)
+                );
             });
         }
 
@@ -198,177 +173,11 @@ export function createConversion<T extends ConversionOptions>(
         processComputable(conversion as T, "nextAt");
         processComputable(conversion as T, "buyMax");
         setDefault(conversion, "buyMax", true);
-        processComputable(conversion as T, "roundUpCost");
-        setDefault(conversion, "roundUpCost", true);
 
         return conversion as unknown as Conversion<T>;
     });
 }
 
-/**
- * A collection of functions that allow a conversion to scale the amount of resources gained based on the input resource.
- * This typically shouldn't be created directly. Instead use one of the scaling function constructors.
- * @see {@link createLinearScaling}.
- * @see {@link createPolynomialScaling}.
- */
-export interface ScalingFunction {
-    /**
-     * Calculates the amount of the output resource a conversion should be able to currently produce.
-     * This should be based off of `conversion.baseResource.value`.
-     * The conversion is responsible for applying the gainModifier, so this function should be un-modified.
-     * It does not need to be clamped or rounded.
-     */
-    currentGain: (conversion: GenericConversion) => DecimalSource;
-    /**
-     * Calculates the amount of the input resource that is required for the current value of `conversion.currentGain`.
-     * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
-     * The conversion is responsible for rounding up the amount as appropriate.
-     * The returned value should not be below 0.
-     */
-    currentAt: (conversion: GenericConversion) => DecimalSource;
-    /**
-     * Calculates the amount of the input resource that would be required for the current value of `conversion.currentGain` to increase.
-     * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate.
-     * The conversion is responsible for rounding up the amount as appropriate.
-     * The returned value should not be below 0.
-     */
-    nextAt: (conversion: GenericConversion) => DecimalSource;
-}
-
-/**
- * Creates a scaling function based off the formula `(baseResource - base) * coefficient`.
- * If the baseResource value is less than base then the currentGain will be 0.
- * @param base The base variable in the scaling formula.
- * @param coefficient The coefficient variable in the scaling formula.
- * @example
- * A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values:
- * | Base Resource | Current Gain |
- * | ------------- | ------------ |
- * | 10            | 1            |
- * | 12            | 2            |
- * | 20            | 6            |
- */
-export function createLinearScaling(
-    base: Computable<DecimalSource>,
-    coefficient: Computable<DecimalSource>
-): ScalingFunction {
-    const processedBase = convertComputable(base);
-    const processedCoefficient = convertComputable(coefficient);
-    return {
-        currentGain(conversion) {
-            let baseAmount: DecimalSource = unref(conversion.baseResource.value);
-            if (conversion.costModifier) {
-                baseAmount = conversion.costModifier.apply(baseAmount);
-            }
-            if (Decimal.lt(baseAmount, unref(processedBase))) {
-                return 0;
-            }
-
-            return Decimal.sub(baseAmount, unref(processedBase))
-                .sub(1)
-                .times(unref(processedCoefficient))
-                .add(1);
-        },
-        currentAt(conversion) {
-            let current: DecimalSource = unref(conversion.currentGain);
-            if (conversion.gainModifier) {
-                current = conversion.gainModifier.revert(current);
-            }
-            current = Decimal.max(0, current)
-                .sub(1)
-                .div(unref(processedCoefficient))
-                .add(unref(processedBase));
-            if (conversion.costModifier) {
-                current = conversion.costModifier.revert(current);
-            }
-            return current;
-        },
-        nextAt(conversion) {
-            let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor();
-            if (conversion.gainModifier) {
-                next = conversion.gainModifier.revert(next);
-            }
-            next = Decimal.max(0, next)
-                .sub(1)
-                .div(unref(processedCoefficient))
-                .add(unref(processedBase))
-                .max(unref(processedBase));
-            if (conversion.costModifier) {
-                next = conversion.costModifier.revert(next);
-            }
-            return next;
-        }
-    };
-}
-
-/**
- * Creates a scaling function based off the formula `(baseResource / base) ^ exponent`.
- * If the baseResource value is less than base then the currentGain will be 0.
- * @param base The base variable in the scaling formula.
- * @param exponent The exponent variable in the scaling formula.
- * @example
- * A scaling function created via `createPolynomialScaling(10, 0.5)` would produce the following values:
- * | Base Resource | Current Gain |
- * | ------------- | ------------ |
- * | 10            | 1            |
- * | 40            | 2            |
- * | 250           | 5            |
- */
-export function createPolynomialScaling(
-    base: Computable<DecimalSource>,
-    exponent: Computable<DecimalSource>
-): ScalingFunction {
-    const processedBase = convertComputable(base);
-    const processedExponent = convertComputable(exponent);
-    return {
-        currentGain(conversion) {
-            let baseAmount: DecimalSource = unref(conversion.baseResource.value);
-            if (conversion.costModifier) {
-                baseAmount = conversion.costModifier.apply(baseAmount);
-            }
-            if (Decimal.lt(baseAmount, unref(processedBase))) {
-                return 0;
-            }
-
-            const gain = Decimal.div(baseAmount, unref(processedBase)).pow(
-                unref(processedExponent)
-            );
-
-            if (gain.isNan()) {
-                return new Decimal(0);
-            }
-            return gain;
-        },
-        currentAt(conversion) {
-            let current: DecimalSource = unref(conversion.currentGain);
-            if (conversion.gainModifier) {
-                current = conversion.gainModifier.revert(current);
-            }
-            current = Decimal.max(0, current)
-                .root(unref(processedExponent))
-                .times(unref(processedBase));
-            if (conversion.costModifier) {
-                current = conversion.costModifier.revert(current);
-            }
-            return current;
-        },
-        nextAt(conversion) {
-            let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor();
-            if (conversion.gainModifier) {
-                next = conversion.gainModifier.revert(next);
-            }
-            next = Decimal.max(0, next)
-                .root(unref(processedExponent))
-                .times(unref(processedBase))
-                .max(unref(processedBase));
-            if (conversion.costModifier) {
-                next = conversion.costModifier.revert(next);
-            }
-            return next;
-        }
-    };
-}
-
 /**
  * Creates a conversion that simply adds to the gainResource amount upon converting.
  * This is similar to the behavior of "normal" layers in The Modding Tree.
@@ -396,13 +205,8 @@ export function createIndependentConversion<S extends ConversionOptions>(
 
         if (conversion.currentGain == null) {
             conversion.currentGain = computed(() => {
-                let gain = conversion.gainModifier
-                    ? conversion.gainModifier.apply(
-                          conversion.scaling.currentGain(conversion as GenericConversion)
-                      )
-                    : conversion.scaling.currentGain(conversion as GenericConversion);
+                let gain = conversion.formula.evaluate(conversion.baseResource.value);
                 gain = Decimal.floor(gain).max(conversion.gainResource.value);
-
                 if (unref(conversion.buyMax) === false) {
                     gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
                 }
@@ -412,7 +216,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
         if (conversion.actualGain == null) {
             conversion.actualGain = computed(() => {
                 let gain = Decimal.sub(
-                    Decimal.floor(conversion.scaling.currentGain(conversion as GenericConversion)),
+                    conversion.formula.evaluate(conversion.baseResource.value),
                     conversion.gainResource.value
                 ).max(0);
 
@@ -424,11 +228,7 @@ export function createIndependentConversion<S extends ConversionOptions>(
         }
         setDefault(conversion, "convert", function () {
             const amountGained = unref((conversion as GenericConversion).actualGain);
-            conversion.gainResource.value = conversion.gainModifier
-                ? conversion.gainModifier.apply(
-                      unref((conversion as GenericConversion).currentGain)
-                  )
-                : unref((conversion as GenericConversion).currentGain);
+            conversion.gainResource.value = unref((conversion as GenericConversion).currentGain);
             (conversion as GenericConversion).spend(amountGained);
             conversion.onConvert?.(amountGained);
         });
@@ -466,71 +266,3 @@ export function setupPassiveGeneration(
         }
     });
 }
-
-/**
- * Given a value, this function finds the amount above a certain value and raises it to a power.
- * If the power is <1, this will effectively make the value scale slower after the cap.
- * @param value The raw value.
- * @param cap The value after which the softcap should be applied.
- * @param power The power to raise value above the cap to.
- * @example
- * A softcap added via `addSoftcap(scaling, 100, 0.5)` would produce the following values:
- * | Raw Value | Softcapped Value |
- * | --------- | ---------------- |
- * | 1         | 1                |
- * | 100       | 100              |
- * | 125       | 105              |
- * | 200       | 110              |
- */
-export function softcap(
-    value: DecimalSource,
-    cap: DecimalSource,
-    power: DecimalSource = 0.5
-): DecimalSource {
-    if (Decimal.lte(value, cap)) {
-        return value;
-    } else {
-        return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power)));
-    }
-}
-
-/**
- * Creates a scaling function based off an existing scaling function, with a softcap applied to it.
- * The softcap will take any value above a certain value and raise it to a power.
- * If the power is <1, this will effectively make the value scale slower after the cap.
- * @param scaling The raw scaling function.
- * @param cap The value after which the softcap should be applied.
- * @param power The power to raise value about the cap to.
- * @see {@link softcap}.
- */
-export function addSoftcap(
-    scaling: ScalingFunction,
-    cap: ProcessedComputable<DecimalSource>,
-    power: ProcessedComputable<DecimalSource> = 0.5
-): ScalingFunction {
-    return {
-        ...scaling,
-        currentAt: conversion =>
-            softcap(scaling.currentAt(conversion), unref(cap), Decimal.recip(unref(power))),
-        nextAt: conversion =>
-            softcap(scaling.nextAt(conversion), unref(cap), Decimal.recip(unref(power))),
-        currentGain: conversion =>
-            softcap(scaling.currentGain(conversion), unref(cap), unref(power))
-    };
-}
-
-/**
- * Creates a scaling function off an existing function, with a hardcap applied to it.
- * The harcap will ensure that the currentGain will stop at a given cap.
- * @param scaling The raw scaling function.
- * @param cap The maximum value the scaling function can output.
- */
-export function addHardcap(
-    scaling: ScalingFunction,
-    cap: ProcessedComputable<DecimalSource>
-): ScalingFunction {
-    return {
-        ...scaling,
-        currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap))
-    };
-}
diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 12ea545..9806580 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -315,6 +315,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         return new Formula({ variable: value }) as InvertibleFormula;
     }
 
+    // TODO add integration support to step-wise functions
     /**
      * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier.
      * This function assumes the incoming {@ref value} will be continuous and monotonically increasing.

From 98cb997056f4de177004092038bd3b519a39b2cf Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 21:47:31 -0500
Subject: [PATCH 24/36] Make conversions pass the variable to the formula

---
 src/features/conversion.ts | 41 ++++++++++++++++++++++++++++----------
 1 file changed, 30 insertions(+), 11 deletions(-)

diff --git a/src/features/conversion.ts b/src/features/conversion.ts
index f5146ca..9eeb792 100644
--- a/src/features/conversion.ts
+++ b/src/features/conversion.ts
@@ -1,7 +1,12 @@
 import type { OptionsFunc, Replace } from "features/feature";
 import { setDefault } from "features/feature";
 import type { Resource } from "features/resources/resource";
-import { InvertibleFormula } from "game/formulas/types";
+import Formula from "game/formulas/formulas";
+import {
+    IntegrableFormula,
+    InvertibleFormula,
+    InvertibleIntegralFormula
+} from "game/formulas/types";
 import type { BaseLayer } from "game/layers";
 import type { DecimalSource } from "util/bignum";
 import Decimal from "util/bignum";
@@ -15,9 +20,11 @@ import { computed, unref } from "vue";
 export interface ConversionOptions {
     /**
      * The formula used to determine how much {@link gainResource} should be earned by this converting.
-     * When evaluating, the variable will always be overidden to the amount of {@link baseResource}.
+     * The passed value will be a Formula representing the {@link baseResource} variable.
      */
-    formula: InvertibleFormula;
+    formula: (
+        variable: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula
+    ) => InvertibleFormula;
     /**
      * How much of the output resource the conversion can currently convert for.
      * Typically this will be set for you in a conversion constructor.
@@ -98,6 +105,7 @@ export type Conversion<T extends ConversionOptions> = Replace<
 export type GenericConversion = Replace<
     Conversion<ConversionOptions>,
     {
+        formula: InvertibleFormula;
         currentGain: ProcessedComputable<DecimalSource>;
         actualGain: ProcessedComputable<DecimalSource>;
         currentAt: ProcessedComputable<DecimalSource>;
@@ -120,9 +128,14 @@ export function createConversion<T extends ConversionOptions>(
     return createLazyProxy(() => {
         const conversion = optionsFunc();
 
+        (conversion as GenericConversion).formula = conversion.formula(
+            Formula.variable(conversion.baseResource)
+        );
         if (conversion.currentGain == null) {
             conversion.currentGain = computed(() => {
-                let gain = conversion.formula.evaluate(conversion.baseResource.value);
+                let gain = (conversion as GenericConversion).formula.evaluate(
+                    conversion.baseResource.value
+                );
                 gain = Decimal.floor(gain).max(0);
 
                 if (unref(conversion.buyMax) === false) {
@@ -136,14 +149,14 @@ export function createConversion<T extends ConversionOptions>(
         }
         if (conversion.currentAt == null) {
             conversion.currentAt = computed(() => {
-                return conversion.formula.invert(
+                return (conversion as GenericConversion).formula.invert(
                     Decimal.floor(unref((conversion as GenericConversion).currentGain))
                 );
             });
         }
         if (conversion.nextAt == null) {
             conversion.nextAt = computed(() => {
-                return conversion.formula.invert(
+                return (conversion as GenericConversion).formula.invert(
                     Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1)
                 );
             });
@@ -205,7 +218,9 @@ export function createIndependentConversion<S extends ConversionOptions>(
 
         if (conversion.currentGain == null) {
             conversion.currentGain = computed(() => {
-                let gain = conversion.formula.evaluate(conversion.baseResource.value);
+                let gain = (conversion as unknown as GenericConversion).formula.evaluate(
+                    conversion.baseResource.value
+                );
                 gain = Decimal.floor(gain).max(conversion.gainResource.value);
                 if (unref(conversion.buyMax) === false) {
                     gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
@@ -216,7 +231,9 @@ export function createIndependentConversion<S extends ConversionOptions>(
         if (conversion.actualGain == null) {
             conversion.actualGain = computed(() => {
                 let gain = Decimal.sub(
-                    conversion.formula.evaluate(conversion.baseResource.value),
+                    (conversion as unknown as GenericConversion).formula.evaluate(
+                        conversion.baseResource.value
+                    ),
                     conversion.gainResource.value
                 ).max(0);
 
@@ -227,9 +244,11 @@ export function createIndependentConversion<S extends ConversionOptions>(
             });
         }
         setDefault(conversion, "convert", function () {
-            const amountGained = unref((conversion as GenericConversion).actualGain);
-            conversion.gainResource.value = unref((conversion as GenericConversion).currentGain);
-            (conversion as GenericConversion).spend(amountGained);
+            const amountGained = unref((conversion as unknown as GenericConversion).actualGain);
+            conversion.gainResource.value = unref(
+                (conversion as unknown as GenericConversion).currentGain
+            );
+            (conversion as unknown as GenericConversion).spend(amountGained);
             conversion.onConvert?.(amountGained);
         });
 

From 804d48ae80efd8c4ec7b85bda37b2dbca263cbfe Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 21:56:43 -0500
Subject: [PATCH 25/36] Fix cost requirement requiring spendResources Also
 fixed it defaulting to false instead of true

---
 src/game/requirements.tsx | 48 +++++++++++++++++++--------------------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx
index 9e775de..acb7f7b 100644
--- a/src/game/requirements.tsx
+++ b/src/game/requirements.tsx
@@ -1,5 +1,12 @@
 import { isArray } from "@vue/shared";
-import { CoercableComponent, isVisible, jsx, setDefault, Visibility } from "features/feature";
+import {
+    CoercableComponent,
+    isVisible,
+    jsx,
+    Replace,
+    setDefault,
+    Visibility
+} from "features/feature";
 import { displayResource, Resource } from "features/resources/resource";
 import Decimal, { DecimalSource } from "lib/break_eternity";
 import {
@@ -78,7 +85,7 @@ export interface CostRequirementOptions {
      * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions.
      * @see {Formula}
      */
-    spendResources: Computable<boolean>;
+    spendResources?: Computable<boolean>;
     /**
      * Pass-through to {@link Requirement.pay}. May be required for maximizing support.
      * @see {@link cost} for restrictions on maximizing support.
@@ -86,7 +93,15 @@ export interface CostRequirementOptions {
     pay?: (amount?: DecimalSource) => void;
 }
 
-export type CostRequirement = Requirement & CostRequirementOptions;
+export type CostRequirement = Replace<
+    Requirement & CostRequirementOptions,
+    {
+        cost: ProcessedComputable<DecimalSource> | GenericFormula;
+        visibility: ProcessedComputable<Visibility.Visible | Visibility.None | boolean>;
+        requiresPay: ProcessedComputable<boolean>;
+        spendResources: ProcessedComputable<boolean>;
+    }
+>;
 
 /**
  * Lazily creates a requirement with the given options, that is based on meeting an amount of a resource.
@@ -109,13 +124,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
                 {displayResource(
                     req.resource,
                     req.cost instanceof Formula
-                        ? calculateCost(
-                              req.cost,
-                              amount ?? 1,
-                              unref(
-                                  req.spendResources as ProcessedComputable<boolean> | undefined
-                              ) ?? true
-                          )
+                        ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
                         : unref(req.cost as ProcessedComputable<DecimalSource>)
                 )}{" "}
                 {req.resource.displayName}
@@ -127,13 +136,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
                 {displayResource(
                     req.resource,
                     req.cost instanceof Formula
-                        ? calculateCost(
-                              req.cost,
-                              amount ?? 1,
-                              unref(
-                                  req.spendResources as ProcessedComputable<boolean> | undefined
-                              ) ?? true
-                          )
+                        ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
                         : unref(req.cost as ProcessedComputable<DecimalSource>)
                 )}{" "}
                 {req.resource.displayName}
@@ -146,16 +149,11 @@ export function createCostRequirement<T extends CostRequirementOptions>(
         processComputable(req as T, "requiresPay");
         setDefault(req, "requiresPay", true);
         processComputable(req as T, "spendResources");
-        setDefault(req, "spendResources", false);
+        setDefault(req, "spendResources", true);
         setDefault(req, "pay", function (amount?: DecimalSource) {
             const cost =
                 req.cost instanceof Formula
-                    ? calculateCost(
-                          req.cost,
-                          amount ?? 1,
-                          unref(req.spendResources as ProcessedComputable<boolean> | undefined) ??
-                              true
-                      )
+                    ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean)
                     : unref(req.cost as ProcessedComputable<DecimalSource>);
             req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
         });
@@ -166,7 +164,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
             req.requirementMet = calculateMaxAffordable(
                 req.cost as InvertibleFormula,
                 req.resource,
-                unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true
+                unref(req.spendResources) as boolean
             );
         } else {
             req.requirementMet = computed(() => {

From f717fe8db286c0048ea4dd6b9424da70c58c25f0 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 21:42:17 -0500
Subject: [PATCH 26/36] Make every component use GenericComponent some projects
 seem to require this and others not?

---
 src/features/achievements/achievement.tsx |  5 +++--
 src/features/bars/bar.ts                  |  4 ++--
 src/features/boards/board.ts              |  6 +++---
 src/features/challenges/challenge.tsx     | 12 +++++++++---
 src/features/clickables/clickable.ts      |  4 ++--
 src/features/grids/grid.ts                | 12 +++++++++---
 src/features/infoboxes/infobox.ts         | 12 +++++++++---
 src/features/links/links.ts               |  6 +++---
 src/features/milestones/milestone.tsx     |  4 ++--
 src/features/repeatable.tsx               | 12 +++++++++---
 src/features/tabs/tab.ts                  | 12 +++++++++---
 src/features/tabs/tabFamily.ts            | 16 +++++++++++-----
 src/features/trees/tree.ts                | 16 +++++++++++-----
 src/features/upgrades/upgrade.ts          |  4 ++--
 14 files changed, 84 insertions(+), 41 deletions(-)

diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx
index 7daaf7e..4776fd1 100644
--- a/src/features/achievements/achievement.tsx
+++ b/src/features/achievements/achievement.tsx
@@ -3,6 +3,7 @@ import {
     CoercableComponent,
     Component,
     GatherProps,
+    GenericComponent,
     getUniqueID,
     isVisible,
     OptionsFunc,
@@ -48,7 +49,7 @@ export interface BaseAchievement {
     earned: Persistent<boolean>;
     complete: VoidFunction;
     type: typeof AchievementType;
-    [Component]: typeof AchievementComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -79,7 +80,7 @@ export function createAchievement<T extends AchievementOptions>(
         const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
         achievement.id = getUniqueID("achievement-");
         achievement.type = AchievementType;
-        achievement[Component] = AchievementComponent;
+        achievement[Component] = AchievementComponent as GenericComponent;
 
         achievement.earned = earned;
         achievement.complete = function () {
diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts
index f0436f4..bab5887 100644
--- a/src/features/bars/bar.ts
+++ b/src/features/bars/bar.ts
@@ -40,7 +40,7 @@ export interface BarOptions {
 export interface BaseBar {
     id: string;
     type: typeof BarType;
-    [Component]: typeof BarComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -77,7 +77,7 @@ export function createBar<T extends BarOptions>(
         const bar = optionsFunc();
         bar.id = getUniqueID("bar-");
         bar.type = BarType;
-        bar[Component] = BarComponent;
+        bar[Component] = BarComponent as GenericComponent;
 
         processComputable(bar as T, "visibility");
         setDefault(bar, "visibility", Visibility.Visible);
diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts
index cc17d2e..528bc26 100644
--- a/src/features/boards/board.ts
+++ b/src/features/boards/board.ts
@@ -1,5 +1,5 @@
 import BoardComponent from "features/boards/Board.vue";
-import type { OptionsFunc, Replace, StyleValue } from "features/feature";
+import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
 import {
     Component,
     findFeatures,
@@ -178,7 +178,7 @@ export interface BaseBoard {
     selectedAction: Ref<GenericBoardNodeAction | null>;
     mousePosition: Ref<{ x: number; y: number } | null>;
     type: typeof BoardType;
-    [Component]: typeof BoardComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -221,7 +221,7 @@ export function createBoard<T extends BoardOptions>(
         const board = optionsFunc();
         board.id = getUniqueID("board-");
         board.type = BoardType;
-        board[Component] = BoardComponent;
+        board[Component] = BoardComponent as GenericComponent;
 
         if (board.state) {
             deletePersistent(state);
diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index 34f850c..2925595 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -1,7 +1,13 @@
 import { isArray } from "@vue/shared";
 import Toggle from "components/fields/Toggle.vue";
 import ChallengeComponent from "features/challenges/Challenge.vue";
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import {
     Component,
     GatherProps,
@@ -67,7 +73,7 @@ export interface BaseChallenge {
     toggle: VoidFunction;
     complete: (remainInChallenge?: boolean) => void;
     type: typeof ChallengeType;
-    [Component]: typeof ChallengeComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -106,7 +112,7 @@ export function createChallenge<T extends ChallengeOptions>(
 
         challenge.id = getUniqueID("challenge-");
         challenge.type = ChallengeType;
-        challenge[Component] = ChallengeComponent;
+        challenge[Component] = ChallengeComponent as GenericComponent;
 
         challenge.completions = completions;
         challenge.active = active;
diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts
index c7c83e3..b44854b 100644
--- a/src/features/clickables/clickable.ts
+++ b/src/features/clickables/clickable.ts
@@ -42,7 +42,7 @@ export interface ClickableOptions {
 export interface BaseClickable {
     id: string;
     type: typeof ClickableType;
-    [Component]: typeof ClickableComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -73,7 +73,7 @@ export function createClickable<T extends ClickableOptions>(
         const clickable = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
         clickable.id = getUniqueID("clickable-");
         clickable.type = ClickableType;
-        clickable[Component] = ClickableComponent;
+        clickable[Component] = ClickableComponent as GenericComponent;
 
         processComputable(clickable as T, "visibility");
         setDefault(clickable, "visibility", Visibility.Visible);
diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts
index 53fd7fb..b7899ae 100644
--- a/src/features/grids/grid.ts
+++ b/src/features/grids/grid.ts
@@ -1,4 +1,10 @@
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
 import GridComponent from "features/grids/Grid.vue";
 import type { Persistent, State } from "game/persistence";
@@ -206,7 +212,7 @@ export interface BaseGrid {
     cells: Record<string | number, GridCell>;
     cellState: Persistent<Record<string | number, State>>;
     type: typeof GridType;
-    [Component]: typeof GridComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -242,7 +248,7 @@ export function createGrid<T extends GridOptions>(
     return createLazyProxy(() => {
         const grid = optionsFunc();
         grid.id = getUniqueID("grid-");
-        grid[Component] = GridComponent;
+        grid[Component] = GridComponent as GenericComponent;
 
         grid.cellState = cellState;
 
diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts
index 81d7bf6..ccd2bd3 100644
--- a/src/features/infoboxes/infobox.ts
+++ b/src/features/infoboxes/infobox.ts
@@ -1,4 +1,10 @@
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
 import InfoboxComponent from "features/infoboxes/Infobox.vue";
 import type { Persistent } from "game/persistence";
@@ -30,7 +36,7 @@ export interface BaseInfobox {
     id: string;
     collapsed: Persistent<boolean>;
     type: typeof InfoboxType;
-    [Component]: typeof InfoboxComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -63,7 +69,7 @@ export function createInfobox<T extends InfoboxOptions>(
         const infobox = optionsFunc();
         infobox.id = getUniqueID("infobox-");
         infobox.type = InfoboxType;
-        infobox[Component] = InfoboxComponent;
+        infobox[Component] = InfoboxComponent as GenericComponent;
 
         infobox.collapsed = collapsed;
 
diff --git a/src/features/links/links.ts b/src/features/links/links.ts
index 5ab8779..77ea0ba 100644
--- a/src/features/links/links.ts
+++ b/src/features/links/links.ts
@@ -1,4 +1,4 @@
-import type { OptionsFunc, Replace } from "features/feature";
+import type { GenericComponent, OptionsFunc, Replace } from "features/feature";
 import { GatherProps, Component } from "features/feature";
 import type { Position } from "game/layers";
 import type { Computable, GetComputableType, ProcessedComputable } from "util/computed";
@@ -22,7 +22,7 @@ export interface LinksOptions {
 
 export interface BaseLinks {
     type: typeof LinksType;
-    [Component]: typeof LinksComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -46,7 +46,7 @@ export function createLinks<T extends LinksOptions>(
     return createLazyProxy(() => {
         const links = optionsFunc();
         links.type = LinksType;
-        links[Component] = LinksComponent;
+        links[Component] = LinksComponent as GenericComponent;
 
         processComputable(links as T, "links");
 
diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx
index 30cd243..0a973db 100644
--- a/src/features/milestones/milestone.tsx
+++ b/src/features/milestones/milestone.tsx
@@ -69,7 +69,7 @@ export interface BaseMilestone {
     earned: Persistent<boolean>;
     complete: VoidFunction;
     type: typeof MilestoneType;
-    [Component]: typeof MilestoneComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -99,7 +99,7 @@ export function createMilestone<T extends MilestoneOptions>(
         const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
         milestone.id = getUniqueID("milestone-");
         milestone.type = MilestoneType;
-        milestone[Component] = MilestoneComponent;
+        milestone[Component] = MilestoneComponent as GenericComponent;
 
         milestone.earned = earned;
         milestone.complete = function () {
diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx
index b442068..ec338eb 100644
--- a/src/features/repeatable.tsx
+++ b/src/features/repeatable.tsx
@@ -1,6 +1,12 @@
 import { isArray } from "@vue/shared";
 import ClickableComponent from "features/clickables/Clickable.vue";
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature";
 import { DefaultValue, Persistent, persistent } from "game/persistence";
 import {
@@ -88,7 +94,7 @@ export interface BaseRepeatable {
     /** A symbol that helps identify features of the same type. */
     type: typeof RepeatableType;
     /** The Vue component used to render this feature. */
-    [Component]: typeof ClickableComponent;
+    [Component]: GenericComponent;
     /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
@@ -131,7 +137,7 @@ export function createRepeatable<T extends RepeatableOptions>(
 
         repeatable.id = getUniqueID("repeatable-");
         repeatable.type = RepeatableType;
-        repeatable[Component] = ClickableComponent;
+        repeatable[Component] = ClickableComponent as GenericComponent;
 
         repeatable.amount = amount;
         repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0;
diff --git a/src/features/tabs/tab.ts b/src/features/tabs/tab.ts
index 3f205fc..6917009 100644
--- a/src/features/tabs/tab.ts
+++ b/src/features/tabs/tab.ts
@@ -1,4 +1,10 @@
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import { Component, GatherProps, getUniqueID } from "features/feature";
 import TabComponent from "features/tabs/Tab.vue";
 import type { Computable, GetComputableType } from "util/computed";
@@ -15,7 +21,7 @@ export interface TabOptions {
 export interface BaseTab {
     id: string;
     type: typeof TabType;
-    [Component]: typeof TabComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -37,7 +43,7 @@ export function createTab<T extends TabOptions>(
         const tab = optionsFunc();
         tab.id = getUniqueID("tab-");
         tab.type = TabType;
-        tab[Component] = TabComponent;
+        tab[Component] = TabComponent as GenericComponent;
 
         tab[GatherProps] = function (this: GenericTab) {
             const { display } = this;
diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts
index 9652f3d..32df1e7 100644
--- a/src/features/tabs/tabFamily.ts
+++ b/src/features/tabs/tabFamily.ts
@@ -1,4 +1,10 @@
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import {
     Component,
     GatherProps,
@@ -37,7 +43,7 @@ export interface TabButtonOptions {
 
 export interface BaseTabButton {
     type: typeof TabButtonType;
-    [Component]: typeof TabButtonComponent;
+    [Component]: GenericComponent;
 }
 
 export type TabButton<T extends TabButtonOptions> = Replace<
@@ -73,7 +79,7 @@ export interface BaseTabFamily {
     activeTab: Ref<GenericTab | CoercableComponent | null>;
     selected: Persistent<string>;
     type: typeof TabFamilyType;
-    [Component]: typeof TabFamilyComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -107,13 +113,13 @@ export function createTabFamily<T extends TabFamilyOptions>(
 
         tabFamily.id = getUniqueID("tabFamily-");
         tabFamily.type = TabFamilyType;
-        tabFamily[Component] = TabFamilyComponent;
+        tabFamily[Component] = TabFamilyComponent as GenericComponent;
 
         tabFamily.tabs = Object.keys(tabs).reduce<Record<string, GenericTabButton>>(
             (parsedTabs, tab) => {
                 const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabs[tab]();
                 tabButton.type = TabButtonType;
-                tabButton[Component] = TabButtonComponent;
+                tabButton[Component] = TabButtonComponent as GenericComponent;
 
                 processComputable(tabButton as TabButtonOptions, "visibility");
                 setDefault(tabButton, "visibility", Visibility.Visible);
diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts
index bcbf333..a3efba7 100644
--- a/src/features/trees/tree.ts
+++ b/src/features/trees/tree.ts
@@ -1,4 +1,10 @@
-import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature";
+import type {
+    CoercableComponent,
+    GenericComponent,
+    OptionsFunc,
+    Replace,
+    StyleValue
+} from "features/feature";
 import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature";
 import type { Link } from "features/links/links";
 import type { GenericReset } from "features/reset";
@@ -39,7 +45,7 @@ export interface TreeNodeOptions {
 export interface BaseTreeNode {
     id: string;
     type: typeof TreeNodeType;
-    [Component]: typeof TreeNodeComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -72,7 +78,7 @@ export function createTreeNode<T extends TreeNodeOptions>(
         const treeNode = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
         treeNode.id = getUniqueID("treeNode-");
         treeNode.type = TreeNodeType;
-        treeNode[Component] = TreeNodeComponent;
+        treeNode[Component] = TreeNodeComponent as GenericComponent;
 
         processComputable(treeNode as T, "visibility");
         setDefault(treeNode, "visibility", Visibility.Visible);
@@ -157,7 +163,7 @@ export interface BaseTree {
     isResetting: Ref<boolean>;
     resettingNode: Ref<GenericTreeNode | null>;
     type: typeof TreeType;
-    [Component]: typeof TreeComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -186,7 +192,7 @@ export function createTree<T extends TreeOptions>(
         const tree = optionsFunc();
         tree.id = getUniqueID("tree-");
         tree.type = TreeType;
-        tree[Component] = TreeComponent;
+        tree[Component] = TreeComponent as GenericComponent;
 
         tree.isResetting = ref(false);
         tree.resettingNode = shallowRef(null);
diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts
index 924e28f..5bcf5df 100644
--- a/src/features/upgrades/upgrade.ts
+++ b/src/features/upgrades/upgrade.ts
@@ -61,7 +61,7 @@ export interface BaseUpgrade {
     canPurchase: Ref<boolean>;
     purchase: VoidFunction;
     type: typeof UpgradeType;
-    [Component]: typeof UpgradeComponent;
+    [Component]: GenericComponent;
     [GatherProps]: () => Record<string, unknown>;
 }
 
@@ -92,7 +92,7 @@ export function createUpgrade<T extends UpgradeOptions>(
         const upgrade = optionsFunc();
         upgrade.id = getUniqueID("upgrade-");
         upgrade.type = UpgradeType;
-        upgrade[Component] = UpgradeComponent;
+        upgrade[Component] = UpgradeComponent as GenericComponent;
 
         upgrade.bought = bought;
         upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements));

From 572566c4c15197c291b6d3d3f2ae10deb9a41bbf Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 22:03:58 -0500
Subject: [PATCH 27/36] Updated prestige layer to new conversion

---
 src/data/layers/prestige.tsx | 7 +++----
 src/features/conversion.ts   | 2 +-
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/data/layers/prestige.tsx b/src/data/layers/prestige.tsx
index 4490ded..30fe99f 100644
--- a/src/data/layers/prestige.tsx
+++ b/src/data/layers/prestige.tsx
@@ -3,7 +3,7 @@
  * @hidden
  */
 import { main } from "data/projEntry";
-import { createCumulativeConversion, createPolynomialScaling } from "features/conversion";
+import { createCumulativeConversion } from "features/conversion";
 import { jsx } from "features/feature";
 import { createHotkey } from "features/hotkey";
 import { createReset } from "features/reset";
@@ -23,10 +23,9 @@ const layer = createLayer(id, function (this: BaseLayer) {
     const points = createResource<DecimalSource>(0, "prestige points");
 
     const conversion = createCumulativeConversion(() => ({
-        scaling: createPolynomialScaling(10, 0.5),
+        formula: x => x.div(10).sqrt(),
         baseResource: main.points,
-        gainResource: points,
-        roundUpCost: true
+        gainResource: points
     }));
 
     const reset = createReset(() => ({
diff --git a/src/features/conversion.ts b/src/features/conversion.ts
index 9eeb792..1ccf26b 100644
--- a/src/features/conversion.ts
+++ b/src/features/conversion.ts
@@ -92,6 +92,7 @@ export interface BaseConversion {
 export type Conversion<T extends ConversionOptions> = Replace<
     T & BaseConversion,
     {
+        formula: InvertibleFormula;
         currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
         actualGain: GetComputableTypeWithDefault<T["actualGain"], Ref<DecimalSource>>;
         currentAt: GetComputableTypeWithDefault<T["currentAt"], Ref<DecimalSource>>;
@@ -105,7 +106,6 @@ export type Conversion<T extends ConversionOptions> = Replace<
 export type GenericConversion = Replace<
     Conversion<ConversionOptions>,
     {
-        formula: InvertibleFormula;
         currentGain: ProcessedComputable<DecimalSource>;
         actualGain: ProcessedComputable<DecimalSource>;
         currentAt: ProcessedComputable<DecimalSource>;

From 528afc6b590edf235e6c6f26433594bd32a6e8b4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 22:09:13 -0500
Subject: [PATCH 28/36] Fix tooltips being pinnable causing SO

---
 src/features/tooltips/tooltip.ts | 24 ++++++++++++++----------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts
index 13cb931..2cb977d 100644
--- a/src/features/tooltips/tooltip.ts
+++ b/src/features/tooltips/tooltip.ts
@@ -1,6 +1,6 @@
 import type { CoercableComponent, Replace, StyleValue } from "features/feature";
 import { Component, GatherProps, setDefault } from "features/feature";
-import { persistent } from "game/persistence";
+import { deletePersistent, Persistent, persistent } from "game/persistence";
 import { Direction } from "util/common";
 import type {
     Computable,
@@ -71,18 +71,22 @@ export function addTooltip<T extends TooltipOptions>(
     processComputable(options as T, "yoffset");
 
     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;
-        } else {
-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            (element as any).pinned = options.pinned = persistent<boolean>(false, false);
-        }
+        options.pinned = persistent<boolean>(false, false);
     }
 
     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;
         const elementGatherProps = element[GatherProps].bind(element);

From 165eba688e2fc67c2073f8378d340655d7acbd2f Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 22:25:18 -0500
Subject: [PATCH 29/36] Slightly improve resource imports

---
 src/features/resources/resource.ts | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/features/resources/resource.ts b/src/features/resources/resource.ts
index be42e7e..8e203d3 100644
--- a/src/features/resources/resource.ts
+++ b/src/features/resources/resource.ts
@@ -1,8 +1,6 @@
 import { globalBus } from "game/events";
-import { NonPersistent, Persistent, State } from "game/persistence";
-import { persistent } from "game/persistence";
-import player from "game/player";
-import settings from "game/settings";
+import type { Persistent, State } from "game/persistence";
+import { NonPersistent, persistent } from "game/persistence";
 import type { DecimalSource } from "util/bignum";
 import Decimal, { format, formatWhole } from "util/bignum";
 import type { ProcessedComputable } from "util/computed";

From e6c7ad62a79d991d96f1282d87be23b09692d490 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 23:33:49 -0500
Subject: [PATCH 30/36] Document challenge

---
 src/features/challenges/challenge.tsx | 69 +++++++++++++++++++++++++--
 1 file changed, 64 insertions(+), 5 deletions(-)

diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index 2925595..3a1ba48 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -36,47 +36,87 @@ import { createLazyProxy } from "util/proxies";
 import type { Ref, WatchStopHandle } from "vue";
 import { computed, unref, watch } from "vue";
 
-export const ChallengeType = Symbol("ChallengeType");
+/** A symbol used to identify {@link Challenge} features. */
+export const ChallengeType = Symbol("Challenge");
 
+/**
+ * An object that configures a {@link Challenge}.
+ */
 export interface ChallengeOptions {
+    /** Whether this challenge should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** Whether this challenge can be started. */
     canStart?: Computable<boolean>;
+    /** The reset function for this challenge. */
     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. */
     mark?: Computable<boolean | string>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** The display to use for this challenge. */
     display?: Computable<
         | CoercableComponent
         | {
+              /** A header to appear at the top of the display. */
               title?: CoercableComponent;
+              /** The main text that appears in the display. */
               description: CoercableComponent;
+              /** A description of the current goal for this challenge. */
               goal?: CoercableComponent;
+              /** A description of what will change upon completing this challenge. */
               reward?: CoercableComponent;
+              /** A description of the current effect of this challenge. */
               effectDisplay?: CoercableComponent;
           }
     >;
+    /** A function that is called when the challenge is completed. */
     onComplete?: VoidFunction;
+    /** A function that is called when the challenge is exited. */
     onExit?: VoidFunction;
+    /** A function that is called when the challenge is entered. */
     onEnter?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
+ */
 export interface BaseChallenge {
+    /** An auto-generated ID for identifying challenges that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** The current amount of times this challenge can be completed. */
     canComplete: Ref<DecimalSource>;
+    /** The current number of times this challenge has been completed. */
     completions: Persistent<DecimalSource>;
+    /** Whether or not this challenge has been completed. */
     completed: Ref<boolean>;
+    /** Whether or not this challenge's completion count is at its limit. */
     maxed: Ref<boolean>;
+    /** Whether or not this challenge is currently active. */
     active: Persistent<boolean>;
+    /** A function to enter or leave the challenge. */
     toggle: VoidFunction;
+    /**
+     * A function to complete this challenge.
+     * @param remainInChallenge - Optional parameter to specify if the challenge should remain active after completion.
+     */
     complete: (remainInChallenge?: boolean) => void;
+    /** A symbol that helps identify features of the same type. */
     type: typeof ChallengeType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that can be entered and exited, and have one or more completions with scaling requirements. */
 export type Challenge<T extends ChallengeOptions> = Replace<
     T & BaseChallenge,
     {
@@ -92,6 +132,7 @@ export type Challenge<T extends ChallengeOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Challenge} object. */
 export type GenericChallenge = Replace<
     Challenge<ChallengeOptions>,
     {
@@ -102,6 +143,10 @@ export type GenericChallenge = Replace<
     }
 >;
 
+/**
+ * Lazily creates a challenge with the given options.
+ * @param optionsFunc Challenge options.
+ */
 export function createChallenge<T extends ChallengeOptions>(
     optionsFunc: OptionsFunc<T, BaseChallenge, GenericChallenge>
 ): Challenge<T> {
@@ -248,6 +293,12 @@ export function createChallenge<T extends ChallengeOptions>(
     });
 }
 
+/**
+ * This will automatically complete a challenge when it's requirements are met.
+ * @param challenge The challenge to auto-complete
+ * @param autoActive Whether or not auto-completing should currently occur
+ * @param exitOnComplete Whether or not to exit the challenge after auto-completion
+ */
 export function setupAutoComplete(
     challenge: GenericChallenge,
     autoActive: Computable<boolean> = true,
@@ -264,19 +315,27 @@ export function setupAutoComplete(
     );
 }
 
+/**
+ * Utility for taking an array of challenges where only one may be active at a time, and giving a ref to the one currently active (or null if none are active)
+ * @param challenges The list of challenges that are mutually exclusive
+ */
 export function createActiveChallenge(
     challenges: GenericChallenge[]
-): Ref<GenericChallenge | undefined> {
-    return computed(() => challenges.find(challenge => challenge.active.value));
+): Ref<GenericChallenge | null> {
+    return computed(() => challenges.find(challenge => challenge.active.value) ?? null);
 }
 
+/**
+ * Utility for reporting if any challenge in a list is currently active. Intended for preventing entering a challenge if another is already active.
+ * @param challenges List of challenges that are mutually exclusive
+ */
 export function isAnyChallengeActive(
-    challenges: GenericChallenge[] | Ref<GenericChallenge | undefined>
+    challenges: GenericChallenge[] | Ref<GenericChallenge | null>
 ): Ref<boolean> {
     if (isArray(challenges)) {
         challenges = createActiveChallenge(challenges);
     }
-    return computed(() => (challenges as Ref<GenericChallenge | undefined>).value != null);
+    return computed(() => (challenges as Ref<GenericChallenge | null>).value != null);
 }
 
 declare module "game/settings" {

From 742d2293d043d8c3e38c58ff3d844734c78e9757 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 23:49:51 -0500
Subject: [PATCH 31/36] Made achievements use requirements system, and document
 them

---
 src/features/achievements/achievement.tsx | 83 ++++++++++++++++-------
 1 file changed, 60 insertions(+), 23 deletions(-)

diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx
index 4776fd1..baa86ca 100644
--- a/src/features/achievements/achievement.tsx
+++ b/src/features/achievements/achievement.tsx
@@ -1,3 +1,4 @@
+import { isArray } from "@vue/shared";
 import AchievementComponent from "features/achievements/Achievement.vue";
 import {
     CoercableComponent,
@@ -5,7 +6,6 @@ import {
     GatherProps,
     GenericComponent,
     getUniqueID,
-    isVisible,
     OptionsFunc,
     Replace,
     setDefault,
@@ -16,6 +16,12 @@ import "game/notifications";
 import type { Persistent } from "game/persistence";
 import { persistent } from "game/persistence";
 import player from "game/player";
+import {
+    createBooleanRequirement,
+    createVisibilityRequirement,
+    Requirements,
+    requirementsMet
+} from "game/requirements";
 import settings from "game/settings";
 import type {
     Computable,
@@ -31,28 +37,50 @@ import { useToast } from "vue-toastification";
 
 const toast = useToast();
 
+/** A symbol used to identify {@link Achievement} features. */
 export const AchievementType = Symbol("Achievement");
 
+/**
+ * An object that configures an {@link Achievement}.
+ */
 export interface AchievementOptions {
+    /** Whether this achievement should be visible. */
     visibility?: Computable<Visibility | boolean>;
-    shouldEarn?: () => boolean;
+    /** The requirement(s) to earn this achievement. Can be left null if using {@link BaseAchievement.complete}. */
+    requirements?: Requirements;
+    /** The display to use for this achievement. */
     display?: Computable<CoercableComponent>;
+    /** Shows a marker on the corner of the feature. */
     mark?: Computable<boolean | string>;
+    /** An image to display as the background for this achievement. */
     image?: Computable<string>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** A function that is called when the achievement is completed. */
     onComplete?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
+ */
 export interface BaseAchievement {
+    /** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** Whether or not this achievement has been earned. */
     earned: Persistent<boolean>;
+    /** A function to complete this achievement. */
     complete: VoidFunction;
+    /** A symbol that helps identify features of the same type. */
     type: typeof AchievementType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature with that is passively earned upon meeting certain requirements. */
 export type Achievement<T extends AchievementOptions> = Replace<
     T & BaseAchievement,
     {
@@ -65,6 +93,7 @@ export type Achievement<T extends AchievementOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Achievement} object. */
 export type GenericAchievement = Replace<
     Achievement<AchievementOptions>,
     {
@@ -72,6 +101,10 @@ export type GenericAchievement = Replace<
     }
 >;
 
+/**
+ * Lazily creates a achievement with the given options.
+ * @param optionsFunc Achievement options.
+ */
 export function createAchievement<T extends AchievementOptions>(
     optionsFunc?: OptionsFunc<T, BaseAchievement, GenericAchievement>
 ): Achievement<T> {
@@ -85,6 +118,21 @@ export function createAchievement<T extends AchievementOptions>(
         achievement.earned = earned;
         achievement.complete = function () {
             earned.value = true;
+            const genericAchievement = achievement as GenericAchievement;
+            genericAchievement.onComplete?.();
+            if (genericAchievement.display != null) {
+                const Display = coerceComponent(unref(genericAchievement.display));
+                toast.info(
+                    <div>
+                        <h3>Achievement earned!</h3>
+                        <div>
+                            {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
+                            {/* @ts-ignore */}
+                            <Display />
+                        </div>
+                    </div>
+                );
+            }
         };
 
         processComputable(achievement as T, "visibility");
@@ -100,30 +148,19 @@ export function createAchievement<T extends AchievementOptions>(
             return { visibility, display, earned, image, style: unref(style), classes, mark, id };
         };
 
-        if (achievement.shouldEarn) {
+        if (achievement.requirements) {
             const genericAchievement = achievement as GenericAchievement;
+            const requirements = [
+                createVisibilityRequirement(genericAchievement),
+                createBooleanRequirement(() => !genericAchievement.earned.value),
+                ...(isArray(achievement.requirements)
+                    ? achievement.requirements
+                    : [achievement.requirements])
+            ];
             watchEffect(() => {
                 if (settings.active !== player.id) return;
-                if (
-                    !genericAchievement.earned.value &&
-                    isVisible(genericAchievement.visibility) &&
-                    genericAchievement.shouldEarn?.()
-                ) {
-                    genericAchievement.earned.value = true;
-                    genericAchievement.onComplete?.();
-                    if (genericAchievement.display != null) {
-                        const Display = coerceComponent(unref(genericAchievement.display));
-                        toast.info(
-                            <div>
-                                <h3>Achievement earned!</h3>
-                                <div>
-                                    {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
-                                    {/* @ts-ignore */}
-                                    <Display />
-                                </div>
-                            </div>
-                        );
-                    }
+                if (requirementsMet(requirements)) {
+                    genericAchievement.complete();
                 }
             });
         }

From 7c7fb38dd802a9d63227b3118fd624688f314f9f Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 3 Apr 2023 00:34:45 -0500
Subject: [PATCH 32/36] Merge milestones and achievements

Yay for removing a whole redundant feature!
---
 src/data/common.tsx                       |  36 ++--
 src/features/achievements/Achievement.vue |  82 +++++++-
 src/features/achievements/achievement.tsx | 141 ++++++++++++-
 src/features/milestones/Milestone.vue     | 128 ------------
 src/features/milestones/milestone.tsx     | 235 ----------------------
 5 files changed, 226 insertions(+), 396 deletions(-)
 delete mode 100644 src/features/milestones/Milestone.vue
 delete mode 100644 src/features/milestones/milestone.tsx

diff --git a/src/data/common.tsx b/src/data/common.tsx
index befb408..d163f72 100644
--- a/src/data/common.tsx
+++ b/src/data/common.tsx
@@ -1,10 +1,10 @@
 import Collapsible from "components/layout/Collapsible.vue";
+import { GenericAchievement } from "features/achievements/achievement";
 import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable";
 import { createClickable } from "features/clickables/clickable";
 import type { GenericConversion } from "features/conversion";
 import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature";
 import { jsx, setDefault } from "features/feature";
-import { GenericMilestone } from "features/milestones/milestone";
 import { displayResource, Resource } from "features/resources/resource";
 import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
 import { createTreeNode } from "features/trees/tree";
@@ -384,35 +384,35 @@ export function colorText(textToColor: string, color = "var(--accent2)"): JSX.El
 }
 
 /**
- * Creates a collapsible display of a list of milestones
- * @param milestones A dictionary of the milestones to display, inserted in the order from easiest to hardest
+ * Creates a collapsible display of a list of achievements
+ * @param achievements A dictionary of the achievements to display, inserted in the order from easiest to hardest
  */
-export function createCollapsibleMilestones(milestones: Record<string, GenericMilestone>) {
-    // Milestones are typically defined from easiest to hardest, and we want to show hardest first
-    const orderedMilestones = Object.values(milestones).reverse();
-    const collapseMilestones = persistent<boolean>(true, false);
-    const lockedMilestones = computed(() =>
-        orderedMilestones.filter(m => m.earned.value === false)
+export function createCollapsibleAchievements(achievements: Record<string, GenericAchievement>) {
+    // Achievements are typically defined from easiest to hardest, and we want to show hardest first
+    const orderedAchievements = Object.values(achievements).reverse();
+    const collapseAchievements = persistent<boolean>(true, false);
+    const lockedAchievements = computed(() =>
+        orderedAchievements.filter(m => m.earned.value === false)
     );
     const { firstFeature, collapsedContent, hasCollapsedContent } = getFirstFeature(
-        orderedMilestones,
+        orderedAchievements,
         m => m.earned.value
     );
     const display = jsx(() => {
-        const milestonesToDisplay = [...lockedMilestones.value];
+        const achievementsToDisplay = [...lockedAchievements.value];
         if (firstFeature.value) {
-            milestonesToDisplay.push(firstFeature.value);
+            achievementsToDisplay.push(firstFeature.value);
         }
         return renderColJSX(
-            ...milestonesToDisplay,
+            ...achievementsToDisplay,
             jsx(() => (
                 <Collapsible
-                    collapsed={collapseMilestones}
+                    collapsed={collapseAchievements}
                     content={collapsedContent}
                     display={
-                        collapseMilestones.value
-                            ? "Show other completed milestones"
-                            : "Hide other completed milestones"
+                        collapseAchievements.value
+                            ? "Show other completed achievements"
+                            : "Hide other completed achievements"
                     }
                     v-show={unref(hasCollapsedContent)}
                 />
@@ -420,7 +420,7 @@ export function createCollapsibleMilestones(milestones: Record<string, GenericMi
         );
     });
     return {
-        collapseMilestones,
+        collapseAchievements: collapseAchievements,
         display
     };
 }
diff --git a/src/features/achievements/Achievement.vue b/src/features/achievements/Achievement.vue
index 78ac17b..e0ffe6d 100644
--- a/src/features/achievements/Achievement.vue
+++ b/src/features/achievements/Achievement.vue
@@ -13,24 +13,27 @@
             achievement: true,
             locked: !unref(earned),
             bought: unref(earned),
+            small: unref(small),
             ...unref(classes)
         }"
     >
-        <component v-if="component" :is="component" />
+        <component v-if="comp" :is="comp" />
         <MarkNode :mark="unref(mark)" />
         <Node :id="id" />
     </div>
 </template>
 
-<script lang="ts">
+<script lang="tsx">
 import "components/common/features.css";
 import MarkNode from "components/MarkNode.vue";
 import Node from "components/Node.vue";
-import type { CoercableComponent } from "features/feature";
+import { CoercableComponent, jsx } from "features/feature";
 import { Visibility, isHidden, isVisible } from "features/feature";
-import { computeOptionalComponent, processedPropType } from "util/vue";
-import type { StyleValue } from "vue";
+import { displayRequirements, Requirements } from "game/requirements";
+import { coerceComponent, computeOptionalComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
+import { Component, shallowRef, StyleValue, UnwrapRef, watchEffect } from "vue";
 import { defineComponent, toRefs, unref } from "vue";
+import { GenericAchievement } from "./achievement";
 
 export default defineComponent({
     props: {
@@ -38,15 +41,17 @@ export default defineComponent({
             type: processedPropType<Visibility | boolean>(Number, Boolean),
             required: true
         },
-        display: processedPropType<CoercableComponent>(Object, String, Function),
+        display: processedPropType<UnwrapRef<GenericAchievement["display"]>>(Object, String, Function),
         earned: {
             type: processedPropType<boolean>(Boolean),
             required: true
         },
+        requirements: processedPropType<Requirements>(Object, Array),
         image: processedPropType<string>(String),
         style: processedPropType<StyleValue>(String, Object, Array),
         classes: processedPropType<Record<string, boolean>>(Object),
         mark: processedPropType<boolean | string>(Boolean, String),
+        small: processedPropType<boolean>(Boolean),
         id: {
             type: String,
             required: true
@@ -57,10 +62,44 @@ export default defineComponent({
         MarkNode
     },
     setup(props) {
-        const { display } = toRefs(props);
+        const { display, requirements } = toRefs(props);
+
+        const comp = shallowRef<Component | string>("");
+
+        watchEffect(() => {
+            const currDisplay = unwrapRef(display);
+            if (currDisplay == null) {
+                comp.value = "";
+                return;
+            }
+            if (isCoercableComponent(currDisplay)) {
+                comp.value = coerceComponent(currDisplay);
+                return;
+            }
+            const Requirement = currDisplay.requirement ? coerceComponent(currDisplay.requirement, "h3") : displayRequirements(unwrapRef(requirements) ?? []);
+            const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
+            const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
+            comp.value = coerceComponent(
+                jsx(() => (
+                    <span>
+                        <Requirement />
+                        {currDisplay.effectDisplay != null ? (
+                            <div>
+                                <EffectDisplay />
+                            </div>
+                        ) : null}
+                        {currDisplay.optionsDisplay != null ? (
+                            <div class="equal-spaced">
+                                <OptionsDisplay />
+                            </div>
+                        ) : null}
+                    </span>
+                ))
+            );
+        });
 
         return {
-            component: computeOptionalComponent(display),
+            comp,
             unref,
             Visibility,
             isVisible,
@@ -78,4 +117,31 @@ export default defineComponent({
     color: white;
     text-shadow: 0 0 2px #000000;
 }
+
+.achievement:not(.small) {
+    width: calc(100% - 10px);
+    min-width: 120px;
+    padding-left: 5px;
+    padding-right: 5px;
+    background-color: var(--locked);
+    border-width: 4px;
+    border-radius: 5px;
+    color: rgba(0, 0, 0, 0.5);
+    font-size: unset;
+    text-shadow: unset;
+}
+
+.achievement.done {
+    background-color: var(--bought);
+    cursor: default;
+}
+
+.achievement :deep(.equal-spaced) {
+    display: flex;
+    justify-content: center;
+}
+
+.achievement :deep(.equal-spaced > *) {
+    margin: auto;
+}
 </style>
diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx
index baa86ca..cafaafa 100644
--- a/src/features/achievements/achievement.tsx
+++ b/src/features/achievements/achievement.tsx
@@ -1,4 +1,6 @@
+import { computed } from "@vue/reactivity";
 import { isArray } from "@vue/shared";
+import Select from "components/fields/Select.vue";
 import AchievementComponent from "features/achievements/Achievement.vue";
 import {
     CoercableComponent,
@@ -6,12 +8,14 @@ import {
     GatherProps,
     GenericComponent,
     getUniqueID,
+    jsx,
     OptionsFunc,
     Replace,
     setDefault,
     StyleValue,
     Visibility
 } from "features/feature";
+import { globalBus } from "game/events";
 import "game/notifications";
 import type { Persistent } from "game/persistence";
 import { persistent } from "game/persistence";
@@ -19,10 +23,12 @@ import player from "game/player";
 import {
     createBooleanRequirement,
     createVisibilityRequirement,
+    displayRequirements,
     Requirements,
     requirementsMet
 } from "game/requirements";
-import settings from "game/settings";
+import settings, { registerSettingField } from "game/settings";
+import { camelToTitle } from "util/common";
 import type {
     Computable,
     GetComputableType,
@@ -31,7 +37,7 @@ import type {
 } from "util/computed";
 import { processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
-import { coerceComponent } from "util/vue";
+import { coerceComponent, isCoercableComponent } from "util/vue";
 import { unref, watchEffect } from "vue";
 import { useToast } from "vue-toastification";
 
@@ -40,6 +46,15 @@ const toast = useToast();
 /** A symbol used to identify {@link Achievement} features. */
 export const AchievementType = Symbol("Achievement");
 
+/** Modes for only displaying some achievements. */
+export enum AchievementDisplay {
+    All = "all",
+    //Last = "last",
+    Configurable = "configurable",
+    Incomplete = "incomplete",
+    None = "none"
+}
+
 /**
  * An object that configures an {@link Achievement}.
  */
@@ -49,15 +64,29 @@ export interface AchievementOptions {
     /** The requirement(s) to earn this achievement. Can be left null if using {@link BaseAchievement.complete}. */
     requirements?: Requirements;
     /** The display to use for this achievement. */
-    display?: Computable<CoercableComponent>;
+    display?: Computable<
+        | CoercableComponent
+        | {
+              /** Description of the requirement(s) for this achievement. If unspecified then the requirements will be displayed automatically based on {@link requirements}. */
+              requirement?: CoercableComponent;
+              /** Description of what will change (if anything) for achieving this. */
+              effectDisplay?: CoercableComponent;
+              /** Any additional things to display on this achievement, such as a toggle for it's effect. */
+              optionsDisplay?: CoercableComponent;
+          }
+    >;
     /** Shows a marker on the corner of the feature. */
     mark?: Computable<boolean | string>;
+    /** Toggles a smaller design for the feature. */
+    small?: Computable<boolean>;
     /** An image to display as the background for this achievement. */
     image?: Computable<string>;
     /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
     /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** Whether or not to display a notification popup when this achievement is earned. */
+    showPopups?: Computable<boolean>;
     /** A function that is called when the achievement is completed. */
     onComplete?: VoidFunction;
 }
@@ -90,6 +119,7 @@ export type Achievement<T extends AchievementOptions> = Replace<
         image: GetComputableType<T["image"]>;
         style: GetComputableType<T["style"]>;
         classes: GetComputableType<T["classes"]>;
+        showPopups: GetComputableTypeWithDefault<T["showPopups"], true>;
     }
 >;
 
@@ -98,6 +128,7 @@ export type GenericAchievement = Replace<
     Achievement<AchievementOptions>,
     {
         visibility: ProcessedComputable<Visibility | boolean>;
+        showPopups: ProcessedComputable<boolean>;
     }
 >;
 
@@ -120,8 +151,19 @@ export function createAchievement<T extends AchievementOptions>(
             earned.value = true;
             const genericAchievement = achievement as GenericAchievement;
             genericAchievement.onComplete?.();
-            if (genericAchievement.display != null) {
-                const Display = coerceComponent(unref(genericAchievement.display));
+            if (
+                genericAchievement.display != null &&
+                unref(genericAchievement.showPopups) === true
+            ) {
+                const display = unref(genericAchievement.display);
+                let Display;
+                if (isCoercableComponent(display)) {
+                    Display = coerceComponent(display);
+                } else if (display.requirement != null) {
+                    Display = coerceComponent(display.requirement);
+                } else {
+                    Display = displayRequirements(genericAchievement.requirements ?? []);
+                }
                 toast.info(
                     <div>
                         <h3>Achievement earned!</h3>
@@ -137,15 +179,69 @@ export function createAchievement<T extends AchievementOptions>(
 
         processComputable(achievement as T, "visibility");
         setDefault(achievement, "visibility", Visibility.Visible);
+        const visibility = achievement.visibility as ProcessedComputable<Visibility | boolean>;
+        achievement.visibility = computed(() => {
+            const display = unref((achievement as GenericAchievement).display);
+            switch (settings.msDisplay) {
+                default:
+                case AchievementDisplay.All:
+                    return unref(visibility);
+                case AchievementDisplay.Configurable:
+                    if (
+                        unref(achievement.earned) &&
+                        !(
+                            display != null &&
+                            typeof display == "object" &&
+                            "optionsDisplay" in (display as Record<string, unknown>)
+                        )
+                    ) {
+                        return Visibility.None;
+                    }
+                    return unref(visibility);
+                case AchievementDisplay.Incomplete:
+                    if (unref(achievement.earned)) {
+                        return Visibility.None;
+                    }
+                    return unref(visibility);
+                case AchievementDisplay.None:
+                    return Visibility.None;
+            }
+        });
+
         processComputable(achievement as T, "display");
         processComputable(achievement as T, "mark");
+        processComputable(achievement as T, "small");
         processComputable(achievement as T, "image");
         processComputable(achievement as T, "style");
         processComputable(achievement as T, "classes");
+        processComputable(achievement as T, "showPopups");
+        setDefault(achievement, "showPopups", true);
 
         achievement[GatherProps] = function (this: GenericAchievement) {
-            const { visibility, display, earned, image, style, classes, mark, id } = this;
-            return { visibility, display, earned, image, style: unref(style), classes, mark, id };
+            const {
+                visibility,
+                display,
+                requirements,
+                earned,
+                image,
+                style,
+                classes,
+                mark,
+                small,
+                id
+            } = this;
+            return {
+                visibility,
+                display,
+                requirements,
+                earned,
+                image,
+                style: unref(style),
+                classes,
+                mark,
+                small,
+                id
+            };
         };
 
         if (achievement.requirements) {
@@ -168,3 +264,34 @@ export function createAchievement<T extends AchievementOptions>(
         return achievement as unknown as Achievement<T>;
     });
 }
+
+declare module "game/settings" {
+    interface Settings {
+        msDisplay: AchievementDisplay;
+    }
+}
+
+globalBus.on("loadSettings", settings => {
+    setDefault(settings, "msDisplay", AchievementDisplay.All);
+});
+
+const msDisplayOptions = Object.values(AchievementDisplay).map(option => ({
+    label: camelToTitle(option),
+    value: option
+}));
+
+registerSettingField(
+    jsx(() => (
+        <Select
+            title={jsx(() => (
+                <span class="option-title">
+                    Show achievements
+                    <desc>Select which achievements to display based on criterias.</desc>
+                </span>
+            ))}
+            options={msDisplayOptions}
+            onUpdate:modelValue={value => (settings.msDisplay = value as AchievementDisplay)}
+            modelValue={settings.msDisplay}
+        />
+    ))
+);
diff --git a/src/features/milestones/Milestone.vue b/src/features/milestones/Milestone.vue
deleted file mode 100644
index e5dac97..0000000
--- a/src/features/milestones/Milestone.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-<template>
-    <div
-        v-if="isVisible(visibility)"
-        :style="[
-            {
-                visibility: isHidden(visibility) ? 'hidden' : undefined
-            },
-            unref(style) ?? {}
-        ]"
-        :class="{ feature: true, milestone: true, done: unref(earned), ...unref(classes) }"
-    >
-        <component :is="unref(comp)" />
-        <Node :id="id" />
-    </div>
-</template>
-
-<script lang="tsx">
-import "components/common/features.css";
-import Node from "components/Node.vue";
-import type { StyleValue } from "features/feature";
-import { isHidden, isVisible, jsx, Visibility } from "features/feature";
-import type { GenericMilestone } from "features/milestones/milestone";
-import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
-import type { Component, UnwrapRef } from "vue";
-import { defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
-
-export default defineComponent({
-    props: {
-        visibility: {
-            type: processedPropType<Visibility | boolean>(Number, Boolean),
-            required: true
-        },
-        display: {
-            type: processedPropType<UnwrapRef<GenericMilestone["display"]>>(
-                String,
-                Object,
-                Function
-            ),
-            required: true
-        },
-        style: processedPropType<StyleValue>(String, Object, Array),
-        classes: processedPropType<Record<string, boolean>>(Object),
-        earned: {
-            type: processedPropType<boolean>(Boolean),
-            required: true
-        },
-        id: {
-            type: String,
-            required: true
-        }
-    },
-    components: {
-        Node
-    },
-    setup(props) {
-        const { display } = toRefs(props);
-
-        const comp = shallowRef<Component | string>("");
-
-        watchEffect(() => {
-            const currDisplay = unwrapRef(display);
-            if (currDisplay == null) {
-                comp.value = "";
-                return;
-            }
-            if (isCoercableComponent(currDisplay)) {
-                comp.value = coerceComponent(currDisplay);
-                return;
-            }
-            const Requirement = coerceComponent(currDisplay.requirement, "h3");
-            const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
-            const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
-            comp.value = coerceComponent(
-                jsx(() => (
-                    <span>
-                        <Requirement />
-                        {currDisplay.effectDisplay != null ? (
-                            <div>
-                                <EffectDisplay />
-                            </div>
-                        ) : null}
-                        {currDisplay.optionsDisplay != null ? (
-                            <div class="equal-spaced">
-                                <OptionsDisplay />
-                            </div>
-                        ) : null}
-                    </span>
-                ))
-            );
-        });
-
-        return {
-            comp,
-            unref,
-            Visibility,
-            isVisible,
-            isHidden
-        };
-    }
-});
-</script>
-
-<style scoped>
-.milestone {
-    width: calc(100% - 10px);
-    min-width: 120px;
-    padding-left: 5px;
-    padding-right: 5px;
-    background-color: var(--locked);
-    border-width: 4px;
-    border-radius: 5px;
-    color: rgba(0, 0, 0, 0.5);
-}
-
-.milestone.done {
-    background-color: var(--bought);
-    cursor: default;
-}
-
-.milestone :deep(.equal-spaced) {
-    display: flex;
-    justify-content: center;
-}
-
-.milestone :deep(.equal-spaced > *) {
-    margin: auto;
-}
-</style>
diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx
deleted file mode 100644
index 0a973db..0000000
--- a/src/features/milestones/milestone.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import Select from "components/fields/Select.vue";
-import type {
-    CoercableComponent,
-    GenericComponent,
-    OptionsFunc,
-    Replace,
-    StyleValue
-} from "features/feature";
-import {
-    Component,
-    GatherProps,
-    getUniqueID,
-    isVisible,
-    jsx,
-    setDefault,
-    Visibility
-} from "features/feature";
-import MilestoneComponent from "features/milestones/Milestone.vue";
-import { globalBus } from "game/events";
-import "game/notifications";
-import type { Persistent } from "game/persistence";
-import { persistent } from "game/persistence";
-import player from "game/player";
-import settings, { registerSettingField } from "game/settings";
-import { camelToTitle } from "util/common";
-import type {
-    Computable,
-    GetComputableType,
-    GetComputableTypeWithDefault,
-    ProcessedComputable
-} from "util/computed";
-import { processComputable } from "util/computed";
-import { createLazyProxy } from "util/proxies";
-import { coerceComponent, isCoercableComponent } from "util/vue";
-import { computed, unref, watchEffect } from "vue";
-import { useToast } from "vue-toastification";
-
-const toast = useToast();
-
-export const MilestoneType = Symbol("Milestone");
-
-export enum MilestoneDisplay {
-    All = "all",
-    //Last = "last",
-    Configurable = "configurable",
-    Incomplete = "incomplete",
-    None = "none"
-}
-
-export interface MilestoneOptions {
-    visibility?: Computable<Visibility | boolean>;
-    shouldEarn?: () => boolean;
-    style?: Computable<StyleValue>;
-    classes?: Computable<Record<string, boolean>>;
-    display?: Computable<
-        | CoercableComponent
-        | {
-              requirement: CoercableComponent;
-              effectDisplay?: CoercableComponent;
-              optionsDisplay?: CoercableComponent;
-          }
-    >;
-    showPopups?: Computable<boolean>;
-    onComplete?: VoidFunction;
-}
-
-export interface BaseMilestone {
-    id: string;
-    earned: Persistent<boolean>;
-    complete: VoidFunction;
-    type: typeof MilestoneType;
-    [Component]: GenericComponent;
-    [GatherProps]: () => Record<string, unknown>;
-}
-
-export type Milestone<T extends MilestoneOptions> = Replace<
-    T & BaseMilestone,
-    {
-        visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
-        style: GetComputableType<T["style"]>;
-        classes: GetComputableType<T["classes"]>;
-        display: GetComputableType<T["display"]>;
-        showPopups: GetComputableType<T["showPopups"]>;
-    }
->;
-
-export type GenericMilestone = Replace<
-    Milestone<MilestoneOptions>,
-    {
-        visibility: ProcessedComputable<Visibility | boolean>;
-    }
->;
-
-export function createMilestone<T extends MilestoneOptions>(
-    optionsFunc?: OptionsFunc<T, BaseMilestone, GenericMilestone>
-): Milestone<T> {
-    const earned = persistent<boolean>(false, false);
-    return createLazyProxy(() => {
-        const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
-        milestone.id = getUniqueID("milestone-");
-        milestone.type = MilestoneType;
-        milestone[Component] = MilestoneComponent as GenericComponent;
-
-        milestone.earned = earned;
-        milestone.complete = function () {
-            const genericMilestone = milestone as GenericMilestone;
-            earned.value = true;
-            genericMilestone.onComplete?.();
-            if (genericMilestone.display != null && unref(genericMilestone.showPopups) === true) {
-                const display = unref(genericMilestone.display);
-                const Display = coerceComponent(
-                    isCoercableComponent(display) ? display : display.requirement
-                );
-                toast(
-                    <>
-                        <h3>Milestone earned!</h3>
-                        <div>
-                            {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
-                            {/* @ts-ignore */}
-                            <Display />
-                        </div>
-                    </>
-                );
-            }
-        };
-
-        processComputable(milestone as T, "visibility");
-        setDefault(milestone, "visibility", Visibility.Visible);
-        const visibility = milestone.visibility as ProcessedComputable<Visibility | boolean>;
-        milestone.visibility = computed(() => {
-            const display = unref((milestone as GenericMilestone).display);
-            switch (settings.msDisplay) {
-                default:
-                case MilestoneDisplay.All:
-                    return unref(visibility);
-                case MilestoneDisplay.Configurable:
-                    if (
-                        unref(milestone.earned) &&
-                        !(
-                            display != null &&
-                            typeof display == "object" &&
-                            "optionsDisplay" in (display as Record<string, unknown>)
-                        )
-                    ) {
-                        return Visibility.None;
-                    }
-                    return unref(visibility);
-                case MilestoneDisplay.Incomplete:
-                    if (unref(milestone.earned)) {
-                        return Visibility.None;
-                    }
-                    return unref(visibility);
-                case MilestoneDisplay.None:
-                    return Visibility.None;
-            }
-        });
-
-        processComputable(milestone as T, "style");
-        processComputable(milestone as T, "classes");
-        processComputable(milestone as T, "display");
-        processComputable(milestone as T, "showPopups");
-
-        milestone[GatherProps] = function (this: GenericMilestone) {
-            const { visibility, display, style, classes, earned, id } = this;
-            return { visibility, display, style: unref(style), classes, earned, id };
-        };
-
-        if (milestone.shouldEarn) {
-            const genericMilestone = milestone as GenericMilestone;
-            watchEffect(() => {
-                if (settings.active !== player.id) return;
-                if (
-                    !genericMilestone.earned.value &&
-                    isVisible(genericMilestone.visibility) &&
-                    genericMilestone.shouldEarn?.()
-                ) {
-                    genericMilestone.earned.value = true;
-                    genericMilestone.onComplete?.();
-                    if (
-                        genericMilestone.display != null &&
-                        unref(genericMilestone.showPopups) === true
-                    ) {
-                        const display = unref(genericMilestone.display);
-                        const Display = coerceComponent(
-                            isCoercableComponent(display) ? display : display.requirement
-                        );
-                        toast(
-                            <>
-                                <h3>Milestone earned!</h3>
-                                <div>
-                                    {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
-                                    {/* @ts-ignore */}
-                                    <Display />
-                                </div>
-                            </>
-                        );
-                    }
-                }
-            });
-        }
-
-        return milestone as unknown as Milestone<T>;
-    });
-}
-
-declare module "game/settings" {
-    interface Settings {
-        msDisplay: MilestoneDisplay;
-    }
-}
-
-globalBus.on("loadSettings", settings => {
-    setDefault(settings, "msDisplay", MilestoneDisplay.All);
-});
-
-const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({
-    label: camelToTitle(option),
-    value: option
-}));
-
-registerSettingField(
-    jsx(() => (
-        <Select
-            title={jsx(() => (
-                <span class="option-title">
-                    Show milestones
-                    <desc>Select which milestones to display based on criterias.</desc>
-                </span>
-            ))}
-            options={msDisplayOptions}
-            onUpdate:modelValue={value => (settings.msDisplay = value as MilestoneDisplay)}
-            modelValue={settings.msDisplay}
-        />
-    ))
-);

From 2b1250fb3816492ba7d0e7bb018e2e31b3df35af Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 3 Apr 2023 00:39:19 -0500
Subject: [PATCH 33/36] Fix build issue

---
 src/features/achievements/Achievement.vue | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/src/features/achievements/Achievement.vue b/src/features/achievements/Achievement.vue
index e0ffe6d..e615244 100644
--- a/src/features/achievements/Achievement.vue
+++ b/src/features/achievements/Achievement.vue
@@ -27,12 +27,10 @@
 import "components/common/features.css";
 import MarkNode from "components/MarkNode.vue";
 import Node from "components/Node.vue";
-import { CoercableComponent, jsx } from "features/feature";
-import { Visibility, isHidden, isVisible } from "features/feature";
+import { isHidden, isVisible, jsx, Visibility } from "features/feature";
 import { displayRequirements, Requirements } from "game/requirements";
-import { coerceComponent, computeOptionalComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
-import { Component, shallowRef, StyleValue, UnwrapRef, watchEffect } from "vue";
-import { defineComponent, toRefs, unref } from "vue";
+import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
+import { Component, defineComponent, shallowRef, StyleValue, toRefs, unref, UnwrapRef, watchEffect } from "vue";
 import { GenericAchievement } from "./achievement";
 
 export default defineComponent({
@@ -76,7 +74,7 @@ export default defineComponent({
                 comp.value = coerceComponent(currDisplay);
                 return;
             }
-            const Requirement = currDisplay.requirement ? coerceComponent(currDisplay.requirement, "h3") : displayRequirements(unwrapRef(requirements) ?? []);
+            const Requirement = coerceComponent(currDisplay.requirement ? currDisplay.requirement : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
             const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
             const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
             comp.value = coerceComponent(

From 61bc53f9545db92aebb3f29021b5c28c3c61af43 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 3 Apr 2023 08:15:16 -0500
Subject: [PATCH 34/36] Large achievement styling changes

---
 src/features/achievements/Achievement.vue | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/features/achievements/Achievement.vue b/src/features/achievements/Achievement.vue
index e615244..5bc1103 100644
--- a/src/features/achievements/Achievement.vue
+++ b/src/features/achievements/Achievement.vue
@@ -12,7 +12,7 @@
             feature: true,
             achievement: true,
             locked: !unref(earned),
-            bought: unref(earned),
+            done: unref(earned),
             small: unref(small),
             ...unref(classes)
         }"
@@ -60,7 +60,7 @@ export default defineComponent({
         MarkNode
     },
     setup(props) {
-        const { display, requirements } = toRefs(props);
+        const { display, requirements, earned } = toRefs(props);
 
         const comp = shallowRef<Component | string>("");
 
@@ -76,7 +76,9 @@ export default defineComponent({
             }
             const Requirement = coerceComponent(currDisplay.requirement ? currDisplay.requirement : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
             const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "", "b");
-            const OptionsDisplay = coerceComponent(currDisplay.optionsDisplay || "", "span");
+            const OptionsDisplay = unwrapRef(earned) ?
+                coerceComponent(currDisplay.optionsDisplay || "", "span") :
+                "";
             comp.value = coerceComponent(
                 jsx(() => (
                     <span>
@@ -117,6 +119,7 @@ export default defineComponent({
 }
 
 .achievement:not(.small) {
+    height: unset;
     width: calc(100% - 10px);
     min-width: 120px;
     padding-left: 5px;

From 44a5b336d665401fd45f6f1aca35a1bab2adfb2c Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 3 Apr 2023 08:19:58 -0500
Subject: [PATCH 35/36] Make challenges display requirements

---
 src/features/challenges/Challenge.vue | 16 ++++++++--------
 src/features/challenges/challenge.tsx |  8 +++++---
 src/features/repeatable.tsx           |  2 +-
 3 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/src/features/challenges/Challenge.vue b/src/features/challenges/Challenge.vue
index f64f203..194173c 100644
--- a/src/features/challenges/Challenge.vue
+++ b/src/features/challenges/Challenge.vue
@@ -38,6 +38,7 @@ import type { GenericChallenge } from "features/challenges/challenge";
 import type { StyleValue } from "features/feature";
 import { isHidden, isVisible, jsx, Visibility } from "features/feature";
 import { getHighNotifyStyle, getNotifyStyle } from "game/notifications";
+import { displayRequirements, Requirements } from "game/requirements";
 import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
 import type { Component, PropType, UnwrapRef } from "vue";
 import { computed, defineComponent, shallowRef, toRefs, unref, watchEffect } from "vue";
@@ -61,6 +62,7 @@ export default defineComponent({
             Object,
             Function
         ),
+        requirements: processedPropType<Requirements>(Object, Array),
         visibility: {
             type: processedPropType<Visibility | boolean>(Number, Boolean),
             required: true
@@ -90,7 +92,7 @@ export default defineComponent({
         Node
     },
     setup(props) {
-        const { active, maxed, canComplete, display } = toRefs(props);
+        const { active, maxed, canComplete, display, requirements } = toRefs(props);
 
         const buttonText = computed(() => {
             if (active.value) {
@@ -128,7 +130,7 @@ export default defineComponent({
             }
             const Title = coerceComponent(currDisplay.title || "", "h3");
             const Description = coerceComponent(currDisplay.description, "div");
-            const Goal = coerceComponent(currDisplay.goal || "");
+            const Goal = coerceComponent(currDisplay.goal != null ? currDisplay.goal : jsx(() => displayRequirements(unwrapRef(requirements) ?? [])), "h3");
             const Reward = coerceComponent(currDisplay.reward || "");
             const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
             comp.value = coerceComponent(
@@ -140,12 +142,10 @@ export default defineComponent({
                             </div>
                         ) : null}
                         <Description />
-                        {currDisplay.goal != null ? (
-                            <div>
-                                <br />
-                                Goal: <Goal />
-                            </div>
-                        ) : null}
+                        <div>
+                            <br />
+                            Goal: <Goal />
+                        </div>
                         {currDisplay.reward != null ? (
                             <div>
                                 <br />
diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index 3a1ba48..c1685db 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -69,7 +69,7 @@ export interface ChallengeOptions {
               title?: CoercableComponent;
               /** The main text that appears in the display. */
               description: CoercableComponent;
-              /** A description of the current goal for this challenge. */
+              /** A description of the current goal for this challenge. If unspecified then the requirements will be displayed automatically based on {@link requirements}.  */
               goal?: CoercableComponent;
               /** A description of what will change upon completing this challenge. */
               reward?: CoercableComponent;
@@ -271,7 +271,8 @@ export function createChallenge<T extends ChallengeOptions>(
                 canStart,
                 mark,
                 id,
-                toggle
+                toggle,
+                requirements
             } = this;
             return {
                 active,
@@ -285,7 +286,8 @@ export function createChallenge<T extends ChallengeOptions>(
                 canStart,
                 mark,
                 id,
-                toggle
+                toggle,
+                requirements
             };
         };
 
diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx
index ec338eb..df5d0be 100644
--- a/src/features/repeatable.tsx
+++ b/src/features/repeatable.tsx
@@ -42,7 +42,7 @@ export type RepeatableDisplay =
           title?: CoercableComponent;
           /** The main text that appears in the display. */
           description?: CoercableComponent;
-          /** A description of the current effect of this repeatable, bsed off its amount. */
+          /** A description of the current effect of this repeatable, based off its amount. */
           effectDisplay?: CoercableComponent;
           /** Whether or not to show the current amount of this repeatable at the bottom of the display. */
           showAmount?: boolean;

From 3ad0d6459005ec634f7ede00b588c6a5010597fc Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 4 Apr 2023 00:02:23 -0500
Subject: [PATCH 36/36] Documented the rest of the features Not the vue
 components though

---
 src/features/achievements/achievement.tsx |  6 +-
 src/features/action.tsx                   | 25 +++++++
 src/features/bars/bar.ts                  | 30 ++++++++
 src/features/boards/board.ts              | 89 +++++++++++++++++++++++
 src/features/challenges/challenge.tsx     |  2 +-
 src/features/clickables/clickable.ts      | 34 +++++++++
 src/features/grids/grid.ts                | 62 ++++++++++++++++
 src/features/hotkey.tsx                   | 19 +++++
 src/features/infoboxes/infobox.ts         | 26 +++++++
 src/features/links/links.ts               | 16 ++++
 src/features/particles/particles.tsx      | 30 ++++++++
 src/features/repeatable.tsx               |  2 +-
 src/features/reset.ts                     | 23 ++++++
 src/features/resources/resource.ts        | 16 ++++
 src/features/tabs/tab.ts                  | 23 ++++++
 src/features/tabs/tabFamily.ts            | 48 ++++++++++++
 src/features/tooltips/tooltip.ts          | 20 +++++
 src/features/trees/tree.ts                | 62 ++++++++++++++++
 src/features/upgrades/upgrade.ts          | 38 +++++++++-
 19 files changed, 565 insertions(+), 6 deletions(-)

diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx
index cafaafa..3f2a7f3 100644
--- a/src/features/achievements/achievement.tsx
+++ b/src/features/achievements/achievement.tsx
@@ -95,7 +95,7 @@ export interface AchievementOptions {
  * The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}.
  */
 export interface BaseAchievement {
-    /** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
     /** Whether or not this achievement has been earned. */
     earned: Persistent<boolean>;
@@ -109,7 +109,7 @@ export interface BaseAchievement {
     [GatherProps]: () => Record<string, unknown>;
 }
 
-/** An object that represents a feature with that is passively earned upon meeting certain requirements. */
+/** An object that represents a feature with requirements that is passively earned upon meeting certain requirements. */
 export type Achievement<T extends AchievementOptions> = Replace<
     T & BaseAchievement,
     {
@@ -133,7 +133,7 @@ export type GenericAchievement = Replace<
 >;
 
 /**
- * Lazily creates a achievement with the given options.
+ * Lazily creates an achievement with the given options.
  * @param optionsFunc Achievement options.
  */
 export function createAchievement<T extends AchievementOptions>(
diff --git a/src/features/action.tsx b/src/features/action.tsx
index 5d7e555..afd8021 100644
--- a/src/features/action.tsx
+++ b/src/features/action.tsx
@@ -32,26 +32,46 @@ import { computed, Ref, ref, unref } from "vue";
 import { BarOptions, createBar, GenericBar } from "./bars/bar";
 import { ClickableOptions } from "./clickables/clickable";
 
+/** A symbol used to identify {@link Action} features. */
 export const ActionType = Symbol("Action");
 
+/**
+ * An object that configures a {@link Action}.
+ */
 export interface ActionOptions extends Omit<ClickableOptions, "onClick" | "onHold"> {
+    /** The cooldown during which the action cannot be performed again, in seconds. */
     duration: Computable<DecimalSource>;
+    /** Whether or not the action should perform automatically when the cooldown is finished. */
     autoStart?: Computable<boolean>;
+    /** A function that is called when the action is clicked. */
     onClick: (amount: DecimalSource) => void;
+    /** A pass-through to the {@link Bar} used to display the cooldown progress for the action. */
     barOptions?: Partial<BarOptions>;
 }
 
+/**
+ * The properties that are added onto a processed {@link ActionOptions} to create an {@link Action}.
+ */
 export interface BaseAction {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** A symbol that helps identify features of the same type. */
     type: typeof ActionType;
+    /** Whether or not the player is holding down the action. Actions will be considered clicked as soon as the cooldown completes when being held down. */
     isHolding: Ref<boolean>;
+    /** The current amount of progress through the cooldown. */
     progress: Ref<DecimalSource>;
+    /** The bar used to display the current cooldown progress. */
     progressBar: GenericBar;
+    /** Update the cooldown the specified number of seconds */
     update: (diff: number) => void;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represens a feature that can be clicked upon, and then have a cooldown before they can be clicked again. */
 export type Action<T extends ActionOptions> = Replace<
     T & BaseAction,
     {
@@ -67,6 +87,7 @@ export type Action<T extends ActionOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Action} object. */
 export type GenericAction = Replace<
     Action<ActionOptions>,
     {
@@ -76,6 +97,10 @@ export type GenericAction = Replace<
     }
 >;
 
+/**
+ * Lazily creates an action with the given options.
+ * @param optionsFunc Action options.
+ */
 export function createAction<T extends ActionOptions>(
     optionsFunc?: OptionsFunc<T, BaseAction, GenericAction>
 ): Action<T> {
diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts
index bab5887..0a6433c 100644
--- a/src/features/bars/bar.ts
+++ b/src/features/bars/bar.ts
@@ -19,31 +19,56 @@ import { processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 import { unref } from "vue";
 
+/** A symbol used to identify {@link Bar} features. */
 export const BarType = Symbol("Bar");
 
+/**
+ * An object that configures a {@link Bar}.
+ */
 export interface BarOptions {
+    /** Whether this bar should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The width of the bar. */
     width: Computable<number>;
+    /** The height of the bar. */
     height: Computable<number>;
+    /** The direction in which the bar progresses. */
     direction: Computable<Direction>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to the bar's border. */
     borderStyle?: Computable<StyleValue>;
+    /** CSS to apply to the bar's base. */
     baseStyle?: Computable<StyleValue>;
+    /** CSS to apply to the bar's text. */
     textStyle?: Computable<StyleValue>;
+    /** CSS to apply to the bar's fill. */
     fillStyle?: Computable<StyleValue>;
+    /** The progress value of the bar, from 0 to 1. */
     progress: Computable<DecimalSource>;
+    /** The display to use for this bar. */
     display?: Computable<CoercableComponent>;
+    /** Shows a marker on the corner of the feature. */
     mark?: Computable<boolean | string>;
 }
 
+/**
+ * The properties that are added onto a processed {@link BarOptions} to create a {@link Bar}.
+ */
 export interface BaseBar {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** A symbol that helps identify features of the same type. */
     type: typeof BarType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that displays some sort of progress or completion or resource with a cap. */
 export type Bar<T extends BarOptions> = Replace<
     T & BaseBar,
     {
@@ -63,6 +88,7 @@ export type Bar<T extends BarOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Bar} object. */
 export type GenericBar = Replace<
     Bar<BarOptions>,
     {
@@ -70,6 +96,10 @@ export type GenericBar = Replace<
     }
 >;
 
+/**
+ * Lazily creates a bar with the given options.
+ * @param optionsFunc Bar options.
+ */
 export function createBar<T extends BarOptions>(
     optionsFunc: OptionsFunc<T, BaseBar, GenericBar>
 ): Bar<T> {
diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts
index 528bc26..b8f66b4 100644
--- a/src/features/boards/board.ts
+++ b/src/features/boards/board.ts
@@ -27,20 +27,27 @@ import type { Link } from "../links/links";
 
 globalBus.on("setupVue", app => panZoom.install(app));
 
+/** A symbol used to identify {@link Board} features. */
 export const BoardType = Symbol("Board");
 
+/**
+ * A type representing a computable value for a node on the board. Used for node types to return different values based on the given node and the state of the board.
+ */
 export type NodeComputable<T> = Computable<T> | ((node: BoardNode) => T);
 
+/** Ways to display progress of an action with a duration. */
 export enum ProgressDisplay {
     Outline = "Outline",
     Fill = "Fill"
 }
 
+/** Node shapes. */
 export enum Shape {
     Circle = "Circle",
     Diamond = "Triangle"
 }
 
+/** An object representing a node on the board. */
 export interface BoardNode {
     id: number;
     position: {
@@ -52,48 +59,76 @@ export interface BoardNode {
     pinned?: boolean;
 }
 
+/** An object representing a link between two nodes on the board. */
 export interface BoardNodeLink extends Omit<Link, "startNode" | "endNode"> {
     startNode: BoardNode;
     endNode: BoardNode;
     pulsing?: boolean;
 }
 
+/** An object representing a label for a node. */
 export interface NodeLabel {
     text: string;
     color?: string;
     pulsing?: boolean;
 }
 
+/** The persistent data for a board. */
 export type BoardData = {
     nodes: BoardNode[];
     selectedNode: number | null;
     selectedAction: string | null;
 };
 
+/**
+ * An object that configures a {@link NodeType}.
+ */
 export interface NodeTypeOptions {
+    /** The title to display for the node. */
     title: NodeComputable<string>;
+    /** An optional label for the node. */
     label?: NodeComputable<NodeLabel | null>;
+    /** The size of the node - diameter for circles, width and height for squares. */
     size: NodeComputable<number>;
+    /** Whether the node is draggable or not. */
     draggable?: NodeComputable<boolean>;
+    /** The shape of the node. */
     shape: NodeComputable<Shape>;
+    /** Whether the node can accept another node being dropped upon it. */
     canAccept?: boolean | Ref<boolean> | ((node: BoardNode, otherNode: BoardNode) => boolean);
+    /** The progress value of the node. */
     progress?: NodeComputable<number>;
+    /** How the progress should be displayed on the node. */
     progressDisplay?: NodeComputable<ProgressDisplay>;
+    /** The color of the progress indicator. */
     progressColor?: NodeComputable<string>;
+    /** The fill color of the node. */
     fillColor?: NodeComputable<string>;
+    /** The outline color of the node. */
     outlineColor?: NodeComputable<string>;
+    /** The color of the title text. */
     titleColor?: NodeComputable<string>;
+    /** The list of action options for the node. */
     actions?: BoardNodeActionOptions[];
+    /** The distance between the center of the node and its actions. */
     actionDistance?: NodeComputable<number>;
+    /** A function that is called when the node is clicked. */
     onClick?: (node: BoardNode) => void;
+    /** A function that is called when a node is dropped onto this node. */
     onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
+    /** A function that is called for each node of this type every tick. */
     update?: (node: BoardNode, diff: number) => void;
 }
 
+/**
+ * The properties that are added onto a processed {@link NodeTypeOptions} to create a {@link NodeType}.
+ */
 export interface BaseNodeType {
+    /** The nodes currently on the board of this type. */
     nodes: Ref<BoardNode[]>;
 }
 
+/** An object that represents a type of node that can appear on a board. It will handle getting properties and callbacks for every node of that type. */
 export type NodeType<T extends NodeTypeOptions> = Replace<
     T & BaseNodeType,
     {
@@ -114,6 +149,7 @@ export type NodeType<T extends NodeTypeOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link NodeType} object. */
 export type GenericNodeType = Replace<
     NodeType<NodeTypeOptions>,
     {
@@ -127,20 +163,34 @@ export type GenericNodeType = Replace<
     }
 >;
 
+/**
+ * An object that configures a {@link BoardNodeAction}.
+ */
 export interface BoardNodeActionOptions {
+    /** A unique identifier for the action. */
     id: string;
+    /** Whether this action should be visible. */
     visibility?: NodeComputable<Visibility | boolean>;
+    /** The icon to display for the action. */
     icon: NodeComputable<string>;
+    /** The fill color of the action. */
     fillColor?: NodeComputable<string>;
+    /** The tooltip text to display for the action. */
     tooltip: NodeComputable<string>;
+    /** An array of board node links associated with the action. They appear when the action is focused. */
     links?: NodeComputable<BoardNodeLink[]>;
+    /** A function that is called when the action is clicked. */
     onClick: (node: BoardNode) => boolean | undefined;
 }
 
+/**
+ * The properties that are added onto a processed {@link BoardNodeActionOptions} to create an {@link BoardNodeAction}.
+ */
 export interface BaseBoardNodeAction {
     links?: Ref<BoardNodeLink[]>;
 }
 
+/** An object that represents an action that can be taken upon a node. */
 export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
     T & BaseBoardNodeAction,
     {
@@ -152,6 +202,7 @@ export type BoardNodeAction<T extends BoardNodeActionOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link BoardNodeAction} object. */
 export type GenericBoardNodeAction = Replace<
     BoardNodeAction<BoardNodeActionOptions>,
     {
@@ -159,29 +210,53 @@ export type GenericBoardNodeAction = Replace<
     }
 >;
 
+/**
+ * An object that configures a {@link Board}.
+ */
 export interface BoardOptions {
+    /** Whether this board should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The height of the board. Defaults to 100% */
     height?: Computable<string>;
+    /** The width of the board. Defaults to 100% */
     width?: Computable<string>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** A function that returns an array of initial board nodes, without IDs. */
     startNodes: () => Omit<BoardNode, "id">[];
+    /** A dictionary of node types that can appear on the board. */
     types: Record<string, NodeTypeOptions>;
+    /** The persistent state of the board. */
     state?: Computable<BoardData>;
+    /** An array of board node links to display. */
     links?: Computable<BoardNodeLink[] | null>;
 }
 
+/**
+ * The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
+ */
 export interface BaseBoard {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** All the nodes currently on the board. */
     nodes: Ref<BoardNode[]>;
+    /** The currently selected node, if any. */
     selectedNode: Ref<BoardNode | null>;
+    /** The currently selected action, if any. */
     selectedAction: Ref<GenericBoardNodeAction | null>;
+    /** The current mouse position, if over the board. */
     mousePosition: Ref<{ x: number; y: number } | null>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof BoardType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that is a zoomable, pannable board with various nodes upon it. */
 export type Board<T extends BoardOptions> = Replace<
     T & BaseBoard,
     {
@@ -196,6 +271,7 @@ export type Board<T extends BoardOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Board} object. */
 export type GenericBoard = Replace<
     Board<BoardOptions>,
     {
@@ -205,6 +281,10 @@ export type GenericBoard = Replace<
     }
 >;
 
+/**
+ * Lazily creates a board with the given options.
+ * @param optionsFunc Board options.
+ */
 export function createBoard<T extends BoardOptions>(
     optionsFunc: OptionsFunc<T, BaseBoard, GenericBoard>
 ): Board<T> {
@@ -368,10 +448,19 @@ export function createBoard<T extends BoardOptions>(
     });
 }
 
+/**
+ * Gets the value of a property for a specified node.
+ * @param property The property to find the value of
+ * @param node The node to get the property of
+ */
 export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {
     return isFunction<T, [BoardNode], Computable<T>>(property) ? property(node) : unref(property);
 }
 
+/**
+ * Utility to get an ID for a node that is guaranteed unique.
+ * @param board The board feature to generate an ID for
+ */
 export function getUniqueNodeID(board: GenericBoard): number {
     let id = 0;
     board.nodes.value.forEach(node => {
diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx
index c1685db..5b6231d 100644
--- a/src/features/challenges/challenge.tsx
+++ b/src/features/challenges/challenge.tsx
@@ -89,7 +89,7 @@ export interface ChallengeOptions {
  * The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}.
  */
 export interface BaseChallenge {
-    /** An auto-generated ID for identifying challenges that appear in the DOM. Will not persist between refreshes or updates. */
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
     /** The current amount of times this challenge can be completed. */
     canComplete: Ref<DecimalSource>;
diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts
index b44854b..d369603 100644
--- a/src/features/clickables/clickable.ts
+++ b/src/features/clickables/clickable.ts
@@ -19,33 +19,56 @@ import { processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 import { computed, unref } from "vue";
 
+/** A symbol used to identify {@link Clickable} features. */
 export const ClickableType = Symbol("Clickable");
 
+/**
+ * An object that configures a {@link Clickable}.
+ */
 export interface ClickableOptions {
+    /** Whether this clickable should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** Whether or not the clickable may be clicked. */
     canClick?: Computable<boolean>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** Shows a marker on the corner of the feature. */
     mark?: Computable<boolean | string>;
+    /** The display to use for this clickable. */
     display?: Computable<
         | CoercableComponent
         | {
+              /** A header to appear at the top of the display. */
               title?: CoercableComponent;
+              /** The main text that appears in the display. */
               description: CoercableComponent;
           }
     >;
+    /** Toggles a smaller design for the feature. */
     small?: boolean;
+    /** A function that is called when the clickable is clicked. */
     onClick?: (e?: MouseEvent | TouchEvent) => void;
+    /** A function that is called when the clickable is held down. */
     onHold?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link ClickableOptions} to create an {@link Clickable}.
+ */
 export interface BaseClickable {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** A symbol that helps identify features of the same type. */
     type: typeof ClickableType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that can be clicked or held down. */
 export type Clickable<T extends ClickableOptions> = Replace<
     T & BaseClickable,
     {
@@ -58,6 +81,7 @@ export type Clickable<T extends ClickableOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Clickable} object. */
 export type GenericClickable = Replace<
     Clickable<ClickableOptions>,
     {
@@ -66,6 +90,10 @@ export type GenericClickable = Replace<
     }
 >;
 
+/**
+ * Lazily creates a clickable with the given options.
+ * @param optionsFunc Clickable options.
+ */
 export function createClickable<T extends ClickableOptions>(
     optionsFunc?: OptionsFunc<T, BaseClickable, GenericClickable>
 ): Clickable<T> {
@@ -132,6 +160,12 @@ export function createClickable<T extends ClickableOptions>(
     });
 }
 
+/**
+ * Utility to auto click a clickable whenever it can be.
+ * @param layer The layer the clickable is apart of
+ * @param clickable The clicker to click automatically
+ * @param autoActive Whether or not the clickable should currently be auto-clicking
+ */
 export function setupAutoClick(
     layer: BaseLayer,
     clickable: GenericClickable,
diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts
index b7899ae..57eb399 100644
--- a/src/features/grids/grid.ts
+++ b/src/features/grids/grid.ts
@@ -21,14 +21,22 @@ import { createLazyProxy } from "util/proxies";
 import type { Ref } from "vue";
 import { computed, unref } from "vue";
 
+/** A symbol used to identify {@link Grid} features. */
 export const GridType = Symbol("Grid");
 
+/** A type representing a computable value for a cell in the grid. */
 export type CellComputable<T> = Computable<T> | ((id: string | number, state: State) => T);
 
+/** Create proxy to more easily get the properties of cells on a grid. */
 function createGridProxy(grid: GenericGrid): Record<string | number, GridCell> {
     return new Proxy({}, getGridHandler(grid)) as Record<string | number, GridCell>;
 }
 
+/**
+ * Returns traps for a proxy that will give cell proxies when accessing any numerical key.
+ * @param grid The grid to get the cells from.
+ * @see {@link createGridProxy}
+ */
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number, GridCell>> {
     const keys = computed(() => {
@@ -86,6 +94,12 @@ function getGridHandler(grid: GenericGrid): ProxyHandler<Record<string | number,
     };
 }
 
+/**
+ * Returns traps for a proxy that will get the properties for the specified cell
+ * @param id The grid cell ID to get properties from.
+ * @see {@link getGridHandler}
+ * @see {@link createGridProxy}
+ */
 function getCellHandler(id: string): ProxyHandler<GenericGrid> {
     const keys = [
         "id",
@@ -175,47 +189,90 @@ function getCellHandler(id: string): ProxyHandler<GenericGrid> {
     };
 }
 
+/**
+ * Represents a cell within a grid. These properties will typically be accessed via a cell proxy that calls functions on the grid to get the properties for a specific cell.
+ * @see {@link createGridProxy}
+ */
 export interface GridCell {
+    /** A unique identifier for the grid cell. */
     id: string;
+    /** Whether this cell should be visible. */
     visibility: Visibility | boolean;
+    /** Whether this cell can be clicked. */
     canClick: boolean;
+    /** The initial persistent state of this cell. */
     startState: State;
+    /** The persistent state of this cell. */
     state: State;
+    /** CSS to apply to this feature. */
     style?: StyleValue;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Record<string, boolean>;
+    /** A header to appear at the top of the display. */
     title?: CoercableComponent;
+    /** The main text that appears in the display. */
     display: CoercableComponent;
+    /** A function that is called when the cell is clicked. */
     onClick?: (e?: MouseEvent | TouchEvent) => void;
+    /** A function that is called when the cell is held down. */
     onHold?: VoidFunction;
 }
 
+/**
+ * An object that configures a {@link Grid}.
+ */
 export interface GridOptions {
+    /** Whether this grid should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The number of rows in the grid. */
     rows: Computable<number>;
+    /** The number of columns in the grid. */
     cols: Computable<number>;
+    /** A computable to determine the visibility of a cell. */
     getVisibility?: CellComputable<Visibility | boolean>;
+    /** A computable to determine if a cell can be clicked. */
     getCanClick?: CellComputable<boolean>;
+    /** A computable to get the initial persistent state of a cell. */
     getStartState: Computable<State> | ((id: string | number) => State);
+    /** A computable to get the CSS styles for a cell. */
     getStyle?: CellComputable<StyleValue>;
+    /** A computable to get the CSS classes for a cell. */
     getClasses?: CellComputable<Record<string, boolean>>;
+    /** A computable to get the title component for a cell. */
     getTitle?: CellComputable<CoercableComponent>;
+    /** A computable to get the display component for a cell. */
     getDisplay: CellComputable<CoercableComponent>;
+    /** A function that is called when a cell is clicked. */
     onClick?: (id: string | number, state: State, e?: MouseEvent | TouchEvent) => void;
+    /** A function that is called when a cell is held down. */
     onHold?: (id: string | number, state: State) => void;
 }
 
+/**
+ * The properties that are added onto a processed {@link BoardOptions} to create a {@link Board}.
+ */
 export interface BaseGrid {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */
     getID: (id: string | number, state: State) => string;
+    /** Get the persistent state of the given cell. */
     getState: (id: string | number) => State;
+    /** Set the persistent state of the given cell. */
     setState: (id: string | number, state: State) => void;
+    /** A dictionary of cells within this grid. */
     cells: Record<string | number, GridCell>;
+    /** The persistent state of this grid, which is a dictionary of cell states. */
     cellState: Persistent<Record<string | number, State>>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof GridType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */
 export type Grid<T extends GridOptions> = Replace<
     T & BaseGrid,
     {
@@ -232,6 +289,7 @@ export type Grid<T extends GridOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Grid} object. */
 export type GenericGrid = Replace<
     Grid<GridOptions>,
     {
@@ -241,6 +299,10 @@ export type GenericGrid = Replace<
     }
 >;
 
+/**
+ * Lazily creates a grid with the given options.
+ * @param optionsFunc Grid options.
+ */
 export function createGrid<T extends GridOptions>(
     optionsFunc: OptionsFunc<T, BaseGrid, GenericGrid>
 ): Grid<T> {
diff --git a/src/features/hotkey.tsx b/src/features/hotkey.tsx
index 595344d..dcfdade 100644
--- a/src/features/hotkey.tsx
+++ b/src/features/hotkey.tsx
@@ -15,20 +15,34 @@ import { createLazyProxy } from "util/proxies";
 import { shallowReactive, unref } from "vue";
 import Hotkey from "components/Hotkey.vue";
 
+/** A dictionary of all hotkeys. */
 export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
+/** A symbol used to identify {@link Hotkey} features. */
 export const HotkeyType = Symbol("Hotkey");
 
+/**
+ * An object that configures a {@link Hotkey}.
+ */
 export interface HotkeyOptions {
+    /** Whether or not this hotkey is currently enabled. */
     enabled?: Computable<boolean>;
+    /** The key tied to this hotkey */
     key: string;
+    /** The description of this hotkey, to display in the settings. */
     description: Computable<string>;
+    /** What to do upon pressing the key. */
     onPress: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link HotkeyOptions} to create an {@link Hotkey}.
+ */
 export interface BaseHotkey {
+    /** A symbol that helps identify features of the same type. */
     type: typeof HotkeyType;
 }
 
+/** An object that represents a hotkey shortcut that performs an action upon a key sequence being pressed. */
 export type Hotkey<T extends HotkeyOptions> = Replace<
     T & BaseHotkey,
     {
@@ -37,6 +51,7 @@ export type Hotkey<T extends HotkeyOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Hotkey} object. */
 export type GenericHotkey = Replace<
     Hotkey<HotkeyOptions>,
     {
@@ -46,6 +61,10 @@ export type GenericHotkey = Replace<
 
 const uppercaseNumbers = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
 
+/**
+ * Lazily creates a hotkey with the given options.
+ * @param optionsFunc Hotkey options.
+ */
 export function createHotkey<T extends HotkeyOptions>(
     optionsFunc: OptionsFunc<T, BaseHotkey, GenericHotkey>
 ): Hotkey<T> {
diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts
index ccd2bd3..f5c7a2f 100644
--- a/src/features/infoboxes/infobox.ts
+++ b/src/features/infoboxes/infobox.ts
@@ -19,27 +19,48 @@ import { processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 import { unref } from "vue";
 
+/** A symbol used to identify {@link Infobox} features. */
 export const InfoboxType = Symbol("Infobox");
 
+/**
+ * An object that configures an {@link Infobox}.
+ */
 export interface InfoboxOptions {
+    /** Whether this clickable should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The background color of the Infobox. */
     color?: Computable<string>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** CSS to apply to the title of the infobox. */
     titleStyle?: Computable<StyleValue>;
+    /** CSS to apply to the body of the infobox. */
     bodyStyle?: Computable<StyleValue>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** A header to appear at the top of the display. */
     title: Computable<CoercableComponent>;
+    /** The main text that appears in the display. */
     display: Computable<CoercableComponent>;
 }
 
+/**
+ * The properties that are added onto a processed {@link InfoboxOptions} to create an {@link Infobox}.
+ */
 export interface BaseInfobox {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** Whether or not this infobox is collapsed. */
     collapsed: Persistent<boolean>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof InfoboxType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that displays information in a collapsible way.  */
 export type Infobox<T extends InfoboxOptions> = Replace<
     T & BaseInfobox,
     {
@@ -54,6 +75,7 @@ export type Infobox<T extends InfoboxOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Infobox} object. */
 export type GenericInfobox = Replace<
     Infobox<InfoboxOptions>,
     {
@@ -61,6 +83,10 @@ export type GenericInfobox = Replace<
     }
 >;
 
+/**
+ * Lazily creates an infobox with the given options.
+ * @param optionsFunc Infobox options.
+ */
 export function createInfobox<T extends InfoboxOptions>(
     optionsFunc: OptionsFunc<T, BaseInfobox, GenericInfobox>
 ): Infobox<T> {
diff --git a/src/features/links/links.ts b/src/features/links/links.ts
index 77ea0ba..5b28c46 100644
--- a/src/features/links/links.ts
+++ b/src/features/links/links.ts
@@ -7,8 +7,10 @@ import { createLazyProxy } from "util/proxies";
 import type { SVGAttributes } from "vue";
 import LinksComponent from "./Links.vue";
 
+/** A symbol used to identify {@link Links} features. */
 export const LinksType = Symbol("Links");
 
+/** Represents a link between two nodes. It will be displayed as an SVG line, and can take any appropriate properties for an SVG line element. */
 export interface Link extends SVGAttributes {
     startNode: { id: string };
     endNode: { id: string };
@@ -16,16 +18,25 @@ export interface Link extends SVGAttributes {
     offsetEnd?: Position;
 }
 
+/** An object that configures a {@link Links}. */
 export interface LinksOptions {
+    /** The list of links to display. */
     links: Computable<Link[]>;
 }
 
+/**
+ * The properties that are added onto a processed {@link LinksOptions} to create an {@link Links}.
+ */
 export interface BaseLinks {
+    /** A symbol that helps identify features of the same type. */
     type: typeof LinksType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a list of links between nodes, which are the elements in the DOM for any renderable feature. */
 export type Links<T extends LinksOptions> = Replace<
     T & BaseLinks,
     {
@@ -33,6 +44,7 @@ export type Links<T extends LinksOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Links} object. */
 export type GenericLinks = Replace<
     Links<LinksOptions>,
     {
@@ -40,6 +52,10 @@ export type GenericLinks = Replace<
     }
 >;
 
+/**
+ * Lazily creates links with the given options.
+ * @param optionsFunc Links options.
+ */
 export function createLinks<T extends LinksOptions>(
     optionsFunc: OptionsFunc<T, BaseLinks, GenericLinks>
 ): Links<T> {
diff --git a/src/features/particles/particles.tsx b/src/features/particles/particles.tsx
index 0b5b23f..549ccee 100644
--- a/src/features/particles/particles.tsx
+++ b/src/features/particles/particles.tsx
@@ -8,24 +8,49 @@ import type { Computable, GetComputableType } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 import { Ref, shallowRef, unref } from "vue";
 
+/** A symbol used to identify {@link Particles} features. */
 export const ParticlesType = Symbol("Particles");
 
+/**
+ * An object that configures {@link Particles}.
+ */
 export interface ParticlesOptions {
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** A function that is called when the particles canvas is resized. */
     onContainerResized?: (boundingRect: DOMRect) => void;
+    /** A function that is called whenever the particles element is reloaded during development. For restarting particle effects. */
     onHotReload?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link ParticlesOptions} to create an {@link Particles}.
+ */
 export interface BaseParticles {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** The Pixi.JS Application powering this particles canvas. */
     app: Ref<null | Application>;
+    /**
+     * A function to asynchronously add an emitter to the canvas.
+     * The returned emitter can then be positioned as appropriate and started.
+     * @see {@link Particles}
+     */
     addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof ParticlesType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/**
+ * An object that represents a feature that display particle effects on the screen.
+ * The config should typically be gotten by designing the effect using the [online particle effect editor](https://pixijs.io/pixi-particles-editor/) and passing it into the {@link upgradeConfig} from @pixi/particle-emitter.
+ */
 export type Particles<T extends ParticlesOptions> = Replace<
     T & BaseParticles,
     {
@@ -34,8 +59,13 @@ export type Particles<T extends ParticlesOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Particles} object. */
 export type GenericParticles = Particles<ParticlesOptions>;
 
+/**
+ * Lazily creates particles with the given options.
+ * @param optionsFunc Particles options.
+ */
 export function createParticles<T extends ParticlesOptions>(
     optionsFunc?: OptionsFunc<T, BaseParticles, GenericParticles>
 ): Particles<T> {
diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx
index df5d0be..2598c19 100644
--- a/src/features/repeatable.tsx
+++ b/src/features/repeatable.tsx
@@ -76,7 +76,7 @@ export interface RepeatableOptions {
  * The properties that are added onto a processed {@link RepeatableOptions} to create a {@link Repeatable}.
  */
 export interface BaseRepeatable {
-    /** An auto-generated ID for identifying features that appear in the DOM. Will not persistent between refreshes or updates. */
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
     /** The current amount this repeatable has. */
     amount: Persistent<DecimalSource>;
diff --git a/src/features/reset.ts b/src/features/reset.ts
index 487238c..0bba943 100644
--- a/src/features/reset.ts
+++ b/src/features/reset.ts
@@ -11,19 +11,32 @@ import { processComputable } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 import { isRef, unref } from "vue";
 
+/** A symbol used to identify {@link Reset} features. */
 export const ResetType = Symbol("Reset");
 
+/**
+ * An object that configures a {@link Clickable}.
+ */
 export interface ResetOptions {
+    /** List of things to reset. Can include objects which will be recursed over for persistent values. */
     thingsToReset: Computable<Record<string, unknown>[]>;
+    /** A function that is called when the reset is performed. */
     onReset?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link ResetOptions} to create an {@link Reset}.
+ */
 export interface BaseReset {
+    /** An auto-generated ID for identifying which reset is being performed. Will not persist between refreshes or updates. */
     id: string;
+    /** Trigger the reset. */
     reset: VoidFunction;
+    /** A symbol that helps identify features of the same type. */
     type: typeof ResetType;
 }
 
+/** An object that represents a reset mechanic, which resets progress back to its initial state. */
 export type Reset<T extends ResetOptions> = Replace<
     T & BaseReset,
     {
@@ -31,8 +44,13 @@ export type Reset<T extends ResetOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Reset} object. */
 export type GenericReset = Reset<ResetOptions>;
 
+/**
+ * Lazily creates a reset with the given options.
+ * @param optionsFunc Reset options.
+ */
 export function createReset<T extends ResetOptions>(
     optionsFunc: OptionsFunc<T, BaseReset, GenericReset>
 ): Reset<T> {
@@ -66,6 +84,11 @@ export function createReset<T extends ResetOptions>(
 }
 
 const listeners: Record<string, Unsubscribe | undefined> = {};
+/**
+ * Track the time since the specified reset last occured.
+ * @param layer The layer the reset is attached to
+ * @param reset The reset mechanic to track the time since
+ */
 export function trackResetTime(layer: BaseLayer, reset: GenericReset): Persistent<Decimal> {
     const resetTime = persistent<Decimal>(new Decimal(0));
     globalBus.on("addLayer", layerBeingAdded => {
diff --git a/src/features/resources/resource.ts b/src/features/resources/resource.ts
index 8e203d3..0247a67 100644
--- a/src/features/resources/resource.ts
+++ b/src/features/resources/resource.ts
@@ -8,12 +8,23 @@ import { loadingSave } from "util/save";
 import type { ComputedRef, Ref } from "vue";
 import { computed, isRef, ref, unref, watch } from "vue";
 
+/** An object that represents a named and quantifiable resource in the game. */
 export interface Resource<T = DecimalSource> extends Ref<T> {
+    /** The name of this resource. */
     displayName: string;
+    /** When displaying the value of this resource, how many significant digits to display. */
     precision: number;
+    /** Whether or not to display very small values using scientific notation, or rounding to 0. */
     small?: boolean;
 }
 
+/**
+ * Creates a resource.
+ * @param defaultValue The initial value of the resource
+ * @param displayName The human readable name of this resource
+ * @param precision The number of significant digits to display by default
+ * @param small Whether or not to display very small values or round to 0, by default
+ */
 export function createResource<T extends State>(
     defaultValue: T,
     displayName?: string,
@@ -49,6 +60,7 @@ export function createResource<T extends State>(
     return resource as Resource<T>;
 }
 
+/** Returns a reference to the highest amount of the resource ever owned, which is updated automatically. */
 export function trackBest(resource: Resource): Ref<DecimalSource> {
     const best = persistent(resource.value);
     watch(resource, amount => {
@@ -62,6 +74,7 @@ export function trackBest(resource: Resource): Ref<DecimalSource> {
     return best;
 }
 
+/** Returns a reference to the total amount of the resource gained, updated automatically. "Refunds" count as gain. */
 export function trackTotal(resource: Resource): Ref<DecimalSource> {
     const total = persistent(resource.value);
     watch(resource, (amount, prevAmount) => {
@@ -77,6 +90,7 @@ export function trackTotal(resource: Resource): Ref<DecimalSource> {
 
 const tetra8 = new Decimal("10^^8");
 const e100 = new Decimal("1e100");
+/** Returns a reference to the amount of resource being gained in terms of orders of magnitude per second, calcualted over the last tick. Useful for situations where the gain rate is increasing very rapidly. */
 export function trackOOMPS(
     resource: Resource,
     pointGain?: ComputedRef<DecimalSource>
@@ -135,6 +149,7 @@ export function trackOOMPS(
     return oompsString;
 }
 
+/** Utility for displaying a resource with the correct precision. */
 export function displayResource(resource: Resource, overrideAmount?: DecimalSource): string {
     const amount = overrideAmount ?? resource.value;
     if (Decimal.eq(resource.precision, 0)) {
@@ -143,6 +158,7 @@ export function displayResource(resource: Resource, overrideAmount?: DecimalSour
     return format(amount, resource.precision, resource.small);
 }
 
+/** Utility for unwrapping a resource that may or may not be inside a ref. */
 export function unwrapResource(resource: ProcessedComputable<Resource>): Resource {
     if ("displayName" in resource) {
         return resource;
diff --git a/src/features/tabs/tab.ts b/src/features/tabs/tab.ts
index 6917009..d19dfdd 100644
--- a/src/features/tabs/tab.ts
+++ b/src/features/tabs/tab.ts
@@ -10,21 +10,39 @@ import TabComponent from "features/tabs/Tab.vue";
 import type { Computable, GetComputableType } from "util/computed";
 import { createLazyProxy } from "util/proxies";
 
+/** A symbol used to identify {@link Tab} features. */
 export const TabType = Symbol("Tab");
 
+/**
+ * An object that configures a {@link Tab}.
+ */
 export interface TabOptions {
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** The display to use for this tab. */
     display: Computable<CoercableComponent>;
 }
 
+/**
+ * The properties that are added onto a processed {@link TabOptions} to create an {@link Tab}.
+ */
 export interface BaseTab {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** A symbol that helps identify features of the same type. */
     type: typeof TabType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/**
+ * An object representing a tab of content in a tabbed interface.
+ * @see {@link TabFamily}
+ */
 export type Tab<T extends TabOptions> = Replace<
     T & BaseTab,
     {
@@ -34,8 +52,13 @@ export type Tab<T extends TabOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Tab} object. */
 export type GenericTab = Tab<TabOptions>;
 
+/**
+ * Lazily creates a tab with the given options.
+ * @param optionsFunc Tab options.
+ */
 export function createTab<T extends TabOptions>(
     optionsFunc: OptionsFunc<T, BaseTab, GenericTab>
 ): Tab<T> {
diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts
index 32df1e7..316b309 100644
--- a/src/features/tabs/tabFamily.ts
+++ b/src/features/tabs/tabFamily.ts
@@ -29,23 +29,43 @@ import type { Ref } from "vue";
 import { computed, unref } from "vue";
 import type { GenericTab } from "./tab";
 
+/** A symbol used to identify {@link TabButton} features. */
 export const TabButtonType = Symbol("TabButton");
+/** A symbol used to identify {@link TabFamily} features. */
 export const TabFamilyType = Symbol("TabFamily");
 
+/**
+ * An object that configures a {@link TabButton}.
+ */
 export interface TabButtonOptions {
+    /** Whether this tab button should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The tab to display when this button is clicked. */
     tab: Computable<GenericTab | CoercableComponent>;
+    /** The label on this button. */
     display: Computable<CoercableComponent>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** The color of the glow effect to display when this button is active. */
     glowColor?: Computable<string>;
 }
 
+/**
+ * The properties that are added onto a processed {@link TabButtonOptions} to create an {@link TabButton}.
+ */
 export interface BaseTabButton {
+    /** A symbol that helps identify features of the same type. */
     type: typeof TabButtonType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
 }
 
+/**
+ * An object that represents a button that can be clicked to change tabs in a tabbed interface.
+ * @see {@link TabFamily}
+ */
 export type TabButton<T extends TabButtonOptions> = Replace<
     T & BaseTabButton,
     {
@@ -58,6 +78,7 @@ export type TabButton<T extends TabButtonOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link TabButton} object. */
 export type GenericTabButton = Replace<
     TabButton<TabButtonOptions>,
     {
@@ -65,24 +86,46 @@ export type GenericTabButton = Replace<
     }
 >;
 
+/**
+ * An object that configures a {@link TabFamily}.
+ */
 export interface TabFamilyOptions {
+    /** Whether this tab button should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** A dictionary of CSS classes to apply to the list of buttons for changing tabs. */
     buttonContainerClasses?: Computable<Record<string, boolean>>;
+    /** CSS to apply to the list of buttons for changing tabs. */
     buttonContainerStyle?: Computable<StyleValue>;
 }
 
+/**
+ * The properties that are added onto a processed {@link TabFamilyOptions} to create an {@link TabFamily}.
+ */
 export interface BaseTabFamily {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** All the tabs within this family. */
     tabs: Record<string, TabButtonOptions>;
+    /** The currently active tab, if any. */
     activeTab: Ref<GenericTab | CoercableComponent | null>;
+    /** The name of the tab that is currently active. */
     selected: Persistent<string>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof TabFamilyType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/**
+ * An object that represents a tabbed interface.
+ * @see {@link TabFamily}
+ */
 export type TabFamily<T extends TabFamilyOptions> = Replace<
     T & BaseTabFamily,
     {
@@ -91,6 +134,7 @@ export type TabFamily<T extends TabFamilyOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link TabFamily} object. */
 export type GenericTabFamily = Replace<
     TabFamily<TabFamilyOptions>,
     {
@@ -98,6 +142,10 @@ export type GenericTabFamily = Replace<
     }
 >;
 
+/**
+ * Lazily creates a tab family with the given options.
+ * @param optionsFunc Tab family options.
+ */
 export function createTabFamily<T extends TabFamilyOptions>(
     tabs: Record<string, () => TabButtonOptions>,
     optionsFunc?: OptionsFunc<T, BaseTabFamily, GenericTabFamily>
diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts
index 2cb977d..55985f4 100644
--- a/src/features/tooltips/tooltip.ts
+++ b/src/features/tooltips/tooltip.ts
@@ -21,20 +21,34 @@ declare module "@vue/runtime-dom" {
     }
 }
 
+/**
+ * An object that configures a {@link Tooltip}.
+ */
 export interface TooltipOptions {
+    /** Whether or not this tooltip can be pinned, meaning it'll stay visible even when not hovered. */
     pinnable?: boolean;
+    /** The text to display inside the tooltip. */
     display: Computable<CoercableComponent>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** The direction in which to display the tooltip */
     direction?: Computable<Direction>;
+    /** The x offset of the tooltip, in px. */
     xoffset?: Computable<string>;
+    /** The y offset of the tooltip, in px. */
     yoffset?: Computable<string>;
 }
 
+/**
+ * The properties that are added onto a processed {@link TooltipOptions} to create an {@link Tooltip}.
+ */
 export interface BaseTooltip {
     pinned?: Ref<boolean>;
 }
 
+/** An object that represents a tooltip that appears when hovering over an element. */
 export type Tooltip<T extends TooltipOptions> = Replace<
     T & BaseTooltip,
     {
@@ -49,6 +63,7 @@ export type Tooltip<T extends TooltipOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Tooltip} object. */
 export type GenericTooltip = Replace<
     Tooltip<TooltipOptions>,
     {
@@ -58,6 +73,11 @@ export type GenericTooltip = Replace<
     }
 >;
 
+/**
+ * Creates a tooltip on the given element with the given options.
+ * @param element The renderable feature to display the tooltip on.
+ * @param optionsFunc Clickable options.
+ */
 export function addTooltip<T extends TooltipOptions>(
     element: VueFeature,
     options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts
index a3efba7..8bd1f7a 100644
--- a/src/features/trees/tree.ts
+++ b/src/features/trees/tree.ts
@@ -25,30 +25,54 @@ import { createLazyProxy } from "util/proxies";
 import type { Ref } from "vue";
 import { computed, ref, shallowRef, unref } from "vue";
 
+/** A symbol used to identify {@link TreeNode} features. */
 export const TreeNodeType = Symbol("TreeNode");
+/** A symbol used to identify {@link Tree} features. */
 export const TreeType = Symbol("Tree");
 
+/**
+ * An object that configures a {@link TreeNode}.
+ */
 export interface TreeNodeOptions {
+    /** Whether this tree node should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** Whether or not this tree node can be clicked. */
     canClick?: Computable<boolean>;
+    /** The background color for this node. */
     color?: Computable<string>;
+    /** The label to display on this tree node. */
     display?: Computable<CoercableComponent>;
+    /** The color of the glow effect shown to notify the user there's something to do with this node. */
     glowColor?: Computable<string>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** Shows a marker on the corner of the feature. */
     mark?: Computable<boolean | string>;
+    /** A reset object attached to this node, used for propagating resets through the tree. */
     reset?: GenericReset;
+    /** A function that is called when the tree node is clicked. */
     onClick?: (e?: MouseEvent | TouchEvent) => void;
+    /** A function that is called when the tree node is held down. */
     onHold?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link TreeNodeOptions} to create an {@link TreeNode}.
+ */
 export interface BaseTreeNode {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** A symbol that helps identify features of the same type. */
     type: typeof TreeNodeType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a node on a tree. */
 export type TreeNode<T extends TreeNodeOptions> = Replace<
     T & BaseTreeNode,
     {
@@ -63,6 +87,7 @@ export type TreeNode<T extends TreeNodeOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link TreeNode} object. */
 export type GenericTreeNode = Replace<
     TreeNode<TreeNodeOptions>,
     {
@@ -71,6 +96,10 @@ export type GenericTreeNode = Replace<
     }
 >;
 
+/**
+ * Lazily creates a tree node with the given options.
+ * @param optionsFunc Tree Node options.
+ */
 export function createTreeNode<T extends TreeNodeOptions>(
     optionsFunc?: OptionsFunc<T, BaseTreeNode, GenericTreeNode>
 ): TreeNode<T> {
@@ -141,32 +170,52 @@ export function createTreeNode<T extends TreeNodeOptions>(
     });
 }
 
+/** Represents a branch between two nodes in a tree. */
 export interface TreeBranch extends Omit<Link, "startNode" | "endNode"> {
     startNode: GenericTreeNode;
     endNode: GenericTreeNode;
 }
 
+/**
+ * An object that configures a {@link Tree}.
+ */
 export interface TreeOptions {
+    /** Whether this clickable should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** The nodes within the tree, in a 2D array. */
     nodes: Computable<GenericTreeNode[][]>;
+    /** Nodes to show on the left side of the tree. */
     leftSideNodes?: Computable<GenericTreeNode[]>;
+    /** Nodes to show on the right side of the tree. */
     rightSideNodes?: Computable<GenericTreeNode[]>;
+    /** The branches between nodes within this tree. */
     branches?: Computable<TreeBranch[]>;
+    /** How to propagate resets through the tree. */
     resetPropagation?: ResetPropagation;
+    /** A function that is called when a node within the tree is reset. */
     onReset?: (node: GenericTreeNode) => void;
 }
 
 export interface BaseTree {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** The link objects for each of the branches of the tree.  */
     links: Ref<Link[]>;
+    /** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
     reset: (node: GenericTreeNode) => void;
+    /** A flag that is true while the reset is still propagating through the tree. */
     isResetting: Ref<boolean>;
+    /** A reference to the node that caused the currently propagating reset. */
     resettingNode: Ref<GenericTreeNode | null>;
+    /** A symbol that helps identify features of the same type. */
     type: typeof TreeType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that is a tree of nodes with branches between them. Contains support for reset mechanics that can propagate through the tree. */
 export type Tree<T extends TreeOptions> = Replace<
     T & BaseTree,
     {
@@ -178,6 +227,7 @@ export type Tree<T extends TreeOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Tree} object. */
 export type GenericTree = Replace<
     Tree<TreeOptions>,
     {
@@ -185,6 +235,10 @@ export type GenericTree = Replace<
     }
 >;
 
+/**
+ * Lazily creates a tree with the given options.
+ * @param optionsFunc Tree options.
+ */
 export function createTree<T extends TreeOptions>(
     optionsFunc: OptionsFunc<T, BaseTree, GenericTree>
 ): Tree<T> {
@@ -227,10 +281,12 @@ export function createTree<T extends TreeOptions>(
     });
 }
 
+/** A function that is used to propagate resets through a tree. */
 export type ResetPropagation = {
     (tree: GenericTree, resettingNode: GenericTreeNode): void;
 };
 
+/** Propagate resets down the tree by resetting every node in a lower row. */
 export const defaultResetPropagation = function (
     tree: GenericTree,
     resettingNode: GenericTreeNode
@@ -242,6 +298,7 @@ export const defaultResetPropagation = function (
     }
 };
 
+/** Propagate resets down the tree by resetting every node in a lower row. */
 export const invertedResetPropagation = function (
     tree: GenericTree,
     resettingNode: GenericTreeNode
@@ -253,6 +310,7 @@ export const invertedResetPropagation = function (
     }
 };
 
+/** Propagate resets down the branches of the tree. */
 export const branchedResetPropagation = function (
     tree: GenericTree,
     resettingNode: GenericTreeNode
@@ -288,6 +346,10 @@ export const branchedResetPropagation = function (
     }
 };
 
+/**
+ * Utility for creating a tooltip for a tree node that displays a resource-based unlock requirement, and after unlock shows the amount of another resource.
+ * It sounds oddly specific, but comes up a lot.
+ */
 export function createResourceTooltip(
     resource: Resource,
     requiredResource: Resource | null = null,
diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts
index 5bcf5df..14a64e7 100644
--- a/src/features/upgrades/upgrade.ts
+++ b/src/features/upgrades/upgrade.ts
@@ -36,35 +36,60 @@ import { createLazyProxy } from "util/proxies";
 import type { Ref } from "vue";
 import { computed, unref } from "vue";
 
+/** A symbol used to identify {@link Upgrade} features. */
 export const UpgradeType = Symbol("Upgrade");
 
+/**
+ * An object that configures a {@link Upgrade}.
+ */
 export interface UpgradeOptions {
+    /** Whether this clickable should be visible. */
     visibility?: Computable<Visibility | boolean>;
+    /** Dictionary of CSS classes to apply to this feature. */
     classes?: Computable<Record<string, boolean>>;
+    /** CSS to apply to this feature. */
     style?: Computable<StyleValue>;
+    /** Shows a marker on the corner of the feature. */
+    mark?: Computable<boolean | string>;
+    /** The display to use for this clickable. */
     display?: Computable<
         | CoercableComponent
         | {
+              /** A header to appear at the top of the display. */
               title?: CoercableComponent;
+              /** The main text that appears in the display. */
               description: CoercableComponent;
+              /** A description of the current effect of the achievement. Useful when the effect changes dynamically. */
               effectDisplay?: CoercableComponent;
           }
     >;
+    /** The requirements to purchase this upgrade. */
     requirements: Requirements;
-    mark?: Computable<boolean | string>;
+    /** A function that is called when the upgrade is purchased. */
     onPurchase?: VoidFunction;
 }
 
+/**
+ * The properties that are added onto a processed {@link UpgradeOptions} to create an {@link Upgrade}.
+ */
 export interface BaseUpgrade {
+    /** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
     id: string;
+    /** Whether or not this upgrade has been purchased. */
     bought: Persistent<boolean>;
+    /** Whether or not the upgrade can currently be purchased. */
     canPurchase: Ref<boolean>;
+    /** Purchase the upgrade */
     purchase: VoidFunction;
+    /** A symbol that helps identify features of the same type. */
     type: typeof UpgradeType;
+    /** The Vue component used to render this feature. */
     [Component]: GenericComponent;
+    /** A function to gather the props the vue component requires for this feature. */
     [GatherProps]: () => Record<string, unknown>;
 }
 
+/** An object that represents a feature that can be purchased a single time. */
 export type Upgrade<T extends UpgradeOptions> = Replace<
     T & BaseUpgrade,
     {
@@ -77,6 +102,7 @@ export type Upgrade<T extends UpgradeOptions> = Replace<
     }
 >;
 
+/** A type that matches any valid {@link Upgrade} object. */
 export type GenericUpgrade = Replace<
     Upgrade<UpgradeOptions>,
     {
@@ -84,6 +110,10 @@ export type GenericUpgrade = Replace<
     }
 >;
 
+/**
+ * Lazily creates an upgrade with the given options.
+ * @param optionsFunc Upgrade options.
+ */
 export function createUpgrade<T extends UpgradeOptions>(
     optionsFunc: OptionsFunc<T, BaseUpgrade, GenericUpgrade>
 ): Upgrade<T> {
@@ -151,6 +181,12 @@ export function createUpgrade<T extends UpgradeOptions>(
     });
 }
 
+/**
+ * Utility to auto purchase a list of upgrades whenever they're affordable.
+ * @param layer The layer the upgrades are apart of
+ * @param autoActive Whether or not the upgrades should currently be auto-purchasing
+ * @param upgrades The specific upgrades to upgrade. If unspecified, uses all upgrades on the layer.
+ */
 export function setupAutoPurchase(
     layer: GenericLayer,
     autoActive: Computable<boolean>,