From 0afcd1cd3d68de87ff33581e999b374e6d732bce Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 16 Aug 2021 23:30:54 -0500
Subject: [PATCH] First pass at typescript support Oh man did this end up
 requiring a *ton* of other work as well. There's still a few typing issues I
 still can't quite work out, and others I'd like to improve when I have time.
 In fact, this version doesn't even really work, it has a stack overflow error
 caused by a tooltip for some reason have a tree inside it, which in turn has
 another tooltip, etc. There's also 17 errors that I *really* feel like
 shouldn't be there, but they are, and 113 warnings - mostly using ! to assert
 that things are non-null. Lots of work left to do, to sum up.

The reason I'm committing this now is because I really need to get to
work on my game jam, and since it won't use a tree or really many of
TMT-X's features, I can get away with using a broken engine :)
---
 .eslintrc.js                                  |   20 +
 .prettierrc.json                              |    4 +
 babel.config.js                               |    6 +-
 jsconfig.json                                 |    5 -
 package-lock.json                             | 2726 ++++++++++++++++-
 package.json                                  |   45 +-
 src/App.vue                                   |   79 +-
 src/components/features/Achievement.vue       |  104 +-
 src/components/features/Achievements.vue      |   72 +-
 src/components/features/Bar.vue               |  190 +-
 src/components/features/Buyable.vue           |  216 +-
 src/components/features/Buyables.vue          |  138 +-
 src/components/features/Challenge.vue         |  151 +-
 src/components/features/Challenges.vue        |   65 +-
 src/components/features/Clickable.vue         |  154 +-
 src/components/features/Clickables.vue        |  124 +-
 .../features/DefaultChallengeDisplay.vue      |   86 +-
 .../features/DefaultPrestigeButtonDisplay.vue |  140 +-
 .../features/DefaultUpgradeDisplay.vue        |   97 +-
 src/components/features/Grid.vue              |   52 +-
 src/components/features/GridCell.vue          |   97 +
 src/components/features/Gridable.vue          |   77 -
 src/components/features/Infobox.vue           |  223 +-
 src/components/features/MainDisplay.vue       |   91 +-
 src/components/features/MarkNode.vue          |   82 +-
 src/components/features/MasterButton.vue      |   86 +-
 src/components/features/Milestone.vue         |   97 +-
 src/components/features/Milestones.vue        |   47 +-
 src/components/features/PrestigeButton.vue    |   83 +-
 src/components/features/ResourceDisplay.vue   |  113 +-
 src/components/features/RespecButton.vue      |  184 +-
 src/components/features/Subtab.vue            |   32 +
 src/components/features/Upgrade.vue           |  105 +-
 src/components/features/Upgrades.vue          |   69 +-
 src/components/fields/DangerButton.vue        |   98 +-
 src/components/fields/FeedbackButton.vue      |  113 +-
 src/components/fields/Select.vue              |   74 +-
 src/components/fields/Slider.vue              |   44 +-
 src/components/fields/Text.vue                |   73 +-
 src/components/fields/Toggle.vue              |  105 +-
 src/components/index.js                       |   28 -
 src/components/index.ts                       |   26 +
 src/components/system/Column.vue              |   20 +-
 src/components/system/DefaultLayerTab.vue     |   73 +-
 src/components/system/GameOverScreen.vue      |  149 +-
 src/components/system/Info.vue                |  211 +-
 src/components/system/LayerProvider.vue       |   39 +-
 src/components/system/LayerTab.vue            |  387 +--
 src/components/system/Microtab.vue            |  151 +-
 src/components/system/Modal.vue               |  178 +-
 src/components/system/NaNScreen.vue           |  204 +-
 src/components/system/Nav.vue                 |  322 +-
 src/components/system/Options.vue             |  139 +-
 src/components/system/Resource.vue            |   27 +-
 src/components/system/Row.vue                 |   20 +-
 src/components/system/Save.vue                |  247 +-
 src/components/system/SavesManager.vue        |  462 +--
 src/components/system/Spacer.vue              |   36 +-
 src/components/system/Sticky.vue              |  113 +-
 src/components/system/Subtab.vue              |   27 -
 src/components/system/TPS.vue                 |   40 +-
 src/components/system/TabButton.vue           |  124 +-
 src/components/system/Tabs.vue                |   97 +-
 src/components/system/Tooltip.vue             |  209 +-
 src/components/system/VerticalRule.vue        |   26 +-
 src/components/tree/BranchLine.vue            |  104 +-
 src/components/tree/BranchNode.vue            |  176 +-
 src/components/tree/Branches.vue              |  187 +-
 src/components/tree/Tree.vue                  |  155 +-
 src/components/tree/TreeNode.vue              |  236 +-
 src/data/layers/aca/a.js                      |   82 -
 src/data/layers/aca/a.ts                      |  103 +
 src/data/layers/aca/c.js                      |  399 ---
 src/data/layers/aca/c.ts                      |  573 ++++
 src/data/layers/aca/f.js                      |  107 -
 src/data/layers/aca/f.ts                      |  151 +
 src/data/layers/demo-infinity.js              |  153 -
 src/data/layers/demo-infinity.ts              |  354 +++
 src/data/layers/demo.js                       |  940 ------
 src/data/layers/demo.ts                       | 2301 ++++++++++++++
 src/data/mod.js                               |  120 -
 src/data/mod.ts                               |  187 ++
 src/data/themes.js                            |   52 -
 src/data/themes.ts                            |   60 +
 src/game/enums.ts                             |   30 +
 src/game/gameLoop.js                          |  156 -
 src/game/gameLoop.ts                          |  171 ++
 src/game/layers.js                            |  452 ---
 src/game/layers.ts                            |  604 ++++
 src/game/player.js                            |   52 -
 src/game/player.ts                            |  112 +
 src/lib/break_eternity.js                     |    2 -
 src/lib/break_eternity.ts                     | 2717 ++++++++++++++++
 src/main.js                                   |   22 -
 src/main.ts                                   |   22 +
 src/shims-vue.d.ts                            |    6 +
 src/typings/branches.d.ts                     |   28 +
 src/typings/cacheableFunction.d.ts            |    3 +
 src/typings/component.d.ts                    |    3 +
 src/typings/computable.d.ts                   |    5 +
 src/typings/features/achievement.d.ts         |   16 +
 src/typings/features/bar.d.ts                 |   15 +
 src/typings/features/buyable.d.ts             |   20 +
 src/typings/features/challenge.d.ts           |   34 +
 src/typings/features/clickable.d.ts           |   15 +
 src/typings/features/feature.d.ts             |   34 +
 src/typings/features/grid.d.ts                |   33 +
 src/typings/features/hotkey.d.ts              |    8 +
 src/typings/features/infobox.d.ts             |   11 +
 src/typings/features/milestone.d.ts           |   12 +
 src/typings/features/subtab.d.ts              |   35 +
 src/typings/features/upgrade.d.ts             |   23 +
 src/typings/global.d.ts                       |   27 +
 src/typings/layer.d.ts                        |  146 +
 src/typings/player.d.ts                       |   66 +
 src/typings/state.d.ts                        |    2 +
 src/typings/theme.d.ts                        |    7 +
 src/util/{bignum.js => bignum.ts}             |   23 +-
 src/util/break_eternity.js                    |  143 -
 src/util/break_eternity.ts                    |  186 ++
 src/util/common.js                            |   24 -
 src/util/common.ts                            |   30 +
 src/util/features.js                          |   77 -
 src/util/features.ts                          |   89 +
 src/util/layers.js                            |  313 --
 src/util/layers.ts                            |  390 +++
 src/util/proxies.js                           |  149 -
 src/util/proxies.ts                           |  187 ++
 src/util/save.js                              |  160 -
 src/util/save.ts                              |  191 ++
 src/util/vue.js                               |   57 -
 src/util/vue.ts                               |  114 +
 tsconfig.json                                 |   43 +
 vue.config.js                                 |   11 +-
 134 files changed, 16132 insertions(+), 7106 deletions(-)
 create mode 100644 .eslintrc.js
 create mode 100644 .prettierrc.json
 delete mode 100644 jsconfig.json
 create mode 100644 src/components/features/GridCell.vue
 delete mode 100644 src/components/features/Gridable.vue
 create mode 100644 src/components/features/Subtab.vue
 delete mode 100644 src/components/index.js
 create mode 100644 src/components/index.ts
 delete mode 100644 src/components/system/Subtab.vue
 delete mode 100644 src/data/layers/aca/a.js
 create mode 100644 src/data/layers/aca/a.ts
 delete mode 100644 src/data/layers/aca/c.js
 create mode 100644 src/data/layers/aca/c.ts
 delete mode 100644 src/data/layers/aca/f.js
 create mode 100644 src/data/layers/aca/f.ts
 delete mode 100644 src/data/layers/demo-infinity.js
 create mode 100644 src/data/layers/demo-infinity.ts
 delete mode 100644 src/data/layers/demo.js
 create mode 100644 src/data/layers/demo.ts
 delete mode 100644 src/data/mod.js
 create mode 100644 src/data/mod.ts
 delete mode 100644 src/data/themes.js
 create mode 100644 src/data/themes.ts
 create mode 100644 src/game/enums.ts
 delete mode 100644 src/game/gameLoop.js
 create mode 100644 src/game/gameLoop.ts
 delete mode 100644 src/game/layers.js
 create mode 100644 src/game/layers.ts
 delete mode 100644 src/game/player.js
 create mode 100644 src/game/player.ts
 delete mode 100644 src/lib/break_eternity.js
 create mode 100644 src/lib/break_eternity.ts
 delete mode 100644 src/main.js
 create mode 100644 src/main.ts
 create mode 100644 src/shims-vue.d.ts
 create mode 100644 src/typings/branches.d.ts
 create mode 100644 src/typings/cacheableFunction.d.ts
 create mode 100644 src/typings/component.d.ts
 create mode 100644 src/typings/computable.d.ts
 create mode 100644 src/typings/features/achievement.d.ts
 create mode 100644 src/typings/features/bar.d.ts
 create mode 100644 src/typings/features/buyable.d.ts
 create mode 100644 src/typings/features/challenge.d.ts
 create mode 100644 src/typings/features/clickable.d.ts
 create mode 100644 src/typings/features/feature.d.ts
 create mode 100644 src/typings/features/grid.d.ts
 create mode 100644 src/typings/features/hotkey.d.ts
 create mode 100644 src/typings/features/infobox.d.ts
 create mode 100644 src/typings/features/milestone.d.ts
 create mode 100644 src/typings/features/subtab.d.ts
 create mode 100644 src/typings/features/upgrade.d.ts
 create mode 100644 src/typings/global.d.ts
 create mode 100644 src/typings/layer.d.ts
 create mode 100644 src/typings/player.d.ts
 create mode 100644 src/typings/state.d.ts
 create mode 100644 src/typings/theme.d.ts
 rename src/util/{bignum.js => bignum.ts} (64%)
 delete mode 100644 src/util/break_eternity.js
 create mode 100644 src/util/break_eternity.ts
 delete mode 100644 src/util/common.js
 create mode 100644 src/util/common.ts
 delete mode 100644 src/util/features.js
 create mode 100644 src/util/features.ts
 delete mode 100644 src/util/layers.js
 create mode 100644 src/util/layers.ts
 delete mode 100644 src/util/proxies.js
 create mode 100644 src/util/proxies.ts
 delete mode 100644 src/util/save.js
 create mode 100644 src/util/save.ts
 delete mode 100644 src/util/vue.js
 create mode 100644 src/util/vue.ts
 create mode 100644 tsconfig.json

diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..a343a1e
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,20 @@
+module.exports = {
+    root: true,
+    env: {
+        node: true
+    },
+    extends: [
+        "plugin:vue/vue3-essential",
+        "eslint:recommended",
+        "@vue/typescript/recommended",
+        "@vue/prettier",
+        "@vue/prettier/@typescript-eslint"
+    ],
+    parserOptions: {
+        ecmaVersion: 2020
+    },
+    rules: {
+        "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
+        "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
+    }
+};
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..ec1a921
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,4 @@
+{
+  "printWidth": 100,
+  "tabWidth": 4
+}
diff --git a/babel.config.js b/babel.config.js
index 414e4ac..4ab6557 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,5 +1,3 @@
 module.exports = {
-	presets: [
-		'@vue/cli-plugin-babel/preset'
-	]
-}
+    presets: ["@vue/cli-plugin-babel/preset"]
+};
diff --git a/jsconfig.json b/jsconfig.json
deleted file mode 100644
index adc4c61..0000000
--- a/jsconfig.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-	"include": [
-		"./src/components/**/*"
-	]
-}
diff --git a/package-lock.json b/package-lock.json
index a532def..82536ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,26 +8,36 @@
       "name": "the-modding-tree-x",
       "version": "0.1.0",
       "dependencies": {
-        "@ivanv/vue-collapse-transition": "^1.0.2",
         "core-js": "^3.6.5",
         "lodash.clonedeep": "^4.5.0",
-        "vue": "^3.1.4",
-        "vue-next-select": "^2.7.0",
+        "vue": "^3.2.2",
+        "vue-class-component": "^8.0.0-rc.1",
+        "vue-next-select": "^2.9.0",
         "vue-sortable": "github:Netbel/vue-sortable#master-fix",
         "vue-textarea-autosize": "^1.1.1",
         "vue-transition-expand": "^0.1.0"
       },
       "devDependencies": {
+        "@ivanv/vue-collapse-transition": "^1.0.2",
+        "@types/lodash.clonedeep": "^4.5.6",
+        "@typescript-eslint/eslint-plugin": "^4.18.0",
+        "@typescript-eslint/parser": "^4.18.0",
         "@vue/cli-plugin-babel": "~4.5.0",
         "@vue/cli-plugin-eslint": "~4.5.0",
+        "@vue/cli-plugin-typescript": "~4.5.0",
         "@vue/cli-service": "~4.5.0",
-        "@vue/compiler-sfc": "^3.0.0-beta.1",
+        "@vue/compiler-sfc": "^3.2.2",
+        "@vue/eslint-config-prettier": "^6.0.0",
+        "@vue/eslint-config-typescript": "^7.0.0",
         "babel-eslint": "^10.1.0",
         "eslint": "^6.7.2",
         "eslint-plugin-vue": "^7.0.0-alpha.0",
+        "prettier": "^1.19.1",
         "raw-loader": "^4.0.2",
         "sass": "^1.36.0",
-        "sass-loader": "^10.2.0"
+        "sass-loader": "^10.2.0",
+        "tsconfig-paths-webpack-plugin": "^3.5.1",
+        "typescript": "~4.1.5"
       }
     },
     "D:/projects/The-Modding-Tree-X/node_modules/@babel/code-frame": {
@@ -13057,7 +13067,8 @@
     "node_modules/@ivanv/vue-collapse-transition": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@ivanv/vue-collapse-transition/-/vue-collapse-transition-1.0.2.tgz",
-      "integrity": "sha512-eWEameFXJM/1khcoKbITvKjYYXDP1WKQ/Xf9ItJVPoEjCiOdocR3AgDAERzDrNNg4oWK28gRGi+0ft8Te27zxw=="
+      "integrity": "sha512-eWEameFXJM/1khcoKbITvKjYYXDP1WKQ/Xf9ItJVPoEjCiOdocR3AgDAERzDrNNg4oWK28gRGi+0ft8Te27zxw==",
+      "dev": true
     },
     "node_modules/@mrmlnc/readdir-enhanced": {
       "version": "2.2.1",
@@ -13072,6 +13083,28 @@
         "node": ">=4"
       }
     },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir/node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/@nodelib/fs.stat": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
@@ -13081,6 +13114,19 @@
         "node": ">= 6"
       }
     },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -13188,6 +13234,21 @@
       "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
       "dev": true
     },
+    "node_modules/@types/lodash": {
+      "version": "4.14.171",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
+      "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==",
+      "dev": true
+    },
+    "node_modules/@types/lodash.clonedeep": {
+      "version": "4.5.6",
+      "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz",
+      "integrity": "sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA==",
+      "dev": true,
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
     "node_modules/@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -13218,6 +13279,13 @@
       "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
       "dev": true
     },
+    "node_modules/@types/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
+      "dev": true,
+      "optional": true
+    },
     "node_modules/@types/q": {
       "version": "1.5.4",
       "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
@@ -13303,6 +13371,12 @@
         "http-proxy-middleware": "^1.0.0"
       }
     },
+    "node_modules/@types/webpack-env": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.2.tgz",
+      "integrity": "sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw==",
+      "dev": true
+    },
     "node_modules/@types/webpack-sources": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.1.tgz",
@@ -13332,6 +13406,469 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz",
+      "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/experimental-utils": "4.28.5",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "debug": "^4.3.1",
+        "functional-red-black-tree": "^1.0.1",
+        "regexpp": "^3.1.0",
+        "semver": "^7.3.5",
+        "tsutils": "^3.21.0"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^4.0.0",
+        "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/regexpp": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+      "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+      "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/tsutils": {
+      "version": "3.21.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+      "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^1.8.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
+    "node_modules/@typescript-eslint/experimental-utils": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz",
+      "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.7",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
+        "eslint-scope": "^5.1.1",
+        "eslint-utils": "^3.0.0"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "*"
+      }
+    },
+    "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+      "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^2.0.0"
+      },
+      "engines": {
+        "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=5"
+      }
+    },
+    "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-visitor-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+      "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz",
+      "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz",
+      "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5"
+      },
+      "engines": {
+        "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz",
+      "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==",
+      "dev": true,
+      "engines": {
+        "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz",
+      "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5",
+        "debug": "^4.3.1",
+        "globby": "^11.0.3",
+        "is-glob": "^4.0.1",
+        "semver": "^7.3.5",
+        "tsutils": "^3.21.0"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+      "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+      "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": {
+      "version": "11.0.4",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+      "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.1.1",
+        "ignore": "^5.1.4",
+        "merge2": "^1.3.0",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": {
+      "version": "5.1.8",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+      "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/micromatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+      "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+      "dev": true,
+      "dependencies": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.2.3"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+      "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+      "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/tsutils": {
+      "version": "3.21.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+      "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^1.8.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz",
+      "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "4.28.5",
+        "eslint-visitor-keys": "^2.0.0"
+      },
+      "engines": {
+        "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+      "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@vue/babel-helper-vue-jsx-merge-props": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@@ -13603,6 +14140,37 @@
         "@vue/cli-service": "^3.0.0 || ^4.0.0-0"
       }
     },
+    "node_modules/@vue/cli-plugin-typescript": {
+      "version": "4.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/cli-plugin-typescript/-/cli-plugin-typescript-4.5.13.tgz",
+      "integrity": "sha512-CpLlIdFNV1gn9uC4Yh6QgWI42uk2x5Z3cb2ScxNSwWsR1vgSdr0/1DdNzoBm68aP8RUtnHHO/HZfPnvXiq42xA==",
+      "dev": true,
+      "dependencies": {
+        "@types/webpack-env": "^1.15.2",
+        "@vue/cli-shared-utils": "^4.5.13",
+        "cache-loader": "^4.1.0",
+        "fork-ts-checker-webpack-plugin": "^3.1.1",
+        "globby": "^9.2.0",
+        "thread-loader": "^2.1.3",
+        "ts-loader": "^6.2.2",
+        "tslint": "^5.20.1",
+        "webpack": "^4.0.0",
+        "yorkie": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "fork-ts-checker-webpack-plugin-v5": "npm:fork-ts-checker-webpack-plugin@^5.0.11"
+      },
+      "peerDependencies": {
+        "@vue/cli-service": "^3.0.0 || ^4.0.0-0",
+        "@vue/compiler-sfc": "^3.0.0-beta.14",
+        "typescript": ">=2"
+      },
+      "peerDependenciesMeta": {
+        "@vue/compiler-sfc": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@vue/cli-plugin-vuex": {
       "version": "4.5.13",
       "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.13.tgz",
@@ -13753,13 +14321,13 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.4.tgz",
-      "integrity": "sha512-TnUz+1z0y74O/A4YKAbzsdUfamyHV73MihrEfvettWpm9bQKVoZd1nEmR1cGN9LsXWlwAvVQBetBlWdOjmQO5Q==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.2.tgz",
+      "integrity": "sha512-QhCI0ZU5nAR0LMcLgzW3v75374tIrHGp8XG5CzJS7Nsy+iuignbE4MZ2XJfh5TGIrtpuzfWA4eTIfukZf/cRdg==",
       "dependencies": {
         "@babel/parser": "^7.12.0",
         "@babel/types": "^7.12.0",
-        "@vue/shared": "3.1.4",
+        "@vue/shared": "3.2.2",
         "estree-walker": "^2.0.1",
         "source-map": "^0.6.1"
       }
@@ -13773,27 +14341,27 @@
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.4.tgz",
-      "integrity": "sha512-3tG2ScHkghhUBuFwl9KgyZhrS8CPFZsO7hUDekJgIp5b1OMkROr4AvxHu6rRMl4WkyvYkvidFNBS2VfOnwa6Kw==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.2.tgz",
+      "integrity": "sha512-ggcc+NV/ENIE0Uc3TxVE/sKrhYVpLepMAAmEiQ047332mbKOvUkowz4TTFZ+YkgOIuBOPP0XpCxmCMg7p874mA==",
       "dependencies": {
-        "@vue/compiler-core": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-core": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.4.tgz",
-      "integrity": "sha512-4KDQg60Khy3SgnF+V/TB2NZqzmM4TyGRmzsxqG1SebGdMSecCweFDSlI/F1vDYk6dKiCHgmpoT9A1sLxswkJ0A==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.2.tgz",
+      "integrity": "sha512-hrtqpQ5L6IPn5v7yVRo7uvLcQxv0z1+KBjZBWMBOcrXz4t+PKUxU/SWd6Tl9T8FDmYlunzKUh6lcx+2CLo6f5A==",
       "dev": true,
       "dependencies": {
         "@babel/parser": "^7.13.9",
         "@babel/types": "^7.13.0",
         "@types/estree": "^0.0.48",
-        "@vue/compiler-core": "3.1.4",
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/compiler-ssr": "3.1.4",
-        "@vue/shared": "3.1.4",
+        "@vue/compiler-core": "3.2.2",
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/compiler-ssr": "3.2.2",
+        "@vue/shared": "3.2.2",
         "consolidate": "^0.16.0",
         "estree-walker": "^2.0.1",
         "hash-sum": "^2.0.0",
@@ -13804,9 +14372,6 @@
         "postcss-modules": "^4.0.0",
         "postcss-selector-parser": "^6.0.4",
         "source-map": "^0.6.1"
-      },
-      "peerDependencies": {
-        "vue": "3.1.4"
       }
     },
     "node_modules/@vue/compiler-sfc/node_modules/icss-utils": {
@@ -13927,13 +14492,13 @@
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.4.tgz",
-      "integrity": "sha512-Box8fCuCFPp0FuimIswjDkjwiSDCBkHvt/xVALyFkYCiIMWv2eR53fIjmlsnEHhcBuZ+VgRC+UanCTcKvSA1gA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.2.tgz",
+      "integrity": "sha512-rVl1agMFhdEN3Go0bCriXo+3cysxKIuRP0yh1Wd8ysRrKfAmokyDhUA8PrGSq2Ymj/LdZTh+4OKfj3p2+C+hlA==",
       "dev": true,
       "dependencies": {
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "node_modules/@vue/component-compiler-utils": {
@@ -13998,6 +14563,38 @@
       "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
       "dev": true
     },
+    "node_modules/@vue/eslint-config-prettier": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz",
+      "integrity": "sha512-wFQmv45c3ige5EA+ngijq40YpVcIkAy0Lihupnsnd1Dao5CBbPyfCzqtejFLZX1EwH/kCJdpz3t6s+5wd3+KxQ==",
+      "dev": true,
+      "dependencies": {
+        "eslint-config-prettier": "^6.0.0"
+      },
+      "peerDependencies": {
+        "eslint": ">= 5.0.0",
+        "eslint-plugin-prettier": "^3.1.0",
+        "prettier": ">= 1.13.0"
+      }
+    },
+    "node_modules/@vue/eslint-config-typescript": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-7.0.0.tgz",
+      "integrity": "sha512-UxUlvpSrFOoF8aQ+zX1leYiEBEm7CZmXYn/ZEM1zwSadUzpamx56RB4+Htdjisv1mX2tOjBegNUqH3kz2OL+Aw==",
+      "dev": true,
+      "dependencies": {
+        "vue-eslint-parser": "^7.0.0"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/eslint-plugin": "^4.4.0",
+        "@typescript-eslint/parser": "^4.4.0",
+        "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0",
+        "eslint-plugin-vue": "^5.2.3 || ^6.0.0 || ^7.0.0"
+      }
+    },
     "node_modules/@vue/preload-webpack-plugin": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
@@ -14012,36 +14609,36 @@
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.4.tgz",
-      "integrity": "sha512-YDlgii2Cr9yAoKVZFzgY4j0mYlVT73986X3e5SPp6ifqckSEoFSUWXZK2Tb53TB/9qO29BEEbspnKD3m3wAwkA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.2.tgz",
+      "integrity": "sha512-IHjhtmrhK6dzacj/EnLQDWOaA3HuzzVk6w84qgV8EpS4uWGIJXiRalMRg6XvGW2ykJvIl3pLsF0aBFlTMRiLOA==",
       "dependencies": {
-        "@vue/shared": "3.1.4"
+        "@vue/shared": "3.2.2"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.4.tgz",
-      "integrity": "sha512-qmVJgJuFxfT7M4qHQ4M6KqhKC66fjuswK+aBivE8dWiZ2rtIGl9gtJGpwqwjQEcKEBTOfvvrtrwBncYArJUO8Q==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.2.tgz",
+      "integrity": "sha512-/aUk1+GO/VPX0oVxhbzSWE1zrf3/wGCsO1ALNisVokYftKqfqLDjbJHE6mrI2hx3MiuwbHrWjJClkGUVTIOPEQ==",
       "dependencies": {
-        "@vue/reactivity": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/reactivity": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.4.tgz",
-      "integrity": "sha512-vbmwgTxku1BU87Kw7r29adv0OIrDXCW0PslOPQT0O/9R5SqcXgS94Yj6zsztDjvghegenwIAPNLlDR1Auh5s+w==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.2.tgz",
+      "integrity": "sha512-1Le/NpCfawCOfePfJezvWUF+oCVLU8N+IHN4oFDOxRe6/PgHNJ+yT+YdxFifBfI+TIAoXI/9PsnqzmJZV+xsmw==",
       "dependencies": {
-        "@vue/runtime-core": "3.1.4",
-        "@vue/shared": "3.1.4",
+        "@vue/runtime-core": "3.2.2",
+        "@vue/shared": "3.2.2",
         "csstype": "^2.6.8"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.4.tgz",
-      "integrity": "sha512-6O45kZAmkLvzGLToBxEz4lR2W6kXohCtebV2UxjH9GXjd8X9AhEn68FN9eNanFtWNzvgw1hqd6HkPRVQalqf7Q=="
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.2.tgz",
+      "integrity": "sha512-dvYb318tk9uOzHtSaT3WII/HscQSIRzoCZ5GyxEb3JlkEXASpAUAQwKnvSe2CudnF8XHFRTB7VITWSnWNLZUtA=="
     },
     "node_modules/@vue/web-component-wrapper": {
       "version": "1.3.0",
@@ -14608,6 +15205,16 @@
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
       "dev": true
     },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -14657,6 +15264,78 @@
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "dev": true
     },
+    "node_modules/babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      }
+    },
+    "node_modules/babel-code-frame/node_modules/ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/babel-code-frame/node_modules/ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/babel-code-frame/node_modules/chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^2.2.1",
+        "escape-string-regexp": "^1.0.2",
+        "has-ansi": "^2.0.0",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/babel-code-frame/node_modules/js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "dev": true
+    },
+    "node_modules/babel-code-frame/node_modules/strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/babel-code-frame/node_modules/supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
     "node_modules/babel-eslint": {
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@@ -15165,6 +15844,15 @@
       "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
       "dev": true
     },
+    "node_modules/builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/builtin-status-codes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@@ -17025,6 +17713,15 @@
       "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
       "dev": true
     },
+    "node_modules/diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/diffie-hellman": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -17496,6 +18193,21 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint-config-prettier": {
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz",
+      "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==",
+      "dev": true,
+      "dependencies": {
+        "get-stdin": "^6.0.0"
+      },
+      "bin": {
+        "eslint-config-prettier-check": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=3.14.1"
+      }
+    },
     "node_modules/eslint-loader": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz",
@@ -17514,6 +18226,28 @@
         "webpack": ">=2.0.0 <5.0.0"
       }
     },
+    "node_modules/eslint-plugin-prettier": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
+      "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "prettier-linter-helpers": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=5.0.0",
+        "prettier": ">=1.13.0"
+      },
+      "peerDependenciesMeta": {
+        "eslint-config-prettier": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/eslint-plugin-vue": {
       "version": "7.12.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.12.1.tgz",
@@ -18142,6 +18876,13 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "node_modules/fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "dev": true,
+      "peer": true
+    },
     "node_modules/fast-glob": {
       "version": "2.2.7",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
@@ -18171,6 +18912,15 @@
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
       "dev": true
     },
+    "node_modules/fastq": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
+      "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
+      "dev": true,
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
     "node_modules/faye-websocket": {
       "version": "0.11.4",
       "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
@@ -18427,6 +19177,313 @@
         "node": "*"
       }
     },
+    "node_modules/fork-ts-checker-webpack-plugin": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-3.1.1.tgz",
+      "integrity": "sha512-DuVkPNrM12jR41KM2e+N+styka0EgLkTnXmNcXdgOM37vtGeY+oCBK/Jx0hzSeEU6memFCtWb4htrHPMDfwwUQ==",
+      "dev": true,
+      "dependencies": {
+        "babel-code-frame": "^6.22.0",
+        "chalk": "^2.4.1",
+        "chokidar": "^3.3.0",
+        "micromatch": "^3.1.10",
+        "minimatch": "^3.0.4",
+        "semver": "^5.6.0",
+        "tapable": "^1.0.0",
+        "worker-rpc": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6.11.5",
+        "yarn": ">=1.0.0"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5": {
+      "name": "fork-ts-checker-webpack-plugin",
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
+      "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.8.3",
+        "@types/json-schema": "^7.0.5",
+        "chalk": "^4.1.0",
+        "cosmiconfig": "^6.0.0",
+        "deepmerge": "^4.2.2",
+        "fs-extra": "^9.0.0",
+        "memfs": "^3.1.2",
+        "minimatch": "^3.0.4",
+        "schema-utils": "2.7.0",
+        "semver": "^7.3.2",
+        "tapable": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=10",
+        "yarn": ">=1.0.0"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "optional": true
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/cosmiconfig": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+      "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.1.0",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.7.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/deepmerge": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+      "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/schema-utils": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+      "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.4",
+        "ajv": "^6.12.2",
+        "ajv-keywords": "^3.4.1"
+      },
+      "engines": {
+        "node": ">= 8.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/semver": {
+      "version": "7.3.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+      "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/universalify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/fork-ts-checker-webpack-plugin-v5/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true,
+      "optional": true
+    },
+    "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
     "node_modules/form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -18495,6 +19552,13 @@
         "node": ">=6 <7 || >=8"
       }
     },
+    "node_modules/fs-monkey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
+      "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
+      "dev": true,
+      "optional": true
+    },
     "node_modules/fs-write-stream-atomic": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@@ -18580,6 +19644,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/get-stdin": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
+      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/get-stream": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -18746,6 +19819,27 @@
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/has-ansi/node_modules/ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/has-bigints": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
@@ -20571,6 +21665,19 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/memfs": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz",
+      "integrity": "sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "fs-monkey": "1.0.3"
+      },
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/memory-fs": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -20629,6 +21736,12 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/microevent.ts": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz",
+      "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==",
+      "dev": true
+    },
     "node_modules/micromatch": {
       "version": "3.1.10",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -22621,7 +23734,6 @@
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
       "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
       "dev": true,
-      "optional": true,
       "bin": {
         "prettier": "bin-prettier.js"
       },
@@ -22629,6 +23741,19 @@
         "node": ">=4"
       }
     },
+    "node_modules/prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "fast-diff": "^1.1.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/pretty-error": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz",
@@ -22817,6 +23942,26 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "dev": true
     },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
     "node_modules/randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -23288,6 +24433,16 @@
         "node": ">= 4"
       }
     },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/rgb-regex": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
@@ -23331,6 +24486,29 @@
         "node": ">=0.12.0"
       }
     },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "node_modules/run-queue": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
@@ -24516,6 +25694,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/strip-eof": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -25108,6 +26295,83 @@
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
       "dev": true
     },
+    "node_modules/ts-loader": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz",
+      "integrity": "sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^2.3.0",
+        "enhanced-resolve": "^4.0.0",
+        "loader-utils": "^1.0.2",
+        "micromatch": "^4.0.0",
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8.6"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      }
+    },
+    "node_modules/ts-loader/node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ts-loader/node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ts-loader/node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/ts-loader/node_modules/micromatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+      "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+      "dev": true,
+      "dependencies": {
+        "braces": "^3.0.1",
+        "picomatch": "^2.2.3"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/ts-loader/node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
     "node_modules/ts-pnp": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
@@ -25122,12 +26386,177 @@
         }
       }
     },
+    "node_modules/tsconfig-paths": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz",
+      "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==",
+      "dev": true,
+      "dependencies": {
+        "json5": "^2.2.0",
+        "minimist": "^1.2.0",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.1.tgz",
+      "integrity": "sha512-n5CMlUUj+N5pjBhBACLq4jdr9cPTitySCjIosoQm0zwK99gmrcTGAfY9CwxRFT9+9OleNWXPRUcxsKP4AYExxQ==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "enhanced-resolve": "^5.7.0",
+        "tsconfig-paths": "^3.9.0"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/enhanced-resolve": {
+      "version": "5.8.2",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+      "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+      "dev": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tsconfig-paths-webpack-plugin/node_modules/tapable": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+      "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/tslib": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
       "dev": true
     },
+    "node_modules/tslint": {
+      "version": "5.20.1",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+      "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "builtin-modules": "^1.1.1",
+        "chalk": "^2.3.0",
+        "commander": "^2.12.1",
+        "diff": "^4.0.1",
+        "glob": "^7.1.1",
+        "js-yaml": "^3.13.1",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "resolve": "^1.3.2",
+        "semver": "^5.3.0",
+        "tslib": "^1.8.0",
+        "tsutils": "^2.29.0"
+      },
+      "bin": {
+        "tslint": "bin/tslint"
+      },
+      "engines": {
+        "node": ">=4.8.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev"
+      }
+    },
+    "node_modules/tslint/node_modules/semver": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/tsutils": {
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^1.8.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev"
+      }
+    },
     "node_modules/tty-browserify": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@@ -25195,6 +26624,19 @@
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
       "dev": true
     },
+    "node_modules/typescript": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
+      "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
     "node_modules/uglify-js": {
       "version": "3.4.10",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
@@ -25473,9 +26915,9 @@
       }
     },
     "node_modules/url-parse": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
-      "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
+      "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
       "dev": true,
       "dependencies": {
         "querystringify": "^2.1.1",
@@ -25609,13 +27051,21 @@
       "dev": true
     },
     "node_modules/vue": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.4.tgz",
-      "integrity": "sha512-p8dcdyeCgmaAiZsbLyDkmOLcFGZb/jEVdCLW65V68LRCXTNX8jKsgah2F7OZ/v/Ai2V0Fb1MNO0vz/GFqsPVMA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.2.tgz",
+      "integrity": "sha512-D/LuzAV30CgNJYGyNheE/VUs5N4toL2IgmS6c9qeOxvyh0xyn4exyRqizpXIrsvfx34zG9x5gCI2tdRHCGvF9w==",
       "dependencies": {
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/runtime-dom": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/runtime-dom": "3.2.2",
+        "@vue/shared": "3.2.2"
+      }
+    },
+    "node_modules/vue-class-component": {
+      "version": "8.0.0-rc.1",
+      "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz",
+      "integrity": "sha512-w1nMzsT/UdbDAXKqhwTmSoyuJzUXKrxLE77PCFVuC6syr8acdFDAq116xgvZh9UCuV0h+rlCtxXolr3Hi3HyPQ==",
+      "peerDependencies": {
+        "vue": "^3.0.0"
       }
     },
     "node_modules/vue-eslint-parser": {
@@ -25784,9 +27234,9 @@
       "dev": true
     },
     "node_modules/vue-next-select": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/vue-next-select/-/vue-next-select-2.7.0.tgz",
-      "integrity": "sha512-GJdps622YGD/vicDFqWtCOmYuWuIrriovpjnzNV4xWGkZI/b7RpFIJvykWv0Bq7d6wYoaJ59wt3CiV900F4wRw==",
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/vue-next-select/-/vue-next-select-2.9.0.tgz",
+      "integrity": "sha512-GjX4pHqZXXitquDeSAtLaf85jXdMUOKyCNzo+EF3xRr4DebGwbST4CtmRvL0TX3EhwLHQjUlAc3JcJX+azpLHg==",
       "hasInstallScript": true,
       "engines": {
         "node": ">=10"
@@ -26685,6 +28135,15 @@
         "errno": "~0.1.7"
       }
     },
+    "node_modules/worker-rpc": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz",
+      "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==",
+      "dev": true,
+      "dependencies": {
+        "microevent.ts": "~0.1.1"
+      }
+    },
     "node_modules/wrap-ansi": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -26824,6 +28283,16 @@
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
       "dev": true
     },
+    "node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/yargs": {
       "version": "16.2.0",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -28196,7 +29665,8 @@
     "@ivanv/vue-collapse-transition": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@ivanv/vue-collapse-transition/-/vue-collapse-transition-1.0.2.tgz",
-      "integrity": "sha512-eWEameFXJM/1khcoKbITvKjYYXDP1WKQ/Xf9ItJVPoEjCiOdocR3AgDAERzDrNNg4oWK28gRGi+0ft8Te27zxw=="
+      "integrity": "sha512-eWEameFXJM/1khcoKbITvKjYYXDP1WKQ/Xf9ItJVPoEjCiOdocR3AgDAERzDrNNg4oWK28gRGi+0ft8Te27zxw==",
+      "dev": true
     },
     "@mrmlnc/readdir-enhanced": {
       "version": "2.2.1",
@@ -28208,12 +29678,40 @@
         "glob-to-regexp": "^0.3.0"
       }
     },
+    "@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "dependencies": {
+        "@nodelib/fs.stat": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+          "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+          "dev": true
+        }
+      }
+    },
     "@nodelib/fs.stat": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
       "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
       "dev": true
     },
+    "@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "requires": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      }
+    },
     "@soda/friendly-errors-webpack-plugin": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -28315,6 +29813,21 @@
       "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.14.171",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
+      "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==",
+      "dev": true
+    },
+    "@types/lodash.clonedeep": {
+      "version": "4.5.6",
+      "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz",
+      "integrity": "sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA==",
+      "dev": true,
+      "requires": {
+        "@types/lodash": "*"
+      }
+    },
     "@types/mime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -28345,6 +29858,13 @@
       "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
       "dev": true
     },
+    "@types/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
+      "dev": true,
+      "optional": true
+    },
     "@types/q": {
       "version": "1.5.4",
       "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
@@ -28437,6 +29957,12 @@
         "http-proxy-middleware": "^1.0.0"
       }
     },
+    "@types/webpack-env": {
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.2.tgz",
+      "integrity": "sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw==",
+      "dev": true
+    },
     "@types/webpack-sources": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.1.tgz",
@@ -28456,6 +29982,307 @@
         }
       }
     },
+    "@typescript-eslint/eslint-plugin": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz",
+      "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/experimental-utils": "4.28.5",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "debug": "^4.3.1",
+        "functional-red-black-tree": "^1.0.1",
+        "regexpp": "^3.1.0",
+        "semver": "^7.3.5",
+        "tsutils": "^3.21.0"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "regexpp": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+          "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+          "dev": true
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "tsutils": {
+          "version": "3.21.0",
+          "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+          "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+          "dev": true,
+          "requires": {
+            "tslib": "^1.8.1"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
+      }
+    },
+    "@typescript-eslint/experimental-utils": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz",
+      "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==",
+      "dev": true,
+      "requires": {
+        "@types/json-schema": "^7.0.7",
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
+        "eslint-scope": "^5.1.1",
+        "eslint-utils": "^3.0.0"
+      },
+      "dependencies": {
+        "eslint-utils": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+          "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+          "dev": true,
+          "requires": {
+            "eslint-visitor-keys": "^2.0.0"
+          }
+        },
+        "eslint-visitor-keys": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+          "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+          "dev": true
+        }
+      }
+    },
+    "@typescript-eslint/parser": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz",
+      "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/scope-manager": "4.28.5",
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/typescript-estree": "4.28.5",
+        "debug": "^4.3.1"
+      }
+    },
+    "@typescript-eslint/scope-manager": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz",
+      "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5"
+      }
+    },
+    "@typescript-eslint/types": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz",
+      "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==",
+      "dev": true
+    },
+    "@typescript-eslint/typescript-estree": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz",
+      "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "4.28.5",
+        "@typescript-eslint/visitor-keys": "4.28.5",
+        "debug": "^4.3.1",
+        "globby": "^11.0.3",
+        "is-glob": "^4.0.1",
+        "semver": "^7.3.5",
+        "tsutils": "^3.21.0"
+      },
+      "dependencies": {
+        "@nodelib/fs.stat": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+          "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+          "dev": true
+        },
+        "array-union": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+          "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+          "dev": true
+        },
+        "braces": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+          "dev": true,
+          "requires": {
+            "fill-range": "^7.0.1"
+          }
+        },
+        "dir-glob": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+          "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+          "dev": true,
+          "requires": {
+            "path-type": "^4.0.0"
+          }
+        },
+        "fast-glob": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
+          "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+          "dev": true,
+          "requires": {
+            "@nodelib/fs.stat": "^2.0.2",
+            "@nodelib/fs.walk": "^1.2.3",
+            "glob-parent": "^5.1.2",
+            "merge2": "^1.3.0",
+            "micromatch": "^4.0.4"
+          }
+        },
+        "fill-range": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+          "dev": true,
+          "requires": {
+            "to-regex-range": "^5.0.1"
+          }
+        },
+        "glob-parent": {
+          "version": "5.1.2",
+          "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+          "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+          "dev": true,
+          "requires": {
+            "is-glob": "^4.0.1"
+          }
+        },
+        "globby": {
+          "version": "11.0.4",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
+          "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
+          "dev": true,
+          "requires": {
+            "array-union": "^2.1.0",
+            "dir-glob": "^3.0.1",
+            "fast-glob": "^3.1.1",
+            "ignore": "^5.1.4",
+            "merge2": "^1.3.0",
+            "slash": "^3.0.0"
+          }
+        },
+        "ignore": {
+          "version": "5.1.8",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
+          "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
+          "dev": true
+        },
+        "is-number": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+          "dev": true
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "micromatch": {
+          "version": "4.0.4",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+          "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+          "dev": true,
+          "requires": {
+            "braces": "^3.0.1",
+            "picomatch": "^2.2.3"
+          }
+        },
+        "path-type": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+          "dev": true
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "slash": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+          "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+          "dev": true
+        },
+        "to-regex-range": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+          "dev": true,
+          "requires": {
+            "is-number": "^7.0.0"
+          }
+        },
+        "tsutils": {
+          "version": "3.21.0",
+          "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+          "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+          "dev": true,
+          "requires": {
+            "tslib": "^1.8.1"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true
+        }
+      }
+    },
+    "@typescript-eslint/visitor-keys": {
+      "version": "4.28.5",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz",
+      "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "4.28.5",
+        "eslint-visitor-keys": "^2.0.0"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+          "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+          "dev": true
+        }
+      }
+    },
     "@vue/babel-helper-vue-jsx-merge-props": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@@ -28672,6 +30499,25 @@
         "@vue/cli-shared-utils": "^4.5.13"
       }
     },
+    "@vue/cli-plugin-typescript": {
+      "version": "4.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/cli-plugin-typescript/-/cli-plugin-typescript-4.5.13.tgz",
+      "integrity": "sha512-CpLlIdFNV1gn9uC4Yh6QgWI42uk2x5Z3cb2ScxNSwWsR1vgSdr0/1DdNzoBm68aP8RUtnHHO/HZfPnvXiq42xA==",
+      "dev": true,
+      "requires": {
+        "@types/webpack-env": "^1.15.2",
+        "@vue/cli-shared-utils": "^4.5.13",
+        "cache-loader": "^4.1.0",
+        "fork-ts-checker-webpack-plugin": "^3.1.1",
+        "fork-ts-checker-webpack-plugin-v5": "npm:fork-ts-checker-webpack-plugin@^5.0.11",
+        "globby": "^9.2.0",
+        "thread-loader": "^2.1.3",
+        "ts-loader": "^6.2.2",
+        "tslint": "^5.20.1",
+        "webpack": "^4.0.0",
+        "yorkie": "^2.0.0"
+      }
+    },
     "@vue/cli-plugin-vuex": {
       "version": "4.5.13",
       "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.13.tgz",
@@ -28781,13 +30627,13 @@
       }
     },
     "@vue/compiler-core": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.4.tgz",
-      "integrity": "sha512-TnUz+1z0y74O/A4YKAbzsdUfamyHV73MihrEfvettWpm9bQKVoZd1nEmR1cGN9LsXWlwAvVQBetBlWdOjmQO5Q==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.2.tgz",
+      "integrity": "sha512-QhCI0ZU5nAR0LMcLgzW3v75374tIrHGp8XG5CzJS7Nsy+iuignbE4MZ2XJfh5TGIrtpuzfWA4eTIfukZf/cRdg==",
       "requires": {
         "@babel/parser": "^7.12.0",
         "@babel/types": "^7.12.0",
-        "@vue/shared": "3.1.4",
+        "@vue/shared": "3.2.2",
         "estree-walker": "^2.0.1",
         "source-map": "^0.6.1"
       },
@@ -28800,27 +30646,27 @@
       }
     },
     "@vue/compiler-dom": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.4.tgz",
-      "integrity": "sha512-3tG2ScHkghhUBuFwl9KgyZhrS8CPFZsO7hUDekJgIp5b1OMkROr4AvxHu6rRMl4WkyvYkvidFNBS2VfOnwa6Kw==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.2.tgz",
+      "integrity": "sha512-ggcc+NV/ENIE0Uc3TxVE/sKrhYVpLepMAAmEiQ047332mbKOvUkowz4TTFZ+YkgOIuBOPP0XpCxmCMg7p874mA==",
       "requires": {
-        "@vue/compiler-core": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-core": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "@vue/compiler-sfc": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.1.4.tgz",
-      "integrity": "sha512-4KDQg60Khy3SgnF+V/TB2NZqzmM4TyGRmzsxqG1SebGdMSecCweFDSlI/F1vDYk6dKiCHgmpoT9A1sLxswkJ0A==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.2.tgz",
+      "integrity": "sha512-hrtqpQ5L6IPn5v7yVRo7uvLcQxv0z1+KBjZBWMBOcrXz4t+PKUxU/SWd6Tl9T8FDmYlunzKUh6lcx+2CLo6f5A==",
       "dev": true,
       "requires": {
         "@babel/parser": "^7.13.9",
         "@babel/types": "^7.13.0",
         "@types/estree": "^0.0.48",
-        "@vue/compiler-core": "3.1.4",
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/compiler-ssr": "3.1.4",
-        "@vue/shared": "3.1.4",
+        "@vue/compiler-core": "3.2.2",
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/compiler-ssr": "3.2.2",
+        "@vue/shared": "3.2.2",
         "consolidate": "^0.16.0",
         "estree-walker": "^2.0.1",
         "hash-sum": "^2.0.0",
@@ -28912,13 +30758,13 @@
       }
     },
     "@vue/compiler-ssr": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.1.4.tgz",
-      "integrity": "sha512-Box8fCuCFPp0FuimIswjDkjwiSDCBkHvt/xVALyFkYCiIMWv2eR53fIjmlsnEHhcBuZ+VgRC+UanCTcKvSA1gA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.2.tgz",
+      "integrity": "sha512-rVl1agMFhdEN3Go0bCriXo+3cysxKIuRP0yh1Wd8ysRrKfAmokyDhUA8PrGSq2Ymj/LdZTh+4OKfj3p2+C+hlA==",
       "dev": true,
       "requires": {
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "@vue/component-compiler-utils": {
@@ -28977,6 +30823,24 @@
         }
       }
     },
+    "@vue/eslint-config-prettier": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz",
+      "integrity": "sha512-wFQmv45c3ige5EA+ngijq40YpVcIkAy0Lihupnsnd1Dao5CBbPyfCzqtejFLZX1EwH/kCJdpz3t6s+5wd3+KxQ==",
+      "dev": true,
+      "requires": {
+        "eslint-config-prettier": "^6.0.0"
+      }
+    },
+    "@vue/eslint-config-typescript": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-7.0.0.tgz",
+      "integrity": "sha512-UxUlvpSrFOoF8aQ+zX1leYiEBEm7CZmXYn/ZEM1zwSadUzpamx56RB4+Htdjisv1mX2tOjBegNUqH3kz2OL+Aw==",
+      "dev": true,
+      "requires": {
+        "vue-eslint-parser": "^7.0.0"
+      }
+    },
     "@vue/preload-webpack-plugin": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
@@ -28985,36 +30849,36 @@
       "requires": {}
     },
     "@vue/reactivity": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.4.tgz",
-      "integrity": "sha512-YDlgii2Cr9yAoKVZFzgY4j0mYlVT73986X3e5SPp6ifqckSEoFSUWXZK2Tb53TB/9qO29BEEbspnKD3m3wAwkA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.2.tgz",
+      "integrity": "sha512-IHjhtmrhK6dzacj/EnLQDWOaA3HuzzVk6w84qgV8EpS4uWGIJXiRalMRg6XvGW2ykJvIl3pLsF0aBFlTMRiLOA==",
       "requires": {
-        "@vue/shared": "3.1.4"
+        "@vue/shared": "3.2.2"
       }
     },
     "@vue/runtime-core": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.4.tgz",
-      "integrity": "sha512-qmVJgJuFxfT7M4qHQ4M6KqhKC66fjuswK+aBivE8dWiZ2rtIGl9gtJGpwqwjQEcKEBTOfvvrtrwBncYArJUO8Q==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.2.tgz",
+      "integrity": "sha512-/aUk1+GO/VPX0oVxhbzSWE1zrf3/wGCsO1ALNisVokYftKqfqLDjbJHE6mrI2hx3MiuwbHrWjJClkGUVTIOPEQ==",
       "requires": {
-        "@vue/reactivity": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/reactivity": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
     "@vue/runtime-dom": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.4.tgz",
-      "integrity": "sha512-vbmwgTxku1BU87Kw7r29adv0OIrDXCW0PslOPQT0O/9R5SqcXgS94Yj6zsztDjvghegenwIAPNLlDR1Auh5s+w==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.2.tgz",
+      "integrity": "sha512-1Le/NpCfawCOfePfJezvWUF+oCVLU8N+IHN4oFDOxRe6/PgHNJ+yT+YdxFifBfI+TIAoXI/9PsnqzmJZV+xsmw==",
       "requires": {
-        "@vue/runtime-core": "3.1.4",
-        "@vue/shared": "3.1.4",
+        "@vue/runtime-core": "3.2.2",
+        "@vue/shared": "3.2.2",
         "csstype": "^2.6.8"
       }
     },
     "@vue/shared": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.4.tgz",
-      "integrity": "sha512-6O45kZAmkLvzGLToBxEz4lR2W6kXohCtebV2UxjH9GXjd8X9AhEn68FN9eNanFtWNzvgw1hqd6HkPRVQalqf7Q=="
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.2.tgz",
+      "integrity": "sha512-dvYb318tk9uOzHtSaT3WII/HscQSIRzoCZ5GyxEb3JlkEXASpAUAQwKnvSe2CudnF8XHFRTB7VITWSnWNLZUtA=="
     },
     "@vue/web-component-wrapper": {
       "version": "1.3.0",
@@ -29495,6 +31359,13 @@
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
       "dev": true
     },
+    "at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "dev": true,
+      "optional": true
+    },
     "atob": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -29528,6 +31399,65 @@
       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
       "dev": true
     },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "esutils": "^2.0.2",
+        "js-tokens": "^3.0.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "js-tokens": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+          "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
     "babel-eslint": {
       "version": "10.1.0",
       "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@@ -29952,6 +31882,12 @@
       "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
       "dev": true
     },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
     "builtin-status-codes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@@ -31442,6 +33378,12 @@
       "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
       "dev": true
     },
+    "diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "dev": true
+    },
     "diffie-hellman": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -31875,6 +33817,15 @@
         }
       }
     },
+    "eslint-config-prettier": {
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz",
+      "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==",
+      "dev": true,
+      "requires": {
+        "get-stdin": "^6.0.0"
+      }
+    },
     "eslint-loader": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz",
@@ -31888,6 +33839,16 @@
         "rimraf": "^2.6.1"
       }
     },
+    "eslint-plugin-prettier": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
+      "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
+      "dev": true,
+      "peer": true,
+      "requires": {
+        "prettier-linter-helpers": "^1.0.0"
+      }
+    },
     "eslint-plugin-vue": {
       "version": "7.12.1",
       "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.12.1.tgz",
@@ -32341,6 +34302,13 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "fast-diff": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+      "dev": true,
+      "peer": true
+    },
     "fast-glob": {
       "version": "2.2.7",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
@@ -32367,6 +34335,15 @@
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
       "dev": true
     },
+    "fastq": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz",
+      "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==",
+      "dev": true,
+      "requires": {
+        "reusify": "^1.0.4"
+      }
+    },
     "faye-websocket": {
       "version": "0.11.4",
       "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
@@ -32561,6 +34538,236 @@
       "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
       "dev": true
     },
+    "fork-ts-checker-webpack-plugin": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-3.1.1.tgz",
+      "integrity": "sha512-DuVkPNrM12jR41KM2e+N+styka0EgLkTnXmNcXdgOM37vtGeY+oCBK/Jx0hzSeEU6memFCtWb4htrHPMDfwwUQ==",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "^6.22.0",
+        "chalk": "^2.4.1",
+        "chokidar": "^3.3.0",
+        "micromatch": "^3.1.10",
+        "minimatch": "^3.0.4",
+        "semver": "^5.6.0",
+        "tapable": "^1.0.0",
+        "worker-rpc": "^0.1.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
+      }
+    },
+    "fork-ts-checker-webpack-plugin-v5": {
+      "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
+      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
+      "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@types/json-schema": "^7.0.5",
+        "chalk": "^4.1.0",
+        "cosmiconfig": "^6.0.0",
+        "deepmerge": "^4.2.2",
+        "fs-extra": "^9.0.0",
+        "memfs": "^3.1.2",
+        "minimatch": "^3.0.4",
+        "schema-utils": "2.7.0",
+        "semver": "^7.3.2",
+        "tapable": "^1.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
+        "cosmiconfig": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+          "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/parse-json": "^4.0.0",
+            "import-fresh": "^3.1.0",
+            "parse-json": "^5.0.0",
+            "path-type": "^4.0.0",
+            "yaml": "^1.7.2"
+          }
+        },
+        "deepmerge": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+          "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+          "dev": true,
+          "optional": true
+        },
+        "fs-extra": {
+          "version": "9.1.0",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+          "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "at-least-node": "^1.0.0",
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^6.0.1",
+            "universalify": "^2.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "import-fresh": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+          "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "parent-module": "^1.0.0",
+            "resolve-from": "^4.0.0"
+          }
+        },
+        "jsonfile": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+          "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "graceful-fs": "^4.1.6",
+            "universalify": "^2.0.0"
+          }
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "parse-json": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+          "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-even-better-errors": "^2.3.0",
+            "lines-and-columns": "^1.1.6"
+          }
+        },
+        "path-type": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+          "dev": true,
+          "optional": true
+        },
+        "resolve-from": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+          "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+          "dev": true,
+          "optional": true
+        },
+        "schema-utils": {
+          "version": "2.7.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+          "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/json-schema": "^7.0.4",
+            "ajv": "^6.12.2",
+            "ajv-keywords": "^3.4.1"
+          }
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "universalify": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+          "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+          "dev": true,
+          "optional": true
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
     "form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -32614,6 +34821,13 @@
         "universalify": "^0.1.0"
       }
     },
+    "fs-monkey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
+      "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==",
+      "dev": true,
+      "optional": true
+    },
     "fs-write-stream-atomic": {
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@@ -32683,6 +34897,12 @@
         "has-symbols": "^1.0.1"
       }
     },
+    "get-stdin": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
+      "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
+      "dev": true
+    },
     "get-stream": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@@ -32817,6 +35037,23 @@
         "function-bind": "^1.1.1"
       }
     },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+          "dev": true
+        }
+      }
+    },
     "has-bigints": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
@@ -34224,6 +36461,16 @@
       "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
       "dev": true
     },
+    "memfs": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz",
+      "integrity": "sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "fs-monkey": "1.0.3"
+      }
+    },
     "memory-fs": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -34275,6 +36522,12 @@
       "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
       "dev": true
     },
+    "microevent.ts": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz",
+      "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==",
+      "dev": true
+    },
     "micromatch": {
       "version": "3.1.10",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -35926,8 +38179,17 @@
       "version": "1.19.1",
       "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
       "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
+      "dev": true
+    },
+    "prettier-linter-helpers": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+      "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
       "dev": true,
-      "optional": true
+      "peer": true,
+      "requires": {
+        "fast-diff": "^1.1.2"
+      }
     },
     "pretty-error": {
       "version": "2.1.2",
@@ -36092,6 +38354,12 @@
       "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
       "dev": true
     },
+    "queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -36465,6 +38733,12 @@
       "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
       "dev": true
     },
+    "reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true
+    },
     "rgb-regex": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
@@ -36502,6 +38776,15 @@
       "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
       "dev": true
     },
+    "run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "requires": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
     "run-queue": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
@@ -37496,6 +39779,12 @@
         "ansi-regex": "^4.1.0"
       }
     },
+    "strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "dev": true
+    },
     "strip-eof": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -37973,18 +40262,203 @@
       "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
       "dev": true
     },
+    "ts-loader": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.2.tgz",
+      "integrity": "sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.3.0",
+        "enhanced-resolve": "^4.0.0",
+        "loader-utils": "^1.0.2",
+        "micromatch": "^4.0.0",
+        "semver": "^6.0.0"
+      },
+      "dependencies": {
+        "braces": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+          "dev": true,
+          "requires": {
+            "fill-range": "^7.0.1"
+          }
+        },
+        "fill-range": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+          "dev": true,
+          "requires": {
+            "to-regex-range": "^5.0.1"
+          }
+        },
+        "is-number": {
+          "version": "7.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+          "dev": true
+        },
+        "micromatch": {
+          "version": "4.0.4",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+          "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+          "dev": true,
+          "requires": {
+            "braces": "^3.0.1",
+            "picomatch": "^2.2.3"
+          }
+        },
+        "to-regex-range": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+          "dev": true,
+          "requires": {
+            "is-number": "^7.0.0"
+          }
+        }
+      }
+    },
     "ts-pnp": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
       "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==",
       "dev": true
     },
+    "tsconfig-paths": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz",
+      "integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==",
+      "dev": true,
+      "requires": {
+        "json5": "^2.2.0",
+        "minimist": "^1.2.0",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "tsconfig-paths-webpack-plugin": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.1.tgz",
+      "integrity": "sha512-n5CMlUUj+N5pjBhBACLq4jdr9cPTitySCjIosoQm0zwK99gmrcTGAfY9CwxRFT9+9OleNWXPRUcxsKP4AYExxQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "enhanced-resolve": "^5.7.0",
+        "tsconfig-paths": "^3.9.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "enhanced-resolve": {
+          "version": "5.8.2",
+          "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+          "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.2.4",
+            "tapable": "^2.2.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "tapable": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+          "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+          "dev": true
+        }
+      }
+    },
     "tslib": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
       "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
       "dev": true
     },
+    "tslint": {
+      "version": "5.20.1",
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
+      "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "builtin-modules": "^1.1.1",
+        "chalk": "^2.3.0",
+        "commander": "^2.12.1",
+        "diff": "^4.0.1",
+        "glob": "^7.1.1",
+        "js-yaml": "^3.13.1",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "resolve": "^1.3.2",
+        "semver": "^5.3.0",
+        "tslib": "^1.8.0",
+        "tsutils": "^2.29.0"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        }
+      }
+    },
+    "tsutils": {
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.8.1"
+      }
+    },
     "tty-browserify": {
       "version": "0.0.0",
       "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@@ -38037,6 +40511,12 @@
       "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
       "dev": true
     },
+    "typescript": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
+      "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
+      "dev": true
+    },
     "uglify-js": {
       "version": "3.4.10",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
@@ -38266,9 +40746,9 @@
       }
     },
     "url-parse": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
-      "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
+      "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
       "dev": true,
       "requires": {
         "querystringify": "^2.1.1",
@@ -38378,15 +40858,21 @@
       "dev": true
     },
     "vue": {
-      "version": "3.1.4",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.4.tgz",
-      "integrity": "sha512-p8dcdyeCgmaAiZsbLyDkmOLcFGZb/jEVdCLW65V68LRCXTNX8jKsgah2F7OZ/v/Ai2V0Fb1MNO0vz/GFqsPVMA==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.2.tgz",
+      "integrity": "sha512-D/LuzAV30CgNJYGyNheE/VUs5N4toL2IgmS6c9qeOxvyh0xyn4exyRqizpXIrsvfx34zG9x5gCI2tdRHCGvF9w==",
       "requires": {
-        "@vue/compiler-dom": "3.1.4",
-        "@vue/runtime-dom": "3.1.4",
-        "@vue/shared": "3.1.4"
+        "@vue/compiler-dom": "3.2.2",
+        "@vue/runtime-dom": "3.2.2",
+        "@vue/shared": "3.2.2"
       }
     },
+    "vue-class-component": {
+      "version": "8.0.0-rc.1",
+      "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz",
+      "integrity": "sha512-w1nMzsT/UdbDAXKqhwTmSoyuJzUXKrxLE77PCFVuC6syr8acdFDAq116xgvZh9UCuV0h+rlCtxXolr3Hi3HyPQ==",
+      "requires": {}
+    },
     "vue-eslint-parser": {
       "version": "7.7.2",
       "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.7.2.tgz",
@@ -38511,9 +40997,9 @@
       }
     },
     "vue-next-select": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/vue-next-select/-/vue-next-select-2.7.0.tgz",
-      "integrity": "sha512-GJdps622YGD/vicDFqWtCOmYuWuIrriovpjnzNV4xWGkZI/b7RpFIJvykWv0Bq7d6wYoaJ59wt3CiV900F4wRw==",
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/vue-next-select/-/vue-next-select-2.9.0.tgz",
+      "integrity": "sha512-GjX4pHqZXXitquDeSAtLaf85jXdMUOKyCNzo+EF3xRr4DebGwbST4CtmRvL0TX3EhwLHQjUlAc3JcJX+azpLHg==",
       "requires": {}
     },
     "vue-sortable": {
@@ -39244,6 +41730,15 @@
         "errno": "~0.1.7"
       }
     },
+    "worker-rpc": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz",
+      "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==",
+      "dev": true,
+      "requires": {
+        "microevent.ts": "~0.1.1"
+      }
+    },
     "wrap-ansi": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -39355,6 +41850,13 @@
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
       "dev": true
     },
+    "yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "dev": true,
+      "optional": true
+    },
     "yargs": {
       "version": "16.2.0",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
diff --git a/package.json b/package.json
index b2db6c3..e65641f 100644
--- a/package.json
+++ b/package.json
@@ -8,44 +8,49 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "@ivanv/vue-collapse-transition": "^1.0.2",
     "core-js": "^3.6.5",
     "lodash.clonedeep": "^4.5.0",
-    "vue": "^3.1.4",
-    "vue-next-select": "^2.7.0",
+    "vue": "^3.2.2",
+    "vue-class-component": "^8.0.0-rc.1",
+    "vue-next-select": "^2.9.0",
     "vue-sortable": "github:Netbel/vue-sortable#master-fix",
     "vue-textarea-autosize": "^1.1.1",
     "vue-transition-expand": "^0.1.0"
   },
   "devDependencies": {
+    "@ivanv/vue-collapse-transition": "^1.0.2",
+    "@types/lodash.clonedeep": "^4.5.6",
+    "@typescript-eslint/eslint-plugin": "^4.18.0",
+    "@typescript-eslint/parser": "^4.18.0",
     "@vue/cli-plugin-babel": "~4.5.0",
     "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-typescript": "~4.5.0",
     "@vue/cli-service": "~4.5.0",
-    "@vue/compiler-sfc": "^3.0.0-beta.1",
+    "@vue/compiler-sfc": "^3.2.2",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "@vue/eslint-config-typescript": "^7.0.0",
     "babel-eslint": "^10.1.0",
     "eslint": "^6.7.2",
     "eslint-plugin-vue": "^7.0.0-alpha.0",
+    "prettier": "^1.19.1",
     "raw-loader": "^4.0.2",
     "sass": "^1.36.0",
-    "sass-loader": "^10.2.0"
-  },
-  "eslintConfig": {
-    "root": true,
-    "env": {
-      "node": true
-    },
-    "extends": [
-      "plugin:vue/vue3-essential",
-      "eslint:recommended"
-    ],
-    "parserOptions": {
-      "parser": "babel-eslint"
-    },
-    "rules": {}
+    "sass-loader": "^10.2.0",
+    "tsconfig-paths-webpack-plugin": "^3.5.1",
+    "typescript": "~4.1.5"
   },
   "browserslist": [
     "> 1%",
     "last 2 versions",
     "not dead"
-  ]
+  ],
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,vue}": [
+      "vue-cli-service lint",
+      "git add"
+    ]
+  }
 }
diff --git a/src/App.vue b/src/App.vue
index 1e09e8c..4000f51 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,53 +1,54 @@
 <template>
-	<div id="modal-root" :style="theme" />
-	<div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
-		<Nav v-if="useHeader" />
-		<Tabs />
-		<TPS v-if="showTPS" />
-		<GameOverScreen />
-		<NaNScreen />
-	</div>
+    <div id="modal-root" :style="theme" />
+    <div class="app" @mousemove="updateMouse" :style="theme" :class="{ useHeader }">
+        <Nav v-if="useHeader" />
+        <Tabs />
+        <TPS v-if="showTPS" />
+        <GameOverScreen />
+        <NaNScreen />
+    </div>
 </template>
 
-<script>
-import themes from './data/themes';
-import player from './game/player';
-import modInfo from './data/modInfo.json';
-import { mapState } from './util/vue';
-import './main.css';
+<script lang="ts">
+import { defineComponent } from "vue";
+import themes from "./data/themes";
+import player from "./game/player";
+import modInfo from "./data/modInfo.json";
+import { mapState } from "./util/vue";
+import "./main.css";
 
-export default {
-	name: 'App',
-	data() {
-		return { useHeader: modInfo.useHeader };
-	},
-	computed: {
-		...mapState([ 'showTPS' ]),
-		theme() {
-			return themes[player.theme].variables;
-		}
-	},
-	methods: {
-		updateMouse(/* event */) {
-			// TODO use event to update mouse position for particles
-		}
-	}
-};
+export default defineComponent({
+    name: "App",
+    data() {
+        return { useHeader: modInfo.useHeader };
+    },
+    computed: {
+        ...mapState(["showTPS"]),
+        theme() {
+            return themes[player.theme].variables;
+        }
+    },
+    methods: {
+        updateMouse(/* event */) {
+            // TODO use event to update mouse position for particles
+        }
+    }
+});
 </script>
 
 <style scoped>
 .app {
-	background-color: var(--background);
-	color: var(--color);
-	display: flex;
-	flex-flow: column;
-	min-height: 100%;
-	height: 100%;
+    background-color: var(--background);
+    color: var(--color);
+    display: flex;
+    flex-flow: column;
+    min-height: 100%;
+    height: 100%;
 }
 
 #modal-root {
     position: absolute;
-	min-height: 100%;
-	height: 100%;
+    min-height: 100%;
+    height: 100%;
 }
 </style>
diff --git a/src/components/features/Achievement.vue b/src/components/features/Achievement.vue
index 96288e4..4c4614b 100644
--- a/src/components/features/Achievement.vue
+++ b/src/components/features/Achievement.vue
@@ -1,52 +1,66 @@
 <template>
-	<tooltip v-if="achievement.unlocked" :display="tooltip">
-		<div :style="style"
-			:class="{ [layer || tab.layer]: true, feature: true, achievement: true, locked: !achievement.earned,
-				bought: achievement.earned }">
-			<component v-if="display" :is="display" />
-			<branch-node :branches="achievement.branches" :id="id" featureType="achievement" />
-		</div>
-	</tooltip>
+    <tooltip v-if="achievement.unlocked" :display="tooltip">
+        <div
+            :style="style"
+            :class="{
+                [layer]: true,
+                feature: true,
+                achievement: true,
+                locked: !achievement.earned,
+                bought: achievement.earned
+            }"
+        >
+            <component v-if="display" :is="display" />
+            <branch-node :branches="achievement.branches" :id="id" featureType="achievement" />
+        </div>
+    </tooltip>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { CoercableComponent } from "@/typings/component";
+import { Achievement } from "@/typings/features/achievement";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'achievement',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		achievement() {
-			return layers[this.layer || this.tab.layer].achievements[this.id];
-		},
-		style() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.achievement,
-				this.achievement.style,
-				this.achievement.image && this.achievement.earned ? {
-					backgroundImage: `url(${this.achievement.image}`
-				} : null
-			];
-		},
-		display() {
-			if (this.achievement.display) {
-				return coerceComponent(this.achievement.display, 'h3');
-			}
-			return coerceComponent(this.achievement.name, 'h3');
-		},
-		tooltip() {
-			if (this.achievement.earned) {
-				return this.achievement.doneTooltip || this.achievement.tooltip || "You did it!";
-			}
-			return this.achievement.goalTooltip || this.achievement.tooltip || "LOCKED";
-		}
-	}
-};
+export default defineComponent({
+    name: "achievement",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        achievement(): Achievement {
+            return layers[this.layer].achievements!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                layers[this.layer].componentStyles?.achievement,
+                this.achievement.style,
+                this.achievement.image && this.achievement.earned
+                    ? {
+                          backgroundImage: `url(${this.achievement.image}`
+                      }
+                    : undefined
+            ];
+        },
+        display(): Component | string {
+            if (this.achievement.display) {
+                return coerceComponent(this.achievement.display, "h3");
+            }
+            return coerceComponent(this.achievement.name!, "h3");
+        },
+        tooltip(): CoercableComponent {
+            if (this.achievement.earned) {
+                return this.achievement.doneTooltip || this.achievement.tooltip || "You did it!";
+            }
+            return this.achievement.goalTooltip || this.achievement.tooltip || "LOCKED";
+        }
+    }
+});
 </script>
 
 <style scoped>
diff --git a/src/components/features/Achievements.vue b/src/components/features/Achievements.vue
index 01975a3..f2e8d0f 100644
--- a/src/components/features/Achievements.vue
+++ b/src/components/features/Achievements.vue
@@ -1,36 +1,48 @@
 <template>
-	<div v-if="filteredAchievements" class="table">
-		<template v-if="filteredAchievements.rows && filteredAchievements.cols">
-			<div v-for="row in filteredAchievements.rows" class="row" :key="row">
-				<div v-for="col in filteredAchievements.cols" :key="col">
-					<achievement v-if="filteredAchievements[row * 10 + col] !== undefined" class="align" :id="row * 10 + col" />
-				</div>
-			</div>
-		</template>
-		<template v-frag v-else>
-			<achievement v-for="(achievement, id) in filteredAchievements" :key="id" :id="id" />
-		</template>
-	</div>
+    <div v-if="filteredAchievements" class="table">
+        <template v-if="filteredAchievements.rows && filteredAchievements.cols">
+            <div v-for="row in filteredAchievements.rows" class="row" :key="row">
+                <div v-for="col in filteredAchievements.cols" :key="col">
+                    <achievement
+                        v-if="filteredAchievements[row * 10 + col] !== undefined"
+                        class="align"
+                        :id="row * 10 + col"
+                    />
+                </div>
+            </div>
+        </template>
+        <template v-frag v-else>
+            <achievement v-for="(achievement, id) in filteredAchievements" :key="id" :id="id" />
+        </template>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Achievement } from "@/typings/features/achievement";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'achievements',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		achievements: Array
-	},
-	computed: {
-		filteredAchievements() {
-			return getFiltered(layers[this.layer || this.tab.layer].achievements, this.achievements);
-		}
-	}
-};
+export default defineComponent({
+    name: "achievements",
+    mixins: [InjectLayerMixin],
+    props: {
+        achievements: {
+            type: Object as PropType<Array<string>>
+        }
+    },
+    computed: {
+        filteredAchievements(): Record<string, Achievement> {
+            if (layers[this.layer].achievements) {
+                return getFiltered<Achievement>(
+                    layers[this.layer].achievements!.data,
+                    this.achievements
+                );
+            }
+            return {};
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/Bar.vue b/src/components/features/Bar.vue
index e6151a9..6c40f22 100644
--- a/src/components/features/Bar.vue
+++ b/src/components/features/Bar.vue
@@ -1,100 +1,104 @@
 <template>
-	<div v-if="bar.unlocked" :style="style" class="bar">
-		<div class="overlayTextContainer border" :style="borderStyle">
-			<component class="overlayText" :style="textStyle" :is="display" />
-		</div>
-		<div class="border" :style="backgroundStyle">
-			<div class="fill" :style="fillStyle" />
-		</div>
-		<branch-node :branches="bar.branches" :id="id" featureType="bar" />
-	</div>
+    <div v-if="bar.unlocked" :style="style" :class="{ [layer]: true, bar: true }">
+        <div class="overlayTextContainer border" :style="borderStyle">
+            <component class="overlayText" :style="textStyle" :is="display" />
+        </div>
+        <div class="border" :style="backgroundStyle">
+            <div class="fill" :style="fillStyle" />
+        </div>
+        <branch-node :branches="bar.branches" :id="id" featureType="bar" />
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { UP, DOWN, LEFT, RIGHT, DEFAULT, coerceComponent } from '../../util/vue';
-import Decimal from '../../util/bignum';
+<script lang="ts">
+import { Direction } from "@/game/enums";
+import { layers } from "@/game/layers";
+import { Bar } from "@/typings/features/bar";
+import Decimal from "@/util/bignum";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'bar',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		bar() {
-			return layers[this.layer || this.tab.layer].bars[this.id];
-		},
-		progress() {
-			let progress = this.bar.progress instanceof Decimal ? this.bar.progress.toNumber() : this.bar.progress;
-			return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
-		},
-		style() {
-			return [
-				{ 'width': this.bar.width + "px", 'height': this.bar.height + "px" },
-				layers[this.layer || this.tab.layer].componentStyles?.bar,
-				this.bar.style
-			];
-		},
-		borderStyle() {
-			return [
-				{ 'width': this.bar.width + "px", 'height': this.bar.height + "px" },
-				this.bar.borderStyle
-			];
-		},
-		textStyle() {
-			return [
-				this.bar.style,
-				this.bar.textStyle
-			];
-		},
-		backgroundStyle() {
-			return [
-				{ 'width': this.bar.width + "px", 'height': this.bar.height + "px" },
-				this.bar.style,
-				this.bar.baseStyle,
-				this.bar.borderStyle
-			];
-		},
-		fillStyle() {
-			const fillStyle = { 'width': (this.bar.width + 0.5) + "px", 'height': (this.bar.height + 0.5) + "px" };
-			switch (this.bar.direction) {
-				case UP:
-					fillStyle['clip-path'] = `inset(${this.progress}% 0% 0% 0%)`;
-					fillStyle.width = this.bar.width + 1 + 'px';
-					break;
-				case DOWN:
-					fillStyle['clip-path'] = `inset(0% 0% ${this.progress}% 0%)`;
-					fillStyle.width = this.bar.width + 1 + 'px';
-					break;
-				case RIGHT:
-					fillStyle['clip-path'] = `inset(0% ${this.progress}% 0% 0%)`;
-					break;
-				case LEFT:
-					fillStyle['clip-path'] = `inset(0% 0% 0% ${this.progress} + '%)`;
-					break;
-				case DEFAULT:
-					fillStyle['clip-path'] = 'inset(0% 50% 0% 0%)';
-					break;
-			}
-			return [
-				fillStyle,
-				this.bar.style,
-				this.bar.fillStyle
-			];
-		},
-		display() {
-			return coerceComponent(this.bar.display);
-		}
-	}
-};
+export default defineComponent({
+    name: "bar",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        bar(): Bar {
+            return layers[this.layer].bars!.data[this.id];
+        },
+        progress(): number {
+            let progress =
+                this.bar.progress instanceof Decimal
+                    ? this.bar.progress.toNumber()
+                    : (this.bar.progress as number);
+            return (1 - Math.min(Math.max(progress, 0), 1)) * 100;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                { width: this.bar.width + "px", height: this.bar.height + "px" },
+                layers[this.layer].componentStyles?.bar,
+                this.bar.style
+            ];
+        },
+        borderStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                { width: this.bar.width + "px", height: this.bar.height + "px" },
+                this.bar.borderStyle
+            ];
+        },
+        textStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [this.bar.style, this.bar.textStyle];
+        },
+        backgroundStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                { width: this.bar.width + "px", height: this.bar.height + "px" },
+                this.bar.style,
+                this.bar.baseStyle,
+                this.bar.borderStyle
+            ];
+        },
+        fillStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            const fillStyle: Partial<CSSStyleDeclaration> = {
+                width: this.bar.width + 0.5 + "px",
+                height: this.bar.height + 0.5 + "px"
+            };
+            switch (this.bar.direction) {
+                case Direction.Up:
+                    fillStyle.clipPath = `inset(${this.progress}% 0% 0% 0%)`;
+                    fillStyle.width = this.bar.width + 1 + "px";
+                    break;
+                case Direction.Down:
+                    fillStyle.clipPath = `inset(0% 0% ${this.progress}% 0%)`;
+                    fillStyle.width = this.bar.width + 1 + "px";
+                    break;
+                case Direction.Right:
+                    fillStyle.clipPath = `inset(0% ${this.progress}% 0% 0%)`;
+                    break;
+                case Direction.Left:
+                    fillStyle.clipPath = `inset(0% 0% 0% ${this.progress} + '%)`;
+                    break;
+                case Direction.Default:
+                    fillStyle.clipPath = "inset(0% 50% 0% 0%)";
+                    break;
+            }
+            return [fillStyle, this.bar.style, this.bar.fillStyle];
+        },
+        display(): Component | string {
+            return coerceComponent(this.bar.display);
+        }
+    }
+});
 </script>
 
 <style scoped>
 .bar {
-	position: relative;
-	display: table;
+    position: relative;
+    display: table;
 }
 
 .overlayTextContainer {
@@ -103,19 +107,19 @@ export default {
     vertical-align: middle;
     display: flex;
     justify-content: center;
-	z-index: 3;
+    z-index: 3;
 }
 
 .overlayText {
-	z-index: 6;
+    z-index: 6;
 }
 
 .border {
-	border: 2px solid;
+    border: 2px solid;
     border-radius: 10px;
     border-color: var(--color);
     overflow: hidden;
-    -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);
+    mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);
     margin: 0;
 }
 
diff --git a/src/components/features/Buyable.vue b/src/components/features/Buyable.vue
index ee8ff97..6b4ddaa 100644
--- a/src/components/features/Buyable.vue
+++ b/src/components/features/Buyable.vue
@@ -1,91 +1,137 @@
 <template>
-	<div v-if="buyable.unlocked" style="display: grid">
-		<button :style="style" @click="buyable.buy" @mousedown="start" @mouseleave="stop" @mouseup="stop" @touchstart="start"
-			:class="{ feature: true, [layer || tab.layer]: true, buyable: true, can: buyable.canBuy, locked: !buyable.canAfford, bought }"
-			@touchend="stop" @touchcancel="stop" :disabled="!buyable.canBuy">
-			<div v-if="title">
-				<component :is="title" />
-			</div>
-			<component :is="display" style="white-space: pre-line;" />
-			<mark-node :mark="buyable.mark" />
-			<branch-node :branches="buyable.branches" :id="id" featureType="buyable" />
-		</button>
-		<div v-if="(buyable.sellOne !== undefined && buyable.canSellOne !== false) ||
-			(buyable.sellAll !== undefined && buyable.canSellAll !== false)" style="width: 100%">
-			<button @click="buyable.sellAll" v-if="buyable.sellAll !== undefined && buyable.canSellAll !== false"
-				:class="{ 'buyable-button': true, can: buyable.unlocked, locked: !buyable.unlocked, feature: true }"
-				:style="{ 'background-color': buyable.canSellAll ? layerColor : '' }">
-				Sell All
-			</button>
-			<button @click="buyable.sellOne" v-if="buyable.sellOne !== undefined && buyable.canSellOne !== false"
-				:class="{ 'buyable-button': true, can: buyable.unlocked, locked: !buyable.unlocked, feature: true }"
-				:style="{ 'background-color': buyable.canSellOne ? layerColor : '' }">
-				Sell One
-			</button>
-		</div>
-	</div>
+    <div v-if="buyable.unlocked" style="display: grid">
+        <button
+            :style="style"
+            @click="buyable.buy"
+            @mousedown="start"
+            @mouseleave="stop"
+            @mouseup="stop"
+            @touchstart="start"
+            :class="{
+                feature: true,
+                [layer]: true,
+                buyable: true,
+                can: buyable.canBuy,
+                locked: !buyable.canAfford,
+                bought
+            }"
+            @touchend="stop"
+            @touchcancel="stop"
+            :disabled="!buyable.canBuy"
+        >
+            <div v-if="title">
+                <component :is="title" />
+            </div>
+            <component :is="display" style="white-space: pre-line;" />
+            <mark-node :mark="buyable.mark" />
+            <branch-node :branches="buyable.branches" :id="id" featureType="buyable" />
+        </button>
+        <div
+            v-if="
+                (buyable.sellOne !== undefined && buyable.canSellOne !== false) ||
+                    (buyable.sellAll !== undefined && buyable.canSellAll !== false)
+            "
+            style="width: 100%"
+        >
+            <button
+                @click="buyable.sellAll"
+                v-if="buyable.sellAll !== undefined && buyable.canSellAll !== false"
+                :class="{
+                    'buyable-button': true,
+                    can: buyable.unlocked,
+                    locked: !buyable.unlocked,
+                    feature: true
+                }"
+                :style="{ 'background-color': buyable.canSellAll ? layerColor : '' }"
+            >
+                Sell All
+            </button>
+            <button
+                @click="buyable.sellOne"
+                v-if="buyable.sellOne !== undefined && buyable.canSellOne !== false"
+                :class="{
+                    'buyable-button': true,
+                    can: buyable.unlocked,
+                    locked: !buyable.unlocked,
+                    feature: true
+                }"
+                :style="{ 'background-color': buyable.canSellOne ? layerColor : '' }"
+            >
+                Sell One
+            </button>
+        </div>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Buyable } from "@/typings/features/buyable";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'buyable',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ],
-		size: [ Number, String ]
-	},
-	data() {
-		return {
-			interval: false,
-			time: 0
-		};
-	},
-	computed: {
-		buyable() {
-			return layers[this.layer || this.tab.layer].buyables[this.id];
-		},
-		bought() {
-			return player[this.layer || this.tab.layer].buyables[this.id].gte(this.buyable.purchaseLimit);
-		},
-		style() {
-			return [
-				this.buyable.canBuy ? { 'background-color': layers[this.layer || this.tab.layer].color } : {},
-				this.size ? { 'height': this.size, 'width': this.size } : {},
-				layers[this.layer || this.tab.layer].componentStyles?.buyable,
-				this.buyable.style
-			];
-		},
-		title() {
-			if (this.buyable.title) {
-				return coerceComponent(this.buyable.title, 'h2');
-			}
-			return null;
-		},
-		display() {
-			return coerceComponent(this.buyable.display, 'div');
-		},
-		layerColor() {
-			return layers[this.layer || this.tab.layer].color;
-		}
-	},
-	methods: {
-		start() {
-			if (!this.interval) {
-				this.interval = setInterval(this.buyable.buy, 250);
-			}
-		},
-		stop() {
-			clearInterval(this.interval);
-			this.interval = false;
-			this.time = 0;
-		}
-	}
-};
+export default defineComponent({
+    name: "buyable",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        },
+        size: Number
+    },
+    data() {
+        return {
+            interval: null,
+            time: 0
+        } as {
+            interval: number | null;
+            time: number;
+        };
+    },
+    computed: {
+        buyable(): Buyable {
+            return layers[this.layer].buyables!.data[this.id];
+        },
+        bought(): boolean {
+            return player.layers[this.layer].buyables[this.id].gte(this.buyable.purchaseLimit);
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.buyable.canBuy ? { backgroundColor: layers[this.layer].color } : undefined,
+                this.size ? { height: this.size + "px", width: this.size + "px" } : undefined,
+                layers[this.layer].componentStyles?.buyable,
+                this.buyable.style
+            ];
+        },
+        title(): Component | string | null {
+            if (this.buyable.title) {
+                return coerceComponent(this.buyable.title, "h2");
+            }
+            return null;
+        },
+        display(): Component | string {
+            return coerceComponent(this.buyable.display, "div");
+        },
+        layerColor(): string {
+            return layers[this.layer].color;
+        }
+    },
+    methods: {
+        start() {
+            if (!this.interval) {
+                this.interval = setInterval(this.buyable.buy, 250);
+            }
+        },
+        stop() {
+            if (this.interval) {
+                clearInterval(this.interval);
+                this.interval = null;
+                this.time = 0;
+            }
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -96,6 +142,6 @@ export default {
 }
 
 .buyable-button {
-	width: calc(100% - 10px);
+    width: calc(100% - 10px);
 }
 </style>
diff --git a/src/components/features/Buyables.vue b/src/components/features/Buyables.vue
index 97de3db..5683b24 100644
--- a/src/components/features/Buyables.vue
+++ b/src/components/features/Buyables.vue
@@ -1,68 +1,90 @@
 <template>
-	<div v-if="filteredBuyables" class="table">
-		<respec-button v-if="showRespec" style="margin-bottom: 12px;" :confirmRespec="confirmRespec"
-			@set-confirm-respec="setConfirmRespec" @respec="respec" />
-		<template v-if="filteredBuyables.rows && filteredBuyables.cols">
-			<div v-for="row in filteredBuyables.rows" class="row" :key="row">
-				<div v-for="col in filteredBuyables.cols" :key="col">
-					<buyable v-if="filteredBuyables[row * 10 + col] !== undefined" class="align buyable-container" :style="{ height }"
-						:id="row * 10 + col" :size="height === 'inherit' ? null : height" />
-				</div>
-			</div>
-		</template>
-		<row v-else>
-			<buyable v-for="(buyable, id) in filteredBuyables" :key="id" class="align buyable-container"
-				:style="{ height }" :id="id" :size="height === 'inherit' ? null : height" />
-		</row>
-	</div>
+    <div v-if="filteredBuyables" class="table">
+        <respec-button
+            v-if="showRespec"
+            style="margin-bottom: 12px;"
+            :confirmRespec="confirmRespec"
+            :respecWarningDisplay="respecWarningDisplay"
+            @set-confirm-respec="setConfirmRespec"
+            @respec="respec"
+        />
+        <template v-if="filteredBuyables.rows && filteredBuyables.cols">
+            <div v-for="row in filteredBuyables.rows" class="row" :key="row">
+                <div v-for="col in filteredBuyables.cols" :key="col">
+                    <buyable
+                        v-if="filteredBuyables[row * 10 + col] !== undefined"
+                        class="align buyable-container"
+                        :style="{ height }"
+                        :id="row * 10 + col"
+                        :size="height === 'inherit' ? null : height"
+                    />
+                </div>
+            </div>
+        </template>
+        <row v-else>
+            <buyable
+                v-for="(buyable, id) in filteredBuyables"
+                :key="id"
+                class="align buyable-container"
+                :style="{ height }"
+                :id="id"
+                :size="height === 'inherit' ? null : height"
+            />
+        </row>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { CoercableComponent } from "@/typings/component";
+import { Buyable } from "@/typings/features/buyable";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'buyables',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		buyables: Array,
-		height: {
-			type: [ Number, String ],
-			default: "inherit"
-		}
-	},
-	emits: [ 'set-confirm-respec' ],
-	computed: {
-		filteredBuyables() {
-			return getFiltered(layers[this.layer || this.tab.layer].buyables, this.buyables);
-		},
-		showRespec() {
-			return layers[this.layer || this.tab.layer].buyables.showRespec;
-		},
-		confirmRespec() {
-			return player[this.layer || this.tab.layer].buyables.confirmRespec;
-		}
-	},
-	methods: {
-		setConfirmRespec(value) {
-			if (this.confirmRespec != undefined) {
-				this.$emit("set-confirm-respec", value);
-			} else {
-				player[this.layer || this.tab.layer].buyables.confirmRespec = value;
-			}
-		},
-		respec() {
-			layers[this.layer || this.tab.layer].buyables.respec?.();
-		}
-	}
-};
+export default defineComponent({
+    name: "buyables",
+    mixins: [InjectLayerMixin],
+    props: {
+        buyables: {
+            type: Object as PropType<Array<string>>
+        },
+        height: {
+            type: [Number, String],
+            default: "inherit"
+        }
+    },
+    computed: {
+        filteredBuyables(): Record<string, Buyable> {
+            if (layers[this.layer].buyables) {
+                return getFiltered<Buyable>(layers[this.layer].buyables!.data, this.buyables);
+            }
+            return {};
+        },
+        showRespec(): boolean | undefined {
+            return layers[this.layer].buyables!.showRespecButton;
+        },
+        confirmRespec(): boolean {
+            return player.layers[this.layer].confirmRespecBuyables;
+        },
+        respecWarningDisplay(): CoercableComponent | undefined {
+            return layers[this.layer].buyables?.respecWarningDisplay;
+        }
+    },
+    methods: {
+        setConfirmRespec(value: boolean) {
+            player.layers[this.layer].confirmRespecBuyables = value;
+        },
+        respec() {
+            layers[this.layer].buyables!.respec?.();
+        }
+    }
+});
 </script>
 
 <style scoped>
 .buyable-container {
-	margin-left: 7px;
-	margin-right: 7px;
+    margin-left: 7px;
+    margin-right: 7px;
 }
 </style>
diff --git a/src/components/features/Challenge.vue b/src/components/features/Challenge.vue
index 2d56188..ef57ed0 100644
--- a/src/components/features/Challenge.vue
+++ b/src/components/features/Challenge.vue
@@ -1,79 +1,84 @@
 <template>
-	<div v-if="challenge.shown" :style="style"
-		:class="{
-			feature: true,
-			challenge: true,
-			resetNotify: challenge.active,
-			notify: challenge.active && challenge.canComplete,
-			done: challenge.completed,
-			maxed: challenge.maxed
-		}">
-		<div v-if="title"><component :is="title" /></div>
-		<button :style="{ backgroundColor: challenge.maxed ? null : buttonColor }" @click="toggle">
-			{{ buttonText }}
-		</button>
-		<component v-if="fullDisplay" :is="fullDisplay" />
-		<default-challenge-display v-else :id="id" />
-		<mark-node :mark="challenge.mark" />
-		<branch-node :branches="challenge.branches" :id="id" featureType="challenge" />
-	</div>
+    <div
+        v-if="challenge.shown"
+        :style="style"
+        :class="{
+            feature: true,
+            [layer]: true,
+            challenge: true,
+            resetNotify: challenge.active,
+            notify: challenge.active && challenge.canComplete,
+            done: challenge.completed,
+            maxed: challenge.maxed
+        }"
+    >
+        <div v-if="title"><component :is="title" /></div>
+        <button :style="{ backgroundColor: challenge.maxed ? null : buttonColor }" @click="toggle">
+            {{ buttonText }}
+        </button>
+        <component v-if="fullDisplay" :is="fullDisplay" />
+        <default-challenge-display v-else :id="id" />
+        <mark-node :mark="challenge.mark" />
+        <branch-node :branches="challenge.branches" :id="id" featureType="challenge" />
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Challenge } from "@/typings/features/challenge";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'challenge',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		challenge() {
-			return layers[this.layer || this.tab.layer].challenges[this.id];
-		},
-		style() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.challenge,
-				this.challenge.style
-			];
-		},
-		title() {
-			if (this.challenge.titleDisplay) {
-				return coerceComponent(this.challenge.titleDisplay, 'div');
-			}
-			if (this.challenge.name) {
-				return coerceComponent(this.challenge.name, 'h3');
-			}
-			return null;
-		},
-		buttonColor() {
-			return layers[this.layer || this.tab.layer].color;
-		},
-		buttonText() {
-			if (this.challenge.active) {
-				return this.challenge.canComplete ? "Finish" : "Exit Early";
-			}
-			if (this.challenge.maxed) {
-				return "Completed";
-			}
-			return "Start";
-		},
-		fullDisplay() {
-			if (this.challenge.fullDisplay) {
-				return coerceComponent(this.challenge.fullDisplay, 'div');
-			}
-			return null;
-		}
-	},
-	methods: {
-		toggle() {
-			this.challenge.toggle();
-		}
-	}
-};
+export default defineComponent({
+    name: "challenge",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        challenge(): Challenge {
+            return layers[this.layer].challenges!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [layers[this.layer].componentStyles?.challenge, this.challenge.style];
+        },
+        title(): Component | string | null {
+            if (this.challenge.titleDisplay) {
+                return coerceComponent(this.challenge.titleDisplay, "div");
+            }
+            if (this.challenge.name) {
+                return coerceComponent(this.challenge.name, "h3");
+            }
+            return null;
+        },
+        buttonColor(): string {
+            return layers[this.layer].color;
+        },
+        buttonText(): string {
+            if (this.challenge.active) {
+                return this.challenge.canComplete ? "Finish" : "Exit Early";
+            }
+            if (this.challenge.maxed) {
+                return "Completed";
+            }
+            return "Start";
+        },
+        fullDisplay(): Component | string | null {
+            if (this.challenge.fullDisplay) {
+                return coerceComponent(this.challenge.fullDisplay, "div");
+            }
+            return null;
+        }
+    },
+    methods: {
+        toggle() {
+            this.challenge.toggle();
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -102,6 +107,6 @@ export default {
 }
 
 .challenge.maxed button {
-    cursor:  unset;
+    cursor: unset;
 }
 </style>
diff --git a/src/components/features/Challenges.vue b/src/components/features/Challenges.vue
index bded8a6..7f78f71 100644
--- a/src/components/features/Challenges.vue
+++ b/src/components/features/Challenges.vue
@@ -1,36 +1,41 @@
 <template>
-	<div v-if="filteredChallenges" class="table">
-		<template v-if="filteredChallenges.rows && filteredChallenges.cols">
-			<div v-for="row in filteredChallenges.rows" class="row" :key="row">
-				<div v-for="col in filteredChallenges.cols" :key="col">
-					<challenge v-if="filteredChallenges[row * 10 + col] !== undefined" :id="row * 10 + col" />
-				</div>
-			</div>
-		</template>
-		<row v-else>
-			<challenge v-for="(challenge, id) in filteredChallenges" :key="id" :id="id" />
-		</row>
-	</div>
+    <div v-if="filteredChallenges" class="table">
+        <template v-if="filteredChallenges.rows && filteredChallenges.cols">
+            <div v-for="row in filteredChallenges.rows" class="row" :key="row">
+                <div v-for="col in filteredChallenges.cols" :key="col">
+                    <challenge
+                        v-if="filteredChallenges[row * 10 + col] !== undefined"
+                        :id="row * 10 + col"
+                    />
+                </div>
+            </div>
+        </template>
+        <row v-else>
+            <challenge v-for="(challenge, id) in filteredChallenges" :key="id" :id="id" />
+        </row>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Challenge } from "@/typings/features/challenge";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'challenges',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		challenges: Array
-	},
-	computed: {
-		filteredChallenges() {
-			return getFiltered(layers[this.layer || this.tab.layer].challenges, this.challenges);
-		}
-	}
-};
+export default defineComponent({
+    name: "challenges",
+    mixins: [InjectLayerMixin],
+    props: {
+        challenges: {
+            type: Object as PropType<Array<string>>
+        }
+    },
+    computed: {
+        filteredChallenges(): Record<string, Challenge> {
+            return getFiltered(layers[this.layer].challenges!.data, this.challenges);
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/Clickable.vue b/src/components/features/Clickable.vue
index 55f594e..74ab28a 100644
--- a/src/components/features/Clickable.vue
+++ b/src/components/features/Clickable.vue
@@ -1,73 +1,95 @@
 <template>
-	<div v-if="clickable.unlocked">
-		<button :style="style" @click="clickable.click" @mousedown="start" @mouseleave="stop" @mouseup="stop" @touchstart="start"
-			@touchend="stop" @touchcancel="stop" :disabled="!clickable.canClick"
-			:class="{ feature: true, [layer || tab.layer]: true, clickable: true, can: clickable.canClick, locked: !clickable.canClick }">
-			<div v-if="title">
-				<component :is="title" />
-			</div>
-			<component :is="display" style="white-space: pre-line;" />
-			<mark-node :mark="clickable.mark" />
-			<branch-node :branches="clickable.branches" :id="id" featureType="clickable" />
-		</button>
-	</div>
+    <div v-if="clickable.unlocked">
+        <button
+            :style="style"
+            @click="clickable.click"
+            @mousedown="start"
+            @mouseleave="stop"
+            @mouseup="stop"
+            @touchstart="start"
+            @touchend="stop"
+            @touchcancel="stop"
+            :disabled="!clickable.canClick"
+            :class="{
+                feature: true,
+                [layer]: true,
+                clickable: true,
+                can: clickable.canClick,
+                locked: !clickable.canClick
+            }"
+        >
+            <div v-if="title">
+                <component :is="title" />
+            </div>
+            <component :is="display" style="white-space: pre-line;" />
+            <mark-node :mark="clickable.mark" />
+            <branch-node :branches="clickable.branches" :id="id" featureType="clickable" />
+        </button>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Clickable } from "@/typings/features/clickable";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'clickable',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ],
-		size: {
-			type: [ Number, String ]
-		}
-	},
-	data() {
-		return {
-			interval: false,
-			time: 0
-		};
-	},
-	computed: {
-		clickable() {
-			return layers[this.layer || this.tab.layer].clickables[this.id];
-		},
-		style() {
-			return [
-				this.clickable.canClick ? { 'background-color': layers[this.layer || this.tab.layer].color } : {},
-				this.size ? {'height': this.size, 'width': this.size} : {},
-				layers[this.layer || this.tab.layer].componentStyles?.clickable,
-				this.clickable.style
-			];
-		},
-		title() {
-			if (this.clickable.title) {
-				return coerceComponent(this.clickable.title, 'h2');
-			}
-			return null;
-		},
-		display() {
-			return coerceComponent(this.clickable.display, 'div');
-		}
-	},
-	methods: {
-		start() {
-			if (!this.interval) {
-				this.interval = setInterval(this.clickable.click, 250);
-			}
-		},
-		stop() {
-			clearInterval(this.interval);
-			this.interval = false;
-			this.time = 0;
-		}
-	}
-};
+export default defineComponent({
+    name: "clickable",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        },
+        size: String
+    },
+    data() {
+        return {
+            interval: null,
+            time: 0
+        } as {
+            interval: number | null;
+            time: number;
+        };
+    },
+    computed: {
+        clickable(): Clickable {
+            return layers[this.layer].clickables!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.clickable.canClick ? { backgroundColor: layers[this.layer].color } : undefined,
+                this.size ? { height: this.size, width: this.size } : undefined,
+                layers[this.layer].componentStyles?.clickable,
+                this.clickable.style
+            ];
+        },
+        title(): Component | string | null {
+            if (this.clickable.title) {
+                return coerceComponent(this.clickable.title, "h2");
+            }
+            return null;
+        },
+        display(): Component | string {
+            return coerceComponent(this.clickable.display, "div");
+        }
+    },
+    methods: {
+        start() {
+            if (!this.interval && this.clickable.click) {
+                this.interval = setInterval(this.clickable.click, 250);
+            }
+        },
+        stop() {
+            if (this.interval) {
+                clearInterval(this.interval);
+                this.interval = null;
+                this.time = 0;
+            }
+        }
+    }
+});
 </script>
 
 <style scoped>
diff --git a/src/components/features/Clickables.vue b/src/components/features/Clickables.vue
index de3d616..1bab8ca 100644
--- a/src/components/features/Clickables.vue
+++ b/src/components/features/Clickables.vue
@@ -1,65 +1,79 @@
 <template>
-	<div v-if="filteredClickables" class="table">
-		<master-button v-if="showMasterButton" style="margin-bottom: 12px;" @press="press" />
-		<template v-if="filteredClickables.rows && filteredClickables.cols">
-			<div v-for="row in filteredClickables.rows" class="row" :key="row">
-				<div v-for="col in filteredClickables.cols" :key="col">
-					<clickable v-if="filteredClickables[row * 10 + col] !== undefined" class="align clickable-container"
-						:style="{ height }" :id="row * 10 + col" :size="height === 'inherit' ? null : height" />
-				</div>
-			</div>
-		</template>
-		<row v-else>
-			<clickable v-for="(clickable, id) in filteredClickables" :key="id" class="align clickable-container" :style="{ height }"
-						:id="id" :size="height === 'inherit' ? null : height" />
-		</row>
-	</div>
+    <div v-if="filteredClickables" class="table">
+        <master-button v-if="showMaster" style="margin-bottom: 12px;" @press="press" />
+        <template v-if="filteredClickables.rows && filteredClickables.cols">
+            <div v-for="row in filteredClickables.rows" class="row" :key="row">
+                <div v-for="col in filteredClickables.cols" :key="col">
+                    <clickable
+                        v-if="filteredClickables[row * 10 + col] !== undefined"
+                        class="align clickable-container"
+                        :style="{ height }"
+                        :id="row * 10 + col"
+                        :size="height === 'inherit' ? null : height"
+                    />
+                </div>
+            </div>
+        </template>
+        <row v-else>
+            <clickable
+                v-for="(clickable, id) in filteredClickables"
+                :key="id"
+                class="align clickable-container"
+                :style="{ height }"
+                :id="id"
+                :size="height === 'inherit' ? null : height"
+            />
+        </row>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { Clickable } from "@/typings/features/clickable";
+import { defineComponent, PropType } from "vue";
+import { layers } from "@/game/layers";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
 
-export default {
-	name: 'clickables',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		clickables: Array,
-		showMaster: {
-			type: Boolean,
-			default: null
-		},
-		height: {
-			type: [ Number, String ],
-			default: "inherit"
-		}
-	},
-	computed: {
-		filteredClickables() {
-			return getFiltered(layers[this.layer || this.tab.layer].clickables, this.clickables);
-		},
-		showMasterButton() {
-			if (layers[this.layer || this.tab.layer].clickables?.masterButtonClick == undefined) {
-				return false;
-			}
-			if (this.showMaster != undefined) {
-				return this.showMaster;
-			}
-			return layers[this.layer || this.tab.layer].clickables?.showMaster;
-		}
-	},
-	methods: {
-		press() {
-			layers[this.layer || this.tab.layer].clickables.masterButtonClick();
-		}
-	}
-};
+export default defineComponent({
+    name: "clickables",
+    mixins: [InjectLayerMixin],
+    props: {
+        achievements: {
+            type: Object as PropType<Array<string>>
+        },
+        showMasterButton: {
+            type: Boolean,
+            default: null
+        },
+        height: {
+            type: [Number, String],
+            default: "inherit"
+        }
+    },
+    computed: {
+        filteredClickables(): Record<string, Clickable> {
+            return getFiltered(layers[this.layer].clickables!.data, this.clickables);
+        },
+        showMaster(): boolean | undefined {
+            if (layers[this.layer].clickables?.masterButtonClick == undefined) {
+                return false;
+            }
+            if (this.showMasterButton != undefined) {
+                return this.showMasterButton;
+            }
+            return layers[this.layer].clickables?.showMasterButton;
+        }
+    },
+    methods: {
+        press() {
+            layers[this.layer].clickables?.masterButtonClick?.();
+        }
+    }
+});
 </script>
 
 <style scoped>
 .clickable-container {
-	margin-left: 7px;
-	margin-right: 7px;
+    margin-left: 7px;
+    margin-right: 7px;
 }
 </style>
diff --git a/src/components/features/DefaultChallengeDisplay.vue b/src/components/features/DefaultChallengeDisplay.vue
index 67c6b19..49b8cf5 100644
--- a/src/components/features/DefaultChallengeDisplay.vue
+++ b/src/components/features/DefaultChallengeDisplay.vue
@@ -1,46 +1,52 @@
 <template>
-	<component :is="challengeDescription" v-bind="$attrs" />
-	<div>Goal: <component :is="goalDescription" /></div>
-	<div>Reward: <component :is="rewardDescription" /></div>
-	<component v-if="rewardDisplay" :is="rewardDisplay" />
+    <component :is="challengeDescription" v-bind="$attrs" />
+    <div>Goal: <component :is="goalDescription" /></div>
+    <div>Reward: <component :is="rewardDescription" /></div>
+    <component v-if="rewardDisplay" :is="rewardDisplay" />
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Challenge } from "@/typings/features/challenge";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'default-challenge-display',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		challenge() {
-			return layers[this.layer || this.tab.layer].challenges[this.id];
-		},
-		challengeDescription() {
-			return coerceComponent(this.challenge.challengeDescription, 'div');
-		},
-		goalDescription() {
-			if (this.challenge.goalDescription) {
-				return coerceComponent(this.challenge.goalDescription);
-			}
-			return coerceComponent(`{{ format(${this.challenge.goal}) }} ${this.challenge.currencyDisplayName || 'points'}`);
-		},
-		rewardDescription() {
-			return coerceComponent(this.challenge.rewardDescription);
-		},
-		rewardDisplay() {
-			if (this.challenge.rewardDisplay) {
-				return coerceComponent(`Currently: ${this.challenge.rewardDisplay}`);
-			}
-			return null;
-		},
-	}
-};
+export default defineComponent({
+    name: "default-challenge-display",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        challenge(): Challenge {
+            return layers[this.layer].challenges!.data[this.id];
+        },
+        challengeDescription(): Component | string {
+            return coerceComponent(this.challenge.challengeDescription, "div");
+        },
+        goalDescription(): Component | string {
+            if (this.challenge.goalDescription) {
+                return coerceComponent(this.challenge.goalDescription);
+            }
+            return coerceComponent(
+                `{{ format(${this.challenge.goal}) }} ${this.challenge.currencyDisplayName ||
+                    "points"}`
+            );
+        },
+        rewardDescription(): Component | string {
+            return coerceComponent(this.challenge.rewardDescription);
+        },
+        rewardDisplay(): Component | string | null {
+            if (this.challenge.rewardDisplay) {
+                return coerceComponent(`Currently: ${this.challenge.rewardDisplay}`);
+            }
+            return null;
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/DefaultPrestigeButtonDisplay.vue b/src/components/features/DefaultPrestigeButtonDisplay.vue
index f900784..ba48e24 100644
--- a/src/components/features/DefaultPrestigeButtonDisplay.vue
+++ b/src/components/features/DefaultPrestigeButtonDisplay.vue
@@ -1,77 +1,79 @@
 <template>
-	<span>
-		{{ resetDescription }}<b>{{ resetGain }}</b>
-		{{ resource }}
-		<br v-if="nextAt"/><br v-if="nextAt"/>
-		{{ nextAt }}
-	</span>
+    <span>
+        {{ resetDescription }}<b>{{ resetGain }}</b>
+        {{ resource }}
+        <br v-if="nextAt" /><br v-if="nextAt" />
+        {{ nextAt }}
+    </span>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { format, formatWhole } from '../../util/bignum';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import Decimal, { format, formatWhole } from "@/util/bignum";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'default-prestige-button-display',
-	inject: [ 'tab' ],
-	props: {
-		layer: String
-	},
-	computed: {
-		resetDescription() {
-			if (player[this.layer || this.tab.layer].points.lt(1e3) || layers[this.layer || this.tab.layer].type === "static") {
-				return layers[this.layer || this.tab.layer].resetDescription || "Reset for ";
-			}
-			return "";
-		},
-		resetGain() {
-			return formatWhole(layers[this.layer || this.tab.layer].resetGain);
-		},
-		resource() {
-			return layers[this.layer || this.tab.layer].resource;
-		},
-		showNextAt() {
-			if (layers[this.layer || this.tab.layer].showNextAt != undefined) {
-				return layers[this.layer || this.tab.layer].showNextAt;
-			} else {
-				return layers[this.layer || this.tab.layer].type === "static" ?
-					player[this.layer || this.tab.layer].points.lt(30) : 											// static
-					player[this.layer || this.tab.layer].points.lt(1e3) && layers[this.layer ||
-						this.tab.layer].resetGain.lt(100);	// normal
-			}
-		},
-		nextAt() {
-			if (this.showNextAt) {
-				let prefix;
-				if (layers[this.layer || this.tab.layer].type === "static") {
-					if (layers[this.layer || this.tab.layer].baseAmount.gte(layers[this.layer || this.tab.layer].nextAt) &&
-						layers[this.layer || this.tab.layer].canBuyMax !== false) {
-						prefix = "Next:";
-					} else {
-						prefix = "Req:";
-					}
+export default defineComponent({
+    name: "default-prestige-button-display",
+    mixins: [InjectLayerMixin],
+    computed: {
+        resetDescription(): string {
+            if (player.layers[this.layer].points.lt(1e3) || layers[this.layer].type === "static") {
+                return layers[this.layer].resetDescription || "Reset for ";
+            }
+            return "";
+        },
+        resetGain(): string {
+            return formatWhole(layers[this.layer].resetGain);
+        },
+        resource(): string {
+            return layers[this.layer].resource;
+        },
+        showNextAt(): boolean {
+            if (layers[this.layer].showNextAt != undefined) {
+                return layers[this.layer].showNextAt!;
+            } else {
+                return layers[this.layer].type === "static"
+                    ? player.layers[this.layer].points.lt(30) // static
+                    : player.layers[this.layer].points.lt(1e3) &&
+                          layers[this.layer].resetGain.lt(100); // normal
+            }
+        },
+        nextAt(): string {
+            if (this.showNextAt) {
+                let prefix;
+                if (layers[this.layer].type === "static") {
+                    if (
+                        Decimal.gte(layers[this.layer].baseAmount!, layers[this.layer].nextAt) &&
+                        layers[this.layer].canBuyMax !== false
+                    ) {
+                        prefix = "Next:";
+                    } else {
+                        prefix = "Req:";
+                    }
 
-					const baseAmount = formatWhole(layers[this.layer || this.tab.layer].baseAmount);
-					const nextAt = (layers[this.layer || this.tab.layer].roundUpCost ? formatWhole : format)(layers[this.layer || this.tab.layer].nextAtMax);
-					const baseResource = layers[this.layer || this.tab.layer].baseResource;
+                    const baseAmount = formatWhole(layers[this.layer].baseAmount!);
+                    const nextAt = (layers[this.layer].roundUpCost ? formatWhole : format)(
+                        layers[this.layer].nextAtMax
+                    );
+                    const baseResource = layers[this.layer].baseResource;
 
-					return `${prefix} ${baseAmount} / ${nextAt} ${baseResource}`;
-				} else {
-					let amount;
-					if (layers[this.layer || this.tab.layer].roundUpCost) {
-						amount = formatWhole(layers[this.layer || this.tab.layer].nextAt);
-					} else {
-						amount = format(layers[this.layer || this.tab.layer].nextAt);
-					}
-					return `Next at ${amount} ${layers[this.layer || this.tab.layer].baseResource}`;
-				}
-			}
-			return "";
-		}
-	}
-};
+                    return `${prefix} ${baseAmount} / ${nextAt} ${baseResource}`;
+                } else {
+                    let amount;
+                    if (layers[this.layer].roundUpCost) {
+                        amount = formatWhole(layers[this.layer].nextAt);
+                    } else {
+                        amount = format(layers[this.layer].nextAt);
+                    }
+                    return `Next at ${amount} ${layers[this.layer].baseResource}`;
+                }
+            }
+            return "";
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/DefaultUpgradeDisplay.vue b/src/components/features/DefaultUpgradeDisplay.vue
index f9ae1c4..f788552 100644
--- a/src/components/features/DefaultUpgradeDisplay.vue
+++ b/src/components/features/DefaultUpgradeDisplay.vue
@@ -1,53 +1,56 @@
 <template>
-	<span>
-		<div v-if="title"><component :is="title" /></div>
-		<component :is="description" />
-		<div v-if="effectDisplay"><br>Currently: <component :is="effectDisplay" /></div>
-		<br>
-		Cost: {{ cost }} {{ costResource }}
-	</span>
+    <span>
+        <div v-if="title"><component :is="title" /></div>
+        <component :is="description" />
+        <div v-if="effectDisplay"><br />Currently: <component :is="effectDisplay" /></div>
+        <br />
+        Cost: {{ cost }} {{ costResource }}
+    </span>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
-import { formatWhole } from '../../util/bignum';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Upgrade } from "@/typings/features/upgrade";
+import { formatWhole } from "@/util/bignum";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'default-upgrade-display',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		upgrade() {
-			return layers[this.layer || this.tab.layer].upgrades[this.id];
-		},
-		title() {
-			if (this.upgrade.title) {
-				return coerceComponent(this.upgrade.title, 'h3');
-			}
-			return null;
-		},
-		description() {
-			return coerceComponent(this.upgrade.description, 'div');
-		},
-		effectDisplay() {
-			if (this.upgrade.effectDisplay) {
-				return coerceComponent(this.upgrade.effectDisplay);
-			}
-			return null;
-		},
-		cost() {
-			return formatWhole(this.upgrade.cost);
-		},
-		costResource() {
-			return this.upgrade.currencyDisplayName || layers[this.layer || this.tab.layer].resource;
-		}
-	}
-};
+export default defineComponent({
+    name: "default-upgrade-display",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        upgrade(): Upgrade {
+            return layers[this.layer].upgrades!.data[this.id];
+        },
+        title(): Component | string | null {
+            if (this.upgrade.title) {
+                return coerceComponent(this.upgrade.title, "h3");
+            }
+            return null;
+        },
+        description(): Component | string {
+            return coerceComponent(this.upgrade.description, "div");
+        },
+        effectDisplay(): Component | string | null {
+            if (this.upgrade.effectDisplay) {
+                return coerceComponent(this.upgrade.effectDisplay);
+            }
+            return null;
+        },
+        cost(): string {
+            return formatWhole(this.upgrade.cost);
+        },
+        costResource(): string {
+            return this.upgrade.currencyDisplayName || layers[this.layer].resource;
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/Grid.vue b/src/components/features/Grid.vue
index 9b0d16d..9d011c9 100644
--- a/src/components/features/Grid.vue
+++ b/src/components/features/Grid.vue
@@ -1,30 +1,34 @@
 <template>
-	<div v-if="grid" class="table">
-		<div v-for="row in grid.rows" class="row" :key="row">
-			<div v-for="col in grid.cols" :key="col">
-				<gridable class="align" :id="id" :cell="row * 100 + col" />
-			</div>
-		</div>
-	</div>
+    <div v-if="grid" class="table">
+        <div v-for="row in grid.rows" class="row" :key="row">
+            <div v-for="col in grid.cols" :key="col">
+                <grid-cell class="align" :id="id" :cell="row * 100 + col" />
+            </div>
+        </div>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Grid } from "@/typings/features/grid";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'grid',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		grid() {
-			return layers[this.layer || this.tab.layer].grids[this.id];
-		}
-	}
-};
+export default defineComponent({
+    name: "grid",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        grid(): Grid {
+            return layers[this.layer].grids!.data[this.id];
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/GridCell.vue b/src/components/features/GridCell.vue
new file mode 100644
index 0000000..2b2e312
--- /dev/null
+++ b/src/components/features/GridCell.vue
@@ -0,0 +1,97 @@
+<template>
+    <button
+        v-if="gridable.unlocked"
+        :class="{ feature: true, tile: true, can: canClick, locked: !canClick }"
+        :style="style"
+        @click="gridable.click"
+        @mousedown="start"
+        @mouseleave="stop"
+        @mouseup="stop"
+        @touchstart="start"
+        @touchend="stop"
+        @touchcancel="stop"
+        :disabled="!canClick"
+    >
+        <div v-if="title"><component :is="title" /></div>
+        <component :is="display" style="white-space: pre-line;" />
+        <branch-node :branches="gridable.branches" :id="id" featureType="gridable" />
+    </button>
+</template>
+
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { GridCell } from "@/typings/features/grid";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
+
+export default defineComponent({
+    name: "grid-cell",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        },
+        cell: {
+            type: [Number, String],
+            required: true
+        },
+        size: [Number, String]
+    },
+    data() {
+        return {
+            interval: null,
+            time: 0
+        } as {
+            interval: number | null;
+            time: number;
+        };
+    },
+    computed: {
+        gridCell(): GridCell {
+            return layers[this.layer].grids!.data[this.id][this.cell] as GridCell;
+        },
+        canClick(): boolean {
+            return this.gridCell.canClick;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.canClick ? { backgroundColor: layers[this.layer].color } : {},
+                layers[this.layer].componentStyles?.["grid-cell"],
+                this.gridCell.style
+            ];
+        },
+        title(): Component | string | null {
+            if (this.gridCell.title) {
+                return coerceComponent(this.gridCell.title, "h3");
+            }
+            return null;
+        },
+        display(): Component | string {
+            return coerceComponent(this.gridCell.display, "div");
+        }
+    },
+    methods: {
+        start() {
+            if (!this.interval && this.gridCell.click) {
+                this.interval = setInterval(this.gridCell.click, 250);
+            }
+        },
+        stop() {
+            if (this.interval) {
+                clearInterval(this.interval);
+                this.interval = null;
+                this.time = 0;
+            }
+        }
+    }
+});
+</script>
+
+<style scoped>
+.tile {
+    min-height: 80px;
+    width: 80px;
+    font-size: 10px;
+}
+</style>
diff --git a/src/components/features/Gridable.vue b/src/components/features/Gridable.vue
deleted file mode 100644
index 57e92e7..0000000
--- a/src/components/features/Gridable.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<template>
-	<button v-if="gridable.unlocked" :class="{ feature: true, tile: true, can: canClick, locked: !canClick}"
-		:style="style" @click="gridable.click" @mousedown="start" @mouseleave="stop" @mouseup="stop" @touchstart="start"
-		@touchend="stop" @touchcancel="stop" :disabled="!canClick">
-		<div v-if="title"><component :is="title" /></div>
-		<component :is="display" style="white-space: pre-line;" />
-		<branch-node :branches="gridable.branches" :id="id" featureType="gridable" />
-	</button>
-</template>
-
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
-
-export default {
-	name: 'gridable',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ],
-		cell: [ Number, String ],
-		size: {
-			type: [ Number, String ]
-		}
-	},
-	data() {
-		return {
-			interval: false,
-			time: 0
-		};
-	},
-	computed: {
-		gridable() {
-			return layers[this.layer || this.tab.layer].grids[this.id][this.cell];
-		},
-		canClick() {
-			return this.gridable.canClick;
-		},
-		style() {
-			return [
-				this.canClick ? { 'background-color': layers[this.layer || this.tab.layer].color } : {},
-				layers[this.layer || this.tab.layer].componentStyles?.gridable,
-				this.gridable.style
-			];
-		},
-		title() {
-			if (this.gridable.title) {
-				return coerceComponent(this.gridable.title, 'h3');
-			}
-			return null;
-		},
-		display() {
-			return coerceComponent(this.gridable.display, 'div');
-		}
-	},
-	methods: {
-		start() {
-			if (!this.interval) {
-				this.interval = setInterval(this.gridable.click, 250);
-			}
-		},
-		stop() {
-			clearInterval(this.interval);
-			this.interval = false;
-			this.time = 0;
-		}
-	}
-};
-</script>
-
-<style scoped>
-.tile {
-	min-height: 80px;
-	width: 80px;
-	font-size: 10px;
-}
-</style>
diff --git a/src/components/features/Infobox.vue b/src/components/features/Infobox.vue
index 658826a..4e06790 100644
--- a/src/components/features/Infobox.vue
+++ b/src/components/features/Infobox.vue
@@ -1,93 +1,96 @@
 <template>
-	<div class="infobox" v-if="infobox.unlocked" :style="style" :class="{ collapsed, stacked }">
-		<button class="title" :style="titleStyle" @click="toggle">
-			<span class="toggle">▼</span>
-			<component :is="title" />
-		</button>
-		<collapse-transition>
-			<div v-if="!collapsed" class="body" :style="{ backgroundColor: borderColor }">
-				<component :is="body" :style="bodyStyle" />
-			</div>
-		</collapse-transition>
-		<branch-node :branches="infobox.branches" :id="id" featureType="infobox" />
-	</div>
+    <div class="infobox" v-if="infobox.unlocked" :style="style" :class="{ collapsed, stacked }">
+        <button class="title" :style="titleStyle" @click="toggle">
+            <span class="toggle">▼</span>
+            <component :is="title" />
+        </button>
+        <collapse-transition>
+            <div v-if="!collapsed" class="body" :style="{ backgroundColor: borderColor }">
+                <component :is="body" :style="bodyStyle" />
+            </div>
+        </collapse-transition>
+        <branch-node :branches="infobox.branches" :id="id" featureType="infobox" />
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
-import themes from '../../data/themes';
+<script lang="ts">
+import themes from "@/data/themes";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Infobox } from "@/typings/features/infobox";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'infobox',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		infobox() {
-			return layers[this.layer || this.tab.layer].infoboxes[this.id];
-		},
-		borderColor() {
-			return this.infobox.borderColor || layers[this.layer || this.tab.layer].color;
-		},
-		style() {
-			return [
-				{ borderColor: this.borderColor },
-				layers[this.layer || this.tab.layer].componentStyles?.infobox,
-				this.infobox.style
-			];
-		},
-		titleStyle() {
-			return [
-				{ backgroundColor: layers[this.layer || this.tab.layer].color },
-				layers[this.layer || this.tab.layer].componentStyles?.['infobox-title'],
-				this.infobox.titleStyle
-			];
-		},
-		bodyStyle() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.['infobox-body'],
-				this.infobox.bodyStyle
-			];
-		},
-		title() {
-			if (this.infobox.title) {
-				return coerceComponent(this.infobox.title);
-			}
-			return coerceComponent(layers[this.layer || this.tab.layer].name);
-		},
-		body() {
-			return coerceComponent(this.infobox.body);
-		},
-		collapsed() {
-			return player[this.layer || this.tab.layer].infoboxes[this.id];
-		},
-		stacked() {
-			return themes[player.theme].stackedInfoboxes;
-		}
-	},
-	methods: {
-		toggle() {
-			player[this.layer || this.tab.layer].infoboxes[this.id] = !player[this.layer || this.tab.layer].infoboxes[this.id];
-		}
-	}
-};
+export default defineComponent({
+    name: "infobox",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        infobox(): Infobox {
+            return layers[this.layer].infoboxes!.data[this.id];
+        },
+        borderColor(): string {
+            return this.infobox.borderColor || layers[this.layer].color;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                { borderColor: this.borderColor },
+                layers[this.layer].componentStyles?.infobox,
+                this.infobox.style
+            ];
+        },
+        titleStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                { backgroundColor: layers[this.layer].color },
+                layers[this.layer].componentStyles?.["infobox-title"],
+                this.infobox.titleStyle
+            ];
+        },
+        bodyStyle(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [layers[this.layer].componentStyles?.["infobox-body"], this.infobox.bodyStyle];
+        },
+        title(): Component | string {
+            if (this.infobox.title) {
+                return coerceComponent(this.infobox.title);
+            }
+            return coerceComponent(layers[this.layer].name || this.layer);
+        },
+        body(): Component | string {
+            return coerceComponent(this.infobox.body);
+        },
+        collapsed(): boolean {
+            return player.layers[this.layer].infoboxes[this.id];
+        },
+        stacked(): boolean {
+            return themes[player.theme].stackedInfoboxes;
+        }
+    },
+    methods: {
+        toggle() {
+            player.layers[this.layer].infoboxes[this.id] = !player.layers[this.layer].infoboxes[
+                this.id
+            ];
+        }
+    }
+});
 </script>
 
 <style scoped>
 .infobox {
-	position: relative;
-	width: 600px;
-	max-width: 95%;
-	margin-top: 0;
-	text-align: left;
+    position: relative;
+    width: 600px;
+    max-width: 95%;
+    margin-top: 0;
+    text-align: left;
     border-style: solid;
     border-width: 0px;
-	box-sizing: border-box;
-	border-radius: 5px;
+    box-sizing: border-box;
+    border-radius: 5px;
 }
 
 .infobox.stacked {
@@ -95,27 +98,27 @@ export default {
 }
 
 .infobox:not(.stacked) + .infobox:not(.stacked) {
-	margin-top: 20px;
+    margin-top: 20px;
 }
 
 .infobox + :not(.infobox) {
-	margin-top: 10px;
+    margin-top: 10px;
 }
 
 .title {
-	font-size: 24px;
-	color: black;
-	cursor: pointer;
-	border: none;
-	padding: 4px;
-	width: auto;
-	text-align: left;
-	padding-left: 30px;
+    font-size: 24px;
+    color: black;
+    cursor: pointer;
+    border: none;
+    padding: 4px;
+    width: auto;
+    text-align: left;
+    padding-left: 30px;
 }
 
 .infobox:not(.stacked) .title {
-	border-top-left-radius: 5px;
-	border-top-right-radius: 5px;
+    border-top-left-radius: 5px;
+    border-top-right-radius: 5px;
 }
 
 .infobox.stacked + .infobox.stacked {
@@ -125,32 +128,32 @@ export default {
 }
 
 .stacked .title {
-	width: 100%;
+    width: 100%;
 }
 
 .collapsed:not(.stacked) .title::after {
-	content: "";
-	position: absolute;
-	left: 0;
-	right: 0;
-	bottom: 0;
+    content: "";
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
     height: 4px;
-	background-color: inherit;
+    background-color: inherit;
 }
 
 .toggle {
-	position: absolute;
-	left: 10px;
+    position: absolute;
+    left: 10px;
 }
 
 .collapsed .toggle {
-	transform: rotate(-90deg);
+    transform: rotate(-90deg);
 }
 
 .body {
-	transition-duration: .5s;
-	border-radius: 5px;
-	border-top-left-radius: 0;
+    transition-duration: 0.5s;
+    border-radius: 5px;
+    border-top-left-radius: 0;
 }
 
 .infobox:not(.stacked) .body {
@@ -158,12 +161,12 @@ export default {
 }
 
 .body > * {
-	padding: 8px;
-	width: 100%;
+    padding: 8px;
+    width: 100%;
     display: block;
     box-sizing: border-box;
-	border-radius: 5px;
-	border-top-left-radius: 0;
-	background-color: var(--background);
+    border-radius: 5px;
+    border-top-left-radius: 0;
+    background-color: var(--background);
 }
 </style>
diff --git a/src/components/features/MainDisplay.vue b/src/components/features/MainDisplay.vue
index 622fae7..3f031ea 100644
--- a/src/components/features/MainDisplay.vue
+++ b/src/components/features/MainDisplay.vue
@@ -1,50 +1,53 @@
 <template>
-	<div>
-		<span v-if="showPrefix">You have </span>
-		<resource :amount="amount" :color="color" />
-		{{ resource }}<!-- remove whitespace -->
-		<span v-if="effectDisplay">, <component :is="effectDisplay" /></span>
-		<br><br>
-	</div>
+    <div>
+        <span v-if="showPrefix">You have </span>
+        <resource :amount="amount" :color="color" />
+        {{ resource
+        }}<!-- remove whitespace -->
+        <span v-if="effectDisplay">, <component :is="effectDisplay"/></span>
+        <br /><br />
+    </div>
 </template>
 
-<script>
-import player from '../../game/player';
-import { layers } from '../../game/layers';
-import { format, formatWhole } from '../../util/bignum';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { format, formatWhole } from "@/util/bignum";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'main-display',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		precision: Number
-	},
-	computed: {
-		style() {
-			return layers[this.layer || this.tab.layer].componentStyles?.['main-display'];
-		},
-		resource() {
-			return layers[this.layer || this.tab.layer].resource;
-		},
-		effectDisplay() {
-			return coerceComponent(layers[this.layer || this.tab.layer].effectDisplay);
-		},
-		showPrefix() {
-			return player[this.layer || this.tab.layer].points.lt('1e1000');
-		},
-		color() {
-			return layers[this.layer || this.tab.layer].color;
-		},
-		amount() {
-			return this.precision == undefined ?
-				formatWhole(player[this.layer || this.tab.layer].points) :
-				format(player[this.layer || this.tab.layer].points, this.precision);
-		}
-	}
-};
+export default defineComponent({
+    name: "main-display",
+    mixins: [InjectLayerMixin],
+    props: {
+        precision: Number
+    },
+    computed: {
+        style(): Partial<CSSStyleDeclaration> | undefined {
+            return layers[this.layer].componentStyles?.["main-display"];
+        },
+        resource(): string {
+            return layers[this.layer].resource;
+        },
+        effectDisplay(): Component | string | undefined {
+            return (
+                layers[this.layer].effectDisplay &&
+                coerceComponent(layers[this.layer].effectDisplay!)
+            );
+        },
+        showPrefix(): boolean {
+            return player.layers[this.layer].points.lt("1e1000");
+        },
+        color(): string {
+            return layers[this.layer].color;
+        },
+        amount(): string {
+            return this.precision == undefined
+                ? formatWhole(player.layers[this.layer].points)
+                : format(player.layers[this.layer].points, this.precision);
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/MarkNode.vue b/src/components/features/MarkNode.vue
index 4ad93bb..6e852c5 100644
--- a/src/components/features/MarkNode.vue
+++ b/src/components/features/MarkNode.vue
@@ -1,28 +1,30 @@
 <template>
-	<div v-if="mark">
-		<div v-if="mark === true" class="mark star"></div>
-		<img v-else class="mark" :src="mark" />
-	</div>
+    <div v-if="mark">
+        <div v-if="mark === true" class="mark star"></div>
+        <img v-else class="mark" :src="mark" />
+    </div>
 </template>
 
-<script>
-export default {
-	name: 'mark-node',
-	props: {
-		mark: [ Boolean, String ]
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "mark-node",
+    props: {
+        mark: [Boolean, String]
+    }
+});
 </script>
 
 <style scoped>
 .mark {
-	position: absolute;
-	left: -25px;
-	top: -10px;
-	width: 30px;
-	height: 30px;
-	z-index: 1;
-	pointer-events: none;
+    position: absolute;
+    left: -25px;
+    top: -10px;
+    width: 30px;
+    height: 30px;
+    z-index: 1;
+    pointer-events: none;
     margin-left: 0.9em;
     margin-right: 0.9em;
     margin-bottom: 1.2em;
@@ -33,34 +35,34 @@ export default {
 }
 
 .star {
-	left: -10px;
-	width: 0;
-	height: 0;
-	margin-left: 0.9em;
-	margin-right: 0.9em;
-	margin-bottom: 1.2em;
-	border-right: 0.3em solid transparent;
-	border-bottom: 0.7em solid #ffcc00;
-	border-left: 0.3em solid transparent;
-	font-size: 10px;
-	pointer-events: none;
+    left: -10px;
+    width: 0;
+    height: 0;
+    margin-left: 0.9em;
+    margin-right: 0.9em;
+    margin-bottom: 1.2em;
+    border-right: 0.3em solid transparent;
+    border-bottom: 0.7em solid #ffcc00;
+    border-left: 0.3em solid transparent;
+    font-size: 10px;
+    pointer-events: none;
 }
 
 .star::before,
 .star::after {
-	content: "";
-	width: 0;
-	height: 0;
-	position: absolute;
-	top: .6em;
-	left: -1em;
-	border-right: 1em solid transparent;
-	border-bottom: 0.7em solid #ffcc00;
-	border-left: 1em solid transparent;
-	transform: rotate(-35deg);
+    content: "";
+    width: 0;
+    height: 0;
+    position: absolute;
+    top: 0.6em;
+    left: -1em;
+    border-right: 1em solid transparent;
+    border-bottom: 0.7em solid #ffcc00;
+    border-left: 1em solid transparent;
+    transform: rotate(-35deg);
 }
 
 .star::after {
-	transform: rotate(35deg);
+    transform: rotate(35deg);
 }
 </style>
diff --git a/src/components/features/MasterButton.vue b/src/components/features/MasterButton.vue
index c7f4b95..6bb6525 100644
--- a/src/components/features/MasterButton.vue
+++ b/src/components/features/MasterButton.vue
@@ -1,48 +1,50 @@
 <template>
-	<button @click="press" :class="{ feature: true, can: unlocked, locked: !unlocked }" :style="style">
-		<component :is="masterButtonDisplay" />
-	</button>
+    <button
+        @click="press"
+        :class="{ feature: true, can: unlocked, locked: !unlocked }"
+        :style="style"
+    >
+        <component :is="masterButtonDisplay" />
+    </button>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { CoercableComponent } from "@/typings/component";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent, PropType } from "vue";
 
-export default {
-	name: 'master-button',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		display: [ String, Object ]
-	},
-	emits: [ 'press' ],
-	computed: {
-		style() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.['master-button']
-			];
-		},
-		unlocked() {
-			return player[this.layer || this.tab.layer].unlocked;
-		},
-		masterButtonDisplay() {
-			if (this.display) {
-				return coerceComponent(this.display);
-			}
-			if (layers[this.layer || this.tab.layer].clickables?.masterButtonDisplay) {
-				return coerceComponent(layers[this.layer || this.tab.layer].clickables?.masterButtonDisplay);
-			}
-			return coerceComponent("Click Me!");
-		}
-	},
-	methods: {
-		press() {
-			this.$emit("press");
-		}
-	}
-};
+export default defineComponent({
+    name: "master-button",
+    mixins: [InjectLayerMixin],
+    props: {
+        display: [String, Object] as PropType<CoercableComponent>
+    },
+    emits: ["press"],
+    computed: {
+        style(): Partial<CSSStyleDeclaration> | undefined {
+            return layers[this.layer].componentStyles?.["master-button"];
+        },
+        unlocked(): boolean {
+            return player.layers[this.layer].unlocked;
+        },
+        masterButtonDisplay(): Component | string {
+            if (this.display) {
+                return coerceComponent(this.display);
+            }
+            if (layers[this.layer].clickables?.masterButtonDisplay) {
+                return coerceComponent(layers[this.layer].clickables!.masterButtonDisplay!);
+            }
+            return coerceComponent("Click Me!");
+        }
+    },
+    methods: {
+        press() {
+            this.$emit("press");
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/Milestone.vue b/src/components/features/Milestone.vue
index ea7e76a..e286ed8 100644
--- a/src/components/features/Milestone.vue
+++ b/src/components/features/Milestone.vue
@@ -1,53 +1,58 @@
 <template>
-	<div v-if="milestone.shown" :style="style" :class="{ feature: true,  milestone: true, done: milestone.earned }">
-		<div v-if="requirementDisplay"><component :is="requirementDisplay" /></div>
-		<div v-if="effectDisplay"><component :is="effectDisplay" /></div>
-		<component v-if="optionsDisplay" :is="optionsDisplay" />
-		<branch-node :branches="milestone.branches" :id="id" featureType="milestone" />
-	</div>
+    <div
+        v-if="milestone.shown"
+        :style="style"
+        :class="{ feature: true, milestone: true, done: milestone.earned }"
+    >
+        <div v-if="requirementDisplay"><component :is="requirementDisplay" /></div>
+        <div v-if="effectDisplay"><component :is="effectDisplay" /></div>
+        <component v-if="optionsDisplay" :is="optionsDisplay" />
+        <branch-node :branches="milestone.branches" :id="id" featureType="milestone" />
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Milestone } from "@/typings/features/milestone";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'milestone',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		milestone() {
-			return layers[this.layer || this.tab.layer].milestones[this.id];
-		},
-		style() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.milestone,
-				this.milestone.style
-			];
-		},
-		requirementDisplay() {
-			if (this.milestone.requirementDisplay) {
-				return coerceComponent(this.milestone.requirementDisplay, 'h3');
-			}
-			return null;
-		},
-		effectDisplay() {
-			if (this.milestone.effectDisplay) {
-				return coerceComponent(this.milestone.effectDisplay, 'b');
-			}
-			return null;
-		},
-		optionsDisplay() {
-			if (this.milestone.optionsDisplay && this.milestone.earned) {
-				return coerceComponent(this.milestone.optionsDisplay, 'div');
-			}
-			return null;
-		}
-	}
-};
+export default defineComponent({
+    name: "milestone",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        milestone(): Milestone {
+            return layers[this.layer].milestones!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [layers[this.layer].componentStyles?.milestone, this.milestone.style];
+        },
+        requirementDisplay(): Component | string | null {
+            if (this.milestone.requirementDisplay) {
+                return coerceComponent(this.milestone.requirementDisplay, "h3");
+            }
+            return null;
+        },
+        effectDisplay(): Component | string | null {
+            if (this.milestone.effectDisplay) {
+                return coerceComponent(this.milestone.effectDisplay, "b");
+            }
+            return null;
+        },
+        optionsDisplay(): Component | string | null {
+            if (this.milestone.optionsDisplay && this.milestone.earned) {
+                return coerceComponent(this.milestone.optionsDisplay, "div");
+            }
+            return null;
+        }
+    }
+});
 </script>
 
 <style scoped>
diff --git a/src/components/features/Milestones.vue b/src/components/features/Milestones.vue
index 3392307..a426334 100644
--- a/src/components/features/Milestones.vue
+++ b/src/components/features/Milestones.vue
@@ -1,27 +1,32 @@
 <template>
-	<div v-if="filteredMilestones" class="table">
-		<milestone v-for="(milestone, id) in filteredMilestones" :key="id" :id="id" />
-	</div>
+    <div v-if="filteredMilestones" class="table">
+        <milestone v-for="(milestone, id) in filteredMilestones" :key="id" :id="id" />
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Milestone } from "@/typings/features/milestone";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'milestones',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		milestones: Array
-	},
-	computed: {
-		filteredMilestones() {
-			return getFiltered(layers[this.layer || this.tab.layer].milestones, this.milestones);
-		}
-	}
-};
+export default defineComponent({
+    name: "milestones",
+    mixins: [InjectLayerMixin],
+    props: {
+        milestones: {
+            type: Object as PropType<Array<string>>
+        }
+    },
+    computed: {
+        filteredMilestones(): Record<string, Milestone> {
+            if (layers[this.layer].milestones) {
+                return getFiltered<Milestone>(layers[this.layer].milestones!.data, this.milestones);
+            }
+            return {};
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/features/PrestigeButton.vue b/src/components/features/PrestigeButton.vue
index 64e287a..cfafc3d 100644
--- a/src/components/features/PrestigeButton.vue
+++ b/src/components/features/PrestigeButton.vue
@@ -1,48 +1,49 @@
 <template>
-	<button :style="style" @click="resetLayer"
-		:class="{ [layer || tab.layer]: true, reset: true, locked: !canReset, can: canReset }" >
-		<component v-if="prestigeButtonDisplay" :is="prestigeButtonDisplay" />
-		<default-prestige-button-display v-else />
-	</button>
+    <button
+        :style="style"
+        @click="resetLayer"
+        :class="{ [layer]: true, reset: true, locked: !canReset, can: canReset }"
+    >
+        <component v-if="prestigeButtonDisplay" :is="prestigeButtonDisplay" />
+        <default-prestige-button-display v-else />
+    </button>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { resetLayer } from '../../util/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { resetLayer } from "@/util/layers";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'prestige-button',
-	inject: [ 'tab' ],
-	props: {
-		layer: String
-	},
-	computed: {
-		canReset() {
-			return layers[this.layer || this.tab.layer].canReset;
-		},
-		color() {
-			return layers[this.layer || this.tab.layer].color;
-		},
-		prestigeButtonDisplay() {
-			if (layers[this.layer || this.tab.layer].prestigeButtonDisplay) {
-				return coerceComponent(layers[this.layer || this.tab.layer].prestigeButtonDisplay);
-			}
-			return null;
-		},
-		style() {
-			return [
-				this.canReset ? { 'background-color': this.color } : {},
-				layers[this.layer || this.tab.layer].componentStyles?.['prestige-button']
-			];
-		}
-	},
-	methods: {
-		resetLayer() {
-			resetLayer(this.layer || this.tab.layer);
-		}
-	}
-};
+export default defineComponent({
+    name: "prestige-button",
+    mixins: [InjectLayerMixin],
+    computed: {
+        canReset(): boolean {
+            return layers[this.layer].canReset;
+        },
+        color(): string {
+            return layers[this.layer].color;
+        },
+        prestigeButtonDisplay(): Component | string | null {
+            if (layers[this.layer].prestigeButtonDisplay) {
+                return coerceComponent(layers[this.layer].prestigeButtonDisplay!);
+            }
+            return null;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.canReset ? { backgroundColor: this.color } : undefined,
+                layers[this.layer].componentStyles?.["prestige-button"]
+            ];
+        }
+    },
+    methods: {
+        resetLayer() {
+            resetLayer(this.layer);
+        }
+    }
+});
 </script>
 
 <style scoped>
diff --git a/src/components/features/ResourceDisplay.vue b/src/components/features/ResourceDisplay.vue
index 0c0b3d2..02b56a9 100644
--- a/src/components/features/ResourceDisplay.vue
+++ b/src/components/features/ResourceDisplay.vue
@@ -1,55 +1,76 @@
 <template>
-	<div class="resource-display" :class="{ empty }">
-		<div v-if="baseAmount != undefined">You have {{ baseAmount }} {{ baseResource }}</div>
-		<div v-if="passiveGeneration != undefined">You are gaining {{ passiveGeneration }} {{ resource }} per second</div>
-		<spacer v-if="(baseAmount != undefined || passiveGeneration != undefined) && (best != undefined || total != undefined)" />
-		<div v-if="best != undefined">Your best {{ resource }} is {{ best }}</div>
-		<div v-if="total != undefined">You have made a total of {{ total }} {{ resource }}</div>
-	</div>
+    <div class="resource-display" :class="{ empty }">
+        <div v-if="baseAmount != undefined && baseResource != undefined">
+            You have {{ baseAmount }} {{ baseResource }}
+        </div>
+        <div v-if="passiveGeneration != undefined">
+            You are gaining {{ passiveGeneration }} {{ resource }} per second
+        </div>
+        <spacer
+            v-if="
+                (baseAmount != undefined || passiveGeneration != undefined) &&
+                    (best != undefined || total != undefined)
+            "
+        />
+        <div v-if="best != undefined">Your best {{ resource }} is {{ best }}</div>
+        <div v-if="total != undefined">You have made a total of {{ total }} {{ resource }}</div>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import Decimal, { formatWhole } from '../../util/bignum';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { DecimalSource } from "@/lib/break_eternity";
+import Decimal, { formatWhole } from "@/util/bignum";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'resource-display',
-	inject: [ 'tab' ],
-	props: {
-		layer: String
-	},
-	computed: {
-		baseAmount() {
-			return layers[this.layer || this.tab.layer].baseAmount ? formatWhole(layers[this.layer || this.tab.layer].baseAmount) : null;
-		},
-		baseResource() {
-			return layers[this.layer || this.tab.layer].baseResource;
-		},
-		passiveGeneration() {
-			return layers[this.layer || this.tab.layer].passiveGeneration ?
-				formatWhole(Decimal.times(layers[this.layer || this.tab.layer].resetGain,
-					layers[this.layer || this.tab.layer].passiveGeneration)) :
-				null;
-		},
-		resource() {
-			return layers[this.layer || this.tab.layer].resource;
-		},
-		best() {
-			return player[this.layer || this.tab.layer].best ? formatWhole(player[this.layer || this.tab.layer].best) : null;
-		},
-		total() {
-			return player[this.layer || this.tab.layer].total ? formatWhole(player[this.layer || this.tab.layer].total) : null;
-		},
-		empty() {
-			return !(this.baseAmount || this.passiveGeneration || this.best || this.total);
-		}
-	}
-};
+export default defineComponent({
+    name: "resource-display",
+    mixins: [InjectLayerMixin],
+    computed: {
+        baseAmount(): string | null {
+            return layers[this.layer].baseAmount
+                ? formatWhole(layers[this.layer].baseAmount!)
+                : null;
+        },
+        baseResource(): string | undefined {
+            return layers[this.layer].baseResource;
+        },
+        passiveGeneration(): string | null {
+            return layers[this.layer].passiveGeneration
+                ? formatWhole(
+                      Decimal.times(
+                          layers[this.layer].resetGain,
+                          layers[this.layer].passiveGeneration === true
+                              ? 1
+                              : (layers[this.layer].passiveGeneration as DecimalSource)
+                      )
+                  )
+                : null;
+        },
+        resource(): string {
+            return layers[this.layer].resource;
+        },
+        best(): string | null {
+            return player.layers[this.layer].best
+                ? formatWhole(player.layers[this.layer].best as Decimal)
+                : null;
+        },
+        total(): string | null {
+            return player.layers[this.layer].total
+                ? formatWhole(player.layers[this.layer].total as Decimal)
+                : null;
+        },
+        empty(): boolean {
+            return !(this.baseAmount || this.passiveGeneration || this.best || this.total);
+        }
+    }
+});
 </script>
 
 <style scoped>
 .resource-display:not(.empty) {
-	margin: 10px;
+    margin: 10px;
 }
-</style>
\ No newline at end of file
+</style>
diff --git a/src/components/features/RespecButton.vue b/src/components/features/RespecButton.vue
index e9c117a..f7fbf15 100644
--- a/src/components/features/RespecButton.vue
+++ b/src/components/features/RespecButton.vue
@@ -1,90 +1,102 @@
 <template>
-	<div style="display: flex">
-		<tooltip display="Disable respec confirmation">
-			<Toggle :value="confirmRespec" @change="setConfirmRespec" />
-		</tooltip>
-		<button @click="respec" :class="{ feature: true, respec: true, can: unlocked, locked: !unlocked }"
-			style="margin-right: 18px" :style="style">
-			<component :is="respecButtonDisplay" />
-		</button>
-		<Modal :show="confirming" @close="cancel">
-			<template v-slot:header>
-				<h2>Confirm Respec</h2>
-			</template>
-			<template v-slot:body>
-				<slot name="respec-warning">
-					<div>Are you sure you want to respec? This will force you to do a {{ name }} respec as well!</div>
-				</slot>
-			</template>
-			<template v-slot:footer>
-				<div class="modal-footer">
-					<div class="modal-flex-grow"></div>
-					<danger-button class="button modal-button" @click="confirm" skipConfirm>Yes</danger-button>
-					<button class="button modal-button" @click="cancel">Cancel</button>
-				</div>
-			</template>
-		</Modal>
-	</div>
+    <div style="display: flex">
+        <tooltip display="Disable respec confirmation">
+            <Toggle :value="confirmRespec" @change="setConfirmRespec" />
+        </tooltip>
+        <button
+            @click="respec"
+            :class="{ feature: true, respec: true, can: unlocked, locked: !unlocked }"
+            style="margin-right: 18px"
+            :style="style"
+        >
+            <component :is="respecButtonDisplay" />
+        </button>
+        <Modal :show="confirming" @close="cancel">
+            <template v-slot:header>
+                <h2>Confirm Respec</h2>
+            </template>
+            <template v-slot:body>
+                <slot name="respec-warning">
+                    <component :is="respecWarning" />
+                </slot>
+            </template>
+            <template v-slot:footer>
+                <div class="modal-footer">
+                    <div class="modal-flex-grow"></div>
+                    <danger-button class="button modal-button" @click="confirm" skipConfirm
+                        >Yes</danger-button
+                    >
+                    <button class="button modal-button" @click="cancel">Cancel</button>
+                </div>
+            </template>
+        </Modal>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { CoercableComponent } from "@/typings/component";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent, PropType } from "vue";
 
-export default {
-	name: 'respec-button',
-	inject: [ 'tab' ],
-	data() {
-		return {
-			confirming: false
-		};
-	},
-	props: {
-		layer: String,
-		confirmRespec: Boolean,
-		display: [ String, Object ]
-	},
-	emits: [ 'set-confirm-respec', 'respec' ],
-	computed: {
-		style() {
-			return [
-				layers[this.layer || this.tab.layer].componentStyles?.['respec-button']
-			];
-		},
-		unlocked() {
-			return player[this.layer || this.tab.layer].unlocked;
-		},
-		respecButtonDisplay() {
-			if (this.display) {
-				return coerceComponent(this.display);
-			}
-			return coerceComponent("Respec");
-		},
-		name() {
-			return layers[this.layer || this.tab.layer].name || this.layer || this.tab.layer;
-		}
-	},
-	methods: {
-		setConfirmRespec(value) {
-			this.$emit("set-confirm-respec", value);
-		},
-		respec() {
-			if (this.confirmRespec) {
-				this.confirming = true;
-			} else {
-				this.$emit("respec");
-			}
-		},
-		confirm() {
-			this.$emit("respec");
-			this.confirming = false;
-		},
-		cancel() {
-			this.confirming = false;
-		}
-	}
-};
+export default defineComponent({
+    name: "respec-button",
+    mixins: [InjectLayerMixin],
+    props: {
+        confirmRespec: Boolean,
+        display: [String, Object] as PropType<CoercableComponent>,
+        respecWarningDisplay: [String, Object] as PropType<CoercableComponent>
+    },
+    data() {
+        return {
+            confirming: false
+        };
+    },
+    emits: ["set-confirm-respec", "respec"],
+    computed: {
+        style(): Partial<CSSStyleDeclaration> | undefined {
+            return layers[this.layer].componentStyles?.["respec-button"];
+        },
+        unlocked(): boolean {
+            return player.layers[this.layer].unlocked;
+        },
+        respecButtonDisplay(): Component | string {
+            if (this.display) {
+                return coerceComponent(this.display);
+            }
+            return coerceComponent("Respec");
+        },
+        respecWarning(): Component | string {
+            if (this.respecWarningDisplay) {
+                return coerceComponent(this.respecWarningDisplay);
+            }
+            return coerceComponent(
+                `Are you sure you want to respec? This will force you to do a ${layers[this.layer]
+                    .name || this.layer} respec as well!`
+            );
+        }
+    },
+    methods: {
+        setConfirmRespec(value: boolean) {
+            this.$emit("set-confirm-respec", value);
+        },
+        respec() {
+            if (this.confirmRespec) {
+                this.confirming = true;
+            } else {
+                this.$emit("respec");
+            }
+        },
+        confirm() {
+            this.$emit("respec");
+            this.confirming = false;
+        },
+        cancel() {
+            this.confirming = false;
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -96,14 +108,14 @@ export default {
 }
 
 .modal-footer {
-	display: flex;
+    display: flex;
 }
 
 .modal-flex-grow {
-	flex-grow: 1;
+    flex-grow: 1;
 }
 
 .modal-button {
-	margin-left: 10px;
+    margin-left: 10px;
 }
 </style>
diff --git a/src/components/features/Subtab.vue b/src/components/features/Subtab.vue
new file mode 100644
index 0000000..d4a4ad6
--- /dev/null
+++ b/src/components/features/Subtab.vue
@@ -0,0 +1,32 @@
+<template>
+    <LayerProvider :layer="layer" :index="tab.index">
+        <component :is="display" />
+    </LayerProvider>
+</template>
+
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
+
+export default defineComponent({
+    name: "subtab",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        display(): Component | string | undefined {
+            return (
+                layers[this.layer].subtabs![this.id].display &&
+                coerceComponent(layers[this.layer].subtabs![this.id].display!)
+            );
+        }
+    }
+});
+</script>
+
+<style scoped></style>
diff --git a/src/components/features/Upgrade.vue b/src/components/features/Upgrade.vue
index 55c6013..f7485a5 100644
--- a/src/components/features/Upgrade.vue
+++ b/src/components/features/Upgrade.vue
@@ -1,54 +1,65 @@
 <template>
-	<button v-if="upgrade.unlocked" :style="style" @click="buy"
-		:class="{
-			feature: true,
-			[tab.layer]: true,
-			upgrade: true,
-			can: upgrade.canAfford && !upgrade.bought,
-			locked: !upgrade.canAfford && !upgrade.bought,
-			bought: upgrade.bought
-		}" :disabled="!upgrade.canAfford && !upgrade.bought">
-		<component v-if="fullDisplay" :is="fullDisplay" />
-		<default-upgrade-display v-else :id="id" />
-		<branch-node :branches="upgrade.branches" :id="id" featureType="upgrade" />
-	</button>
+    <button
+        v-if="upgrade.unlocked"
+        :style="style"
+        @click="buy"
+        :class="{
+            feature: true,
+            [layer]: true,
+            upgrade: true,
+            can: upgrade.canAfford && !upgrade.bought,
+            locked: !upgrade.canAfford && !upgrade.bought,
+            bought: upgrade.bought
+        }"
+        :disabled="!upgrade.canAfford && !upgrade.bought"
+    >
+        <component v-if="fullDisplay" :is="fullDisplay" />
+        <default-upgrade-display v-else :id="id" />
+        <branch-node :branches="upgrade.branches" :id="id" featureType="upgrade" />
+    </button>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Upgrade } from "@/typings/features/upgrade";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'upgrade',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: [ Number, String ]
-	},
-	computed: {
-		upgrade() {
-			return layers[this.layer || this.tab.layer].upgrades[this.id];
-		},
-		style() {
-			return [
-				this.upgrade.canAfford && !this.upgrade.bought ? { 'background-color': layers[this.layer || this.tab.layer].color } : {},
-				layers[this.layer || this.tab.layer].componentStyles?.upgrade,
-				this.upgrade.style
-			];
-		},
-		fullDisplay() {
-			if (this.upgrade.fullDisplay) {
-				return coerceComponent(this.upgrade.fullDisplay, 'div');
-			}
-			return null;
-		}
-	},
-	methods: {
-		buy() {
-			this.upgrade.buy();
-		}
-	}
-};
+export default defineComponent({
+    name: "upgrade",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    computed: {
+        upgrade(): Upgrade {
+            return layers[this.layer].upgrades!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.upgrade.canAfford && !this.upgrade.bought
+                    ? { backgroundColor: layers[this.layer].color }
+                    : undefined,
+                layers[this.layer].componentStyles?.upgrade,
+                this.upgrade.style
+            ];
+        },
+        fullDisplay(): Component | string | null {
+            if (this.upgrade.fullDisplay) {
+                return coerceComponent(this.upgrade.fullDisplay, "div");
+            }
+            return null;
+        }
+    },
+    methods: {
+        buy() {
+            this.upgrade.buy();
+        }
+    }
+});
 </script>
 
 <style scoped>
diff --git a/src/components/features/Upgrades.vue b/src/components/features/Upgrades.vue
index 8546b7b..99a61a2 100644
--- a/src/components/features/Upgrades.vue
+++ b/src/components/features/Upgrades.vue
@@ -1,36 +1,45 @@
 <template>
-	<div v-if="filteredUpgrades" class="table">
-		<template v-if="filteredUpgrades.rows && filteredUpgrades.cols">
-			<div v-for="row in filteredUpgrades.rows" class="row" :key="row">
-				<div v-for="col in filteredUpgrades.cols" :key="col">
-					<upgrade v-if="filteredUpgrades[row * 10 + col] !== undefined" class="align" :id="row * 10 + col" />
-				</div>
-			</div>
-		</template>
-		<row v-else>
-			<upgrade v-for="(upgrade, id) in filteredUpgrades" :key="id" class="align" :id="id" />
-		</row>
-	</div>
+    <div v-if="filteredUpgrades" class="table">
+        <template v-if="filteredUpgrades.rows && filteredUpgrades.cols">
+            <div v-for="row in filteredUpgrades.rows" class="row" :key="row">
+                <div v-for="col in filteredUpgrades.cols" :key="col">
+                    <upgrade
+                        v-if="filteredUpgrades[row * 10 + col] !== undefined"
+                        class="align"
+                        :id="row * 10 + col"
+                    />
+                </div>
+            </div>
+        </template>
+        <row v-else>
+            <upgrade v-for="(upgrade, id) in filteredUpgrades" :key="id" class="align" :id="id" />
+        </row>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { getFiltered } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { Upgrade } from "@/typings/features/upgrade";
+import { getFiltered, InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'upgrades',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		upgrades: Array
-	},
-	computed: {
-		filteredUpgrades() {
-			return getFiltered(layers[this.layer || this.tab.layer].upgrades, this.upgrades);
-		}
-	}
-};
+export default defineComponent({
+    name: "upgrades",
+    mixins: [InjectLayerMixin],
+    props: {
+        upgrades: {
+            type: Object as PropType<Array<string>>
+        }
+    },
+    computed: {
+        filteredUpgrades(): Record<string, Upgrade> {
+            if (layers[this.layer].upgrades) {
+                return getFiltered<Upgrade>(layers[this.layer].upgrades!.data, this.upgrades);
+            }
+            return {};
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/fields/DangerButton.vue b/src/components/fields/DangerButton.vue
index 799396c..48e89f9 100644
--- a/src/components/fields/DangerButton.vue
+++ b/src/components/fields/DangerButton.vue
@@ -1,76 +1,78 @@
 <template>
-	<span class="container" :class="{ confirming }">
-		<span v-if="confirming">Are you sure?</span>
-		<button @click.stop="click" class="button danger" :disabled="disabled">
-			<span v-if="confirming">Yes</span>
-			<slot v-else />
-		</button>
-		<button v-if="confirming" class="button" @click.stop="cancel">No</button>
-	</span>
+    <span class="container" :class="{ confirming }">
+        <span v-if="confirming">Are you sure?</span>
+        <button @click.stop="click" class="button danger" :disabled="disabled">
+            <span v-if="confirming">Yes</span>
+            <slot v-else />
+        </button>
+        <button v-if="confirming" class="button" @click.stop="cancel">No</button>
+    </span>
 </template>
 
-<script>
-export default {
-	name: 'danger-button',
-	data() {
-		return {
-			confirming: false
-		}
-	},
-	props: {
-		disabled: Boolean,
-		skipConfirm: Boolean
-	},
-	emits: [ 'click', 'confirmingChanged' ],
-	watch: {
-		confirming(newValue) {
-			this.$emit('confirmingChanged', newValue);
-		}
-	},
-	methods: {
-		click() {
-			if (this.skipConfirm) {
-				this.$emit('click');
-				return;
-			}
-			if (this.confirming) {
-				this.$emit('click');
-			}
-			this.confirming = !this.confirming;
-		},
-		cancel() {
-			this.confirming = false;
-		}
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "danger-button",
+    data() {
+        return {
+            confirming: false
+        };
+    },
+    props: {
+        disabled: Boolean,
+        skipConfirm: Boolean
+    },
+    emits: ["click", "confirmingChanged"],
+    watch: {
+        confirming(newValue) {
+            this.$emit("confirmingChanged", newValue);
+        }
+    },
+    methods: {
+        click() {
+            if (this.skipConfirm) {
+                this.$emit("click");
+                return;
+            }
+            if (this.confirming) {
+                this.$emit("click");
+            }
+            this.confirming = !this.confirming;
+        },
+        cancel() {
+            this.confirming = false;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .container {
-	display: flex;
+    display: flex;
     align-items: center;
 }
 
 .container.confirming button {
-	font-size: 1em;
+    font-size: 1em;
 }
 
 .container > * {
-	margin: 0 4px;
+    margin: 0 4px;
 }
 </style>
 
 <style>
 .danger {
     position: relative;
-	border: solid 2px var(--danger);
+    border: solid 2px var(--danger);
     border-right-width: 16px;
 }
 
 .danger::after {
     position: absolute;
-	content:  "!";
-	color: white;
+    content: "!";
+    color: white;
     right: -13px;
 }
 </style>
diff --git a/src/components/fields/FeedbackButton.vue b/src/components/fields/FeedbackButton.vue
index 49a77bf..269ec0f 100644
--- a/src/components/fields/FeedbackButton.vue
+++ b/src/components/fields/FeedbackButton.vue
@@ -1,75 +1,82 @@
 <template>
-	<button @click.stop="click" class="feedback" :class="{ activated, left }"><slot /></button>
+    <button @click.stop="click" class="feedback" :class="{ activated, left }">
+        <slot />
+    </button>
 </template>
 
-<script>
-export default {
-	name: 'feedback-button',
-	data() {
-		return {
-			activated: false,
-			activatedTimeout: null
-		}
-	},
-	props: {
-		left: Boolean
-	},
-	emits: [ 'click' ],
-	methods: {
-		click() {
-			this.$emit('click');
+<script lang="ts">
+import { defineComponent } from "vue";
 
-			// Give feedback to user
-			if (this.activatedTimeout) {
-				clearTimeout(this.activatedTimeout);
-			}
-			this.activated = false;
-			this.$nextTick(() => {
-				this.activated = true;
-				this.activatedTimeout = setTimeout(() => this.activated = false, 500);
-			});
-		}
-	}
-};
+export default defineComponent({
+    name: "feedback-button",
+    data() {
+        return {
+            activated: false,
+            activatedTimeout: null
+        } as {
+            activated: boolean;
+            activatedTimeout: number | null;
+        };
+    },
+    props: {
+        left: Boolean
+    },
+    emits: ["click"],
+    methods: {
+        click() {
+            this.$emit("click");
+
+            // Give feedback to user
+            if (this.activatedTimeout) {
+                clearTimeout(this.activatedTimeout);
+            }
+            this.activated = false;
+            this.$nextTick(() => {
+                this.activated = true;
+                this.activatedTimeout = setTimeout(() => (this.activated = false), 500);
+            });
+        }
+    }
+});
 </script>
 
 <style scoped>
 .feedback {
-	position: relative;
+    position: relative;
 }
 
 .feedback::after {
-	position: absolute;
-	left: calc(100% + 5px);
-	top: 50%;
-	transform: translateY(-50%);
-	content: '✔';
-	opacity: 0;
-	pointer-events: none;
-	box-shadow: inset 0 0 0 35px rgba(111,148,182,0);
-	text-shadow: none;
+    position: absolute;
+    left: calc(100% + 5px);
+    top: 50%;
+    transform: translateY(-50%);
+    content: "✔";
+    opacity: 0;
+    pointer-events: none;
+    box-shadow: inset 0 0 0 35px rgba(111, 148, 182, 0);
+    text-shadow: none;
 }
 
 .feedback.left::after {
-	left: unset;
-	right: calc(100% + 5px);
+    left: unset;
+    right: calc(100% + 5px);
 }
 
 .feedback.activated::after {
-	animation: feedback .5s ease-out forwards;
+    animation: feedback 0.5s ease-out forwards;
 }
 
 @keyframes feedback {
-	0% {
-		opacity: 1;
-		transform: scale3d(0.4, 0.4, 1), translateY(-50%);
-	}
-	80% {
-		opacity: 0.1;
-	}
-	100% {
-		opacity: 0;
-		transform: scale3d(1.2, 1.2, 1), translateY(-50%);
-	}
+    0% {
+        opacity: 1;
+        transform: scale3d(0.4, 0.4, 1), translateY(-50%);
+    }
+    80% {
+        opacity: 0.1;
+    }
+    100% {
+        opacity: 0;
+        transform: scale3d(1.2, 1.2, 1), translateY(-50%);
+    }
 }
 </style>
diff --git a/src/components/fields/Select.vue b/src/components/fields/Select.vue
index 8a55ae6..4de7c0f 100644
--- a/src/components/fields/Select.vue
+++ b/src/components/fields/Select.vue
@@ -1,32 +1,42 @@
 <template>
-	<div class="field">
-		<span class="field-title" v-if="title">{{ title }}</span>
-		<vue-select :options="options" :model-value="value" @update:modelValue="setSelected" label-by="label" :value-by="getValue" :placeholder="placeholder" :close-on-select="closeOnSelect" />
-	</div>
+    <div class="field">
+        <span class="field-title" v-if="title">{{ title }}</span>
+        <vue-select
+            :options="options"
+            :model-value="value"
+            @update:modelValue="setSelected"
+            label-by="label"
+            :value-by="getValue"
+            :placeholder="placeholder"
+            :close-on-select="closeOnSelect"
+        />
+    </div>
 </template>
 
-<script>
-export default {
-	name: 'Select',
-	props: {
-		title: String,
-		options: Array, // https://vue-select.org/guide/options.html#options-prop
-		value: [ String, Object ],
-		default: [ String, Object ],
-		placeholder: String,
-		closeOnSelect: Boolean
-	},
-	emits: [ 'change' ],
-	methods: {
-		setSelected(value) {
-			value = value || this.default;
-			this.$emit('change', value);
-		},
-		getValue(item) {
-			return item?.value;
-		}
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "Select",
+    props: {
+        title: String,
+        options: Array, // https://vue-select.org/guide/options.html#options-prop
+        value: [String, Object],
+        default: [String, Object],
+        placeholder: String,
+        closeOnSelect: Boolean
+    },
+    emits: ["change"],
+    methods: {
+        setSelected(value: any) {
+            value = value || this.default;
+            this.$emit("change", value);
+        },
+        getValue(item?: { value: any }) {
+            return item?.value;
+        }
+    }
+});
 </script>
 
 <style>
@@ -35,28 +45,28 @@ export default {
 }
 
 .field-buttons .vue-select {
-	width: unset;
+    width: unset;
 }
 
 .vue-select,
 .vue-dropdown {
-	border-color: rgba(var(--color), .26);
+    border-color: rgba(var(--color), 0.26);
 }
 
 .vue-dropdown {
-	background: var(--secondary-background);
+    background: var(--secondary-background);
 }
 
 .vue-dropdown-item {
-	color: var(--color);
+    color: var(--color);
 }
 
 .vue-dropdown-item.highlighted {
-	background-color: var(--background-tooltip);
+    background-color: var(--background-tooltip);
 }
 
 .vue-dropdown-item.selected,
 .vue-dropdown-item.highlighted.selected {
-	background-color: var(--bought);
+    background-color: var(--bought);
 }
 </style>
diff --git a/src/components/fields/Slider.vue b/src/components/fields/Slider.vue
index 0be60e2..92203e5 100644
--- a/src/components/fields/Slider.vue
+++ b/src/components/fields/Slider.vue
@@ -1,27 +1,35 @@
 <template>
-	<div class="field">
-		<span class="field-title" v-if="title">{{ title }}</span>
-		<tooltip :display="`${value}`" :class="{ fullWidth: !title }">
-			<input type="range" :value="value" @input="e => $emit('change', parseInt(e.target.value))" :min="min" :max="max" />
-		</tooltip>
-	</div>
+    <div class="field">
+        <span class="field-title" v-if="title">{{ title }}</span>
+        <tooltip :display="`${value}`" :class="{ fullWidth: !title }">
+            <input
+                type="range"
+                :value="value"
+                @input="e => $emit('change', parseInt(e.target.value))"
+                :min="min"
+                :max="max"
+            />
+        </tooltip>
+    </div>
 </template>
 
-<script>
-export default {
-	name: 'Slider',
-	props: {
-		title: String,
-		value: Number,
-		min: Number,
-		max: Number
-	},
-	emits: [ 'change' ]
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "Slider",
+    props: {
+        title: String,
+        value: Number,
+        min: Number,
+        max: Number
+    },
+    emits: ["change"]
+});
 </script>
 
 <style scoped>
 .fullWidth {
-	width: 100%;
+    width: 100%;
 }
 </style>
diff --git a/src/components/fields/Text.vue b/src/components/fields/Text.vue
index 44cd585..c166b5b 100644
--- a/src/components/fields/Text.vue
+++ b/src/components/fields/Text.vue
@@ -1,47 +1,62 @@
 <template>
-	<form @submit.prevent="$emit('submit')">
-		<div class="field">
-			<span class="field-title" v-if="title">{{ title }}</span>
-			<textarea-autosize v-if="textarea" :placeholder="placeholder" :value="value" :maxHeight="maxHeight"
-				@input="value => $emit('change', value)" ref="field" />
-			<input v-else type="text" :value="value" @input="e => $emit('change', e.target.value)" :placeholder="placeholder" ref="field"
-				:class="{ fullWidth: !title }" />
-		</div>
-	</form>
+    <form @submit.prevent="$emit('submit')">
+        <div class="field">
+            <span class="field-title" v-if="title">{{ title }}</span>
+            <textarea-autosize
+                v-if="textarea"
+                :placeholder="placeholder"
+                :value="value"
+                :maxHeight="maxHeight"
+                @input="value => $emit('change', value)"
+                ref="field"
+            />
+            <input
+                v-else
+                type="text"
+                :value="value"
+                @input="e => $emit('change', e.target.value)"
+                :placeholder="placeholder"
+                ref="field"
+                :class="{ fullWidth: !title }"
+            />
+        </div>
+    </form>
 </template>
 
-<script>
-export default {
-	name: 'TextField',
-	props: {
-		title: String,
-		value: String,
-		textarea: Boolean,
-		placeholder: String,
-		maxHeight: Number
-	},
-	emits: [ 'change', 'submit', 'input' ],
-	mounted() {
-		this.$refs.field.focus();
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "TextField",
+    props: {
+        title: String,
+        value: String,
+        textarea: Boolean,
+        placeholder: String,
+        maxHeight: Number
+    },
+    emits: ["change", "submit"],
+    mounted() {
+        (this.$refs.field as HTMLElement).focus();
+    }
+});
 </script>
 
 <style scoped>
 form {
-	margin: 0;
-	width: 100%;
+    margin: 0;
+    width: 100%;
 }
 
 .field > * {
-	margin: 0;
+    margin: 0;
 }
 
 input {
-	width: 50%;
+    width: 50%;
 }
 
 .fullWidth {
-	width: 100%;
+    width: 100%;
 }
 </style>
diff --git a/src/components/fields/Toggle.vue b/src/components/fields/Toggle.vue
index 1d69bb0..a90a110 100644
--- a/src/components/fields/Toggle.vue
+++ b/src/components/fields/Toggle.vue
@@ -1,98 +1,101 @@
 <template>
-	<label class="field">
-		<input type="checkbox" class="toggle" :checked="value" @input="handleInput" />
-		<span>{{ title }}</span>
-	</label>
+    <label class="field">
+        <input type="checkbox" class="toggle" :checked="value" @input="handleInput" />
+        <span>{{ title }}</span>
+    </label>
 </template>
 
-<script>
+<script lang="ts">
+import { defineComponent } from "vue";
+
 // Reference: https://codepen.io/finnhvman/pen/pOeyjE
-export default {
-	name: 'Toggle',
-	props: {
-		title: String,
-		value: Boolean
-	},
-	emits: [ 'change' ],
-	methods: {
-		handleInput(e) {
-			this.$emit('change', e.target.checked);
-		}
-	}
-};
+export default defineComponent({
+    name: "Toggle",
+    props: {
+        title: String,
+        value: Boolean
+    },
+    emits: ["change"],
+    methods: {
+        handleInput(e: InputEvent) {
+            this.$emit("change", (e.target as HTMLInputElement).checked);
+        }
+    }
+});
 </script>
 
 <style scoped>
 .field {
-	cursor: pointer;
+    cursor: pointer;
 }
 
 input {
-	appearance: none;
-	pointer-events: none;
+    appearance: none;
+    pointer-events: none;
 }
 
 span {
-	width: 100%;
+    width: 100%;
 }
 
 /* track */
 span::before {
-	content: "";
-	float: right;
-	margin: 5px 0 5px 10px;
-	border-radius: 7px;
-	width: 36px;
-	height: 14px;
-	background-color: rgba(0, 0, 0, 0.38);
-	vertical-align: top;
-	transition: background-color 0.2s, opacity 0.2s;
+    content: "";
+    float: right;
+    margin: 5px 0 5px 10px;
+    border-radius: 7px;
+    width: 36px;
+    height: 14px;
+    background-color: rgba(0, 0, 0, 0.38);
+    vertical-align: top;
+    transition: background-color 0.2s, opacity 0.2s;
 }
 
 /* thumb */
 span::after {
-	content: "";
-	position: absolute;
-	top: 6px;
-	right: 16px;
-	border-radius: 50%;
-	width: 20px;
-	height: 20px;
-	background-color: white;
-	box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
-	transition: background-color 0.2s, transform 0.2s;
+    content: "";
+    position: absolute;
+    top: 6px;
+    right: 16px;
+    border-radius: 50%;
+    width: 20px;
+    height: 20px;
+    background-color: white;
+    box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+        0 1px 5px 0 rgba(0, 0, 0, 0.12);
+    transition: background-color 0.2s, transform 0.2s;
 }
 
 input:checked + span::before {
-	background-color: rgba(33, 150, 243, 0.6);
+    background-color: rgba(33, 150, 243, 0.6);
 }
 
 input:checked + span::after {
-	background-color: rgb(33, 150, 243);
-	transform: translateX(16px);
+    background-color: rgb(33, 150, 243);
+    transform: translateX(16px);
 }
 
 /* active */
 input:active + span::before {
-	background-color: rgba(33, 150, 243, 0.6);
+    background-color: rgba(33, 150, 243, 0.6);
 }
 
 input:checked:active + span::before {
-	background-color: rgba(0, 0, 0, 0.38);
+    background-color: rgba(0, 0, 0, 0.38);
 }
 
 /* disabled */
 input:disabled + span {
-	color: black;
-	opacity: 0.38;
-	cursor: default;
+    color: black;
+    opacity: 0.38;
+    cursor: default;
 }
 
 input:disabled + span::before {
-	background-color: rgba(0, 0, 0, 0.38);
+    background-color: rgba(0, 0, 0, 0.38);
 }
 
 input:checked:disabled + span::before {
-	background-color: rgba(33, 150, 243, 0.6);
+    background-color: rgba(33, 150, 243, 0.6);
 }
 </style>
diff --git a/src/components/index.js b/src/components/index.js
deleted file mode 100644
index 157bbf8..0000000
--- a/src/components/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// Import and register all components,
-// which will allow us to use them in any template strings anywhere in the project
-
-//import TransitionExpand from 'vue-transition-expand';
-//import 'vue-transition-expand/dist/vue-transition-expand.css';
-import CollapseTransition from '@ivanv/vue-collapse-transition/src/CollapseTransition.vue';
-import VueTextareaAutosize from 'vue-textarea-autosize';
-import Sortable from 'vue-sortable';
-import VueNextSelect from 'vue-next-select';
-import 'vue-next-select/dist/index.css';
-
-export function registerComponents(vue) {
-	/* from files */
-	const componentsContext = require.context('./');
-	componentsContext.keys().forEach(path => {
-		const component = componentsContext(path).default;
-		if (component && !(component.name in vue._context.components)) {
-			vue.component(component.name, component);
-		}
-	});
-
-	/* from packages */
-	//vue.use(TransitionExpand);
-	vue.component('collapse-transition', CollapseTransition);
-	vue.use(VueTextareaAutosize);
-	vue.use(Sortable);
-	vue.component('vue-select', VueNextSelect);
-}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..9619597
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,26 @@
+// Import and register all components,
+// which will allow us to use them in any template strings anywhere in the project
+
+import CollapseTransition from "@ivanv/vue-collapse-transition/src/CollapseTransition.vue";
+import VueTextareaAutosize from "vue-textarea-autosize";
+import Sortable from "vue-sortable";
+import VueNextSelect from "vue-next-select";
+import "vue-next-select/dist/index.css";
+import { App } from "vue";
+
+export function registerComponents(vue: App): void {
+    /* from files */
+    const componentsContext = require.context("./");
+    componentsContext.keys().forEach(path => {
+        const component = componentsContext(path).default;
+        if (component && !(component.name in vue._context.components)) {
+            vue.component(component.name, component);
+        }
+    });
+
+    /* from packages */
+    vue.component("collapse-transition", CollapseTransition);
+    vue.use(VueTextareaAutosize);
+    vue.use(Sortable);
+    vue.component("vue-select", VueNextSelect);
+}
diff --git a/src/components/system/Column.vue b/src/components/system/Column.vue
index ef80589..86de362 100644
--- a/src/components/system/Column.vue
+++ b/src/components/system/Column.vue
@@ -1,13 +1,15 @@
 <template>
-     <div class="table">
-          <div class="col">
-               <slot />
-          </div>
-     </div>
+    <div class="table">
+        <div class="col">
+            <slot />
+        </div>
+    </div>
 </template>
 
-<script>
-export default {
-     name: 'column'
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "column"
+});
 </script>
diff --git a/src/components/system/DefaultLayerTab.vue b/src/components/system/DefaultLayerTab.vue
index 444cd19..d464c50 100644
--- a/src/components/system/DefaultLayerTab.vue
+++ b/src/components/system/DefaultLayerTab.vue
@@ -1,43 +1,42 @@
 <template>
-	<infobox v-if="infobox != undefined" :id="infobox" />
-	<main-display />
-	<sticky v-if="showPrestigeButton"><prestige-button /></sticky>
-	<resource-display />
-	<milestones />
-	<component v-if="midsection" :is="midsection" />
-	<clickables />
-	<buyables />
-	<upgrades />
-	<challenges />
-	<achievements />
+    <infobox v-if="infobox != undefined" :id="infobox" />
+    <main-display />
+    <sticky v-if="showPrestigeButton"><prestige-button /></sticky>
+    <resource-display />
+    <milestones />
+    <component v-if="midsection" :is="midsection" />
+    <clickables />
+    <buyables />
+    <upgrades />
+    <challenges />
+    <achievements />
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'default-layer-tab',
-	inject: [ 'tab' ],
-	props: {
-		layer: String
-	},
-	computed: {
-		infobox() {
-			return layers[this.layer || this.tab.layer].infoboxes && Object.keys(layers[this.layer || this.tab.layer].infoboxes)[0];
-		},
-		midsection() {
-			if (layers[this.layer || this.tab.layer].midsection) {
-				return coerceComponent(layers[this.layer || this.tab.layer].midsection);
-			}
-			return null;
-		},
-		showPrestigeButton() {
-			return layers[this.layer || this.tab.layer].type !== "none";
-		},
-	}
-};
+export default defineComponent({
+    name: "default-layer-tab",
+    mixins: [InjectLayerMixin],
+    computed: {
+        infobox(): string | undefined {
+            return (
+                layers[this.layer].infoboxes && Object.keys(layers[this.layer].infoboxes!.data)[0]
+            );
+        },
+        midsection(): Component | string | null {
+            if (layers[this.layer].midsection) {
+                return coerceComponent(layers[this.layer].midsection!);
+            }
+            return null;
+        },
+        showPrestigeButton(): boolean {
+            return layers[this.layer].type !== "none";
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/system/GameOverScreen.vue b/src/components/system/GameOverScreen.vue
index 890f920..bd09e20 100644
--- a/src/components/system/GameOverScreen.vue
+++ b/src/components/system/GameOverScreen.vue
@@ -1,66 +1,77 @@
 <template>
-	<Modal :show="show">
-		<template v-slot:header>
-			<div class="game-over-modal-header">
-				<img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="modInfo" />
-				<div class="game-over-modal-title">
-					<h2>Congratulations!</h2>
-					<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
-				</div>
-			</div>
-		</template>
-		<template v-slot:body="{ shown }">
-			<div v-if="shown">
-				<div>It took you {{ timePlayed }} to beat the game.</div>
-				<br>
-				<div>Please check the Discord to discuss the game or to check for new content updates!</div>
-				<br>
-				<div>
-					<a :href="discordLink">
-						<img src="images/discord.png" class="game-over-modal-discord" />
-						{{ discordName }}
-					</a>
-				</div>
-			</div>
-		</template>
-		<template v-slot:footer>
-			<div class="game-over-footer">
-				<button @click="keepGoing" class="button">Keep Going</button>
-				<button @click="playAgain" class="button danger">Play Again</button>
-			</div>
-		</template>
-	</Modal>
+    <Modal :show="show">
+        <template v-slot:header>
+            <div class="game-over-modal-header">
+                <img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="modInfo" />
+                <div class="game-over-modal-title">
+                    <h2>Congratulations!</h2>
+                    <h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
+                </div>
+            </div>
+        </template>
+        <template v-slot:body="{ shown }">
+            <div v-if="shown">
+                <div>It took you {{ timePlayed }} to beat the game.</div>
+                <br />
+                <div>
+                    Please check the Discord to discuss the game or to check for new content
+                    updates!
+                </div>
+                <br />
+                <div>
+                    <a :href="discordLink">
+                        <img src="images/discord.png" class="game-over-modal-discord" />
+                        {{ discordName }}
+                    </a>
+                </div>
+            </div>
+        </template>
+        <template v-slot:footer>
+            <div class="game-over-footer">
+                <button @click="keepGoing" class="button">Keep Going</button>
+                <button @click="playAgain" class="button danger">Play Again</button>
+            </div>
+        </template>
+    </Modal>
 </template>
 
-<script>
-import modInfo from '../../data/modInfo.json';
-import { hasWon } from '../../data/mod';
-import { formatTime } from '../../util/bignum';
-import player from '../../game/player';
+<script lang="ts">
+import { hasWon } from "@/data/mod";
+import modInfo from "@/data/modInfo.json";
+import player from "@/game/player";
+import { formatTime } from "@/util/bignum";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'GameOverScreen',
-	data() {
-		const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
-		return { title, logo, discordName, discordLink, versionNumber, versionTitle };
-	},
-	computed: {
-		timePlayed() {
-			return formatTime(player.timePlayed);
-		},
-		show() {
-			return hasWon.value && !player.keepGoing;
-		}
-	},
-	methods: {
-		keepGoing() {
-			player.keepGoing = true;
-		},
-		playAgain() {
-			console.warn("Not yet implemented!");
-		}
-	}
-};
+export default defineComponent({
+    name: "GameOverScreen",
+    data() {
+        const { title, logo, discordName, discordLink, versionNumber, versionTitle } = modInfo;
+        return {
+            title,
+            logo,
+            discordName,
+            discordLink,
+            versionNumber,
+            versionTitle
+        };
+    },
+    computed: {
+        timePlayed() {
+            return formatTime(player.timePlayed);
+        },
+        show() {
+            return hasWon.value && !player.keepGoing;
+        }
+    },
+    methods: {
+        keepGoing() {
+            player.keepGoing = true;
+        },
+        playAgain() {
+            console.warn("Not yet implemented!");
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -71,36 +82,36 @@ export default {
 }
 
 .game-over-modal-header * {
-	margin: 0;
+    margin: 0;
 }
 
 .game-over-modal-logo {
-	height: 4em;
+    height: 4em;
     width: 4em;
 }
 
 .game-over-modal-title {
-	padding: 10px 0;
+    padding: 10px 0;
     margin-left: 10px;
 }
 
 .game-over-footer {
-	display: flex;
+    display: flex;
     justify-content: flex-end;
 }
 
 .game-over-footer button {
-	margin: 0 10px;
+    margin: 0 10px;
 }
 
 .game-over-modal-discord-link {
-	display: flex;
-	align-items: center;
+    display: flex;
+    align-items: center;
 }
 
 .game-over-modal-discord {
-	height: 2em;
-	margin: 0;
-	margin-right: 4px;
+    height: 2em;
+    margin: 0;
+    margin-right: 4px;
 }
 </style>
diff --git a/src/components/system/Info.vue b/src/components/system/Info.vue
index 1e380b3..455c111 100644
--- a/src/components/system/Info.vue
+++ b/src/components/system/Info.vue
@@ -1,98 +1,115 @@
 <template>
-	<Modal :show="show" @close="$emit('closeDialog', 'Info')">
-		<template v-slot:header>
-			<div class="info-modal-header">
-				<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
-				<div class="info-modal-title">
-					<h2>{{ title }}</h2>
-					<h4>v{{ versionNumber}}: {{ versionTitle }}</h4>
-				</div>
-			</div>
-		</template>
-		<template v-slot:body="{ shown }">
-			<div v-if="shown">
-				<div v-if="author">
-					By {{ author }}
-				</div>
-				<div>
-					Made in TMT-X, by thepaperpilot with inspiration from Acameada, Jacorb, and Aarex
-				</div>
-				<br/>
-				<div class="link" @click="$emit('openDialog', 'Changelog')">Changelog</div>
-				<br>
-				<div>
-					<a :href="discordLink" v-if="discordLink !== 'https://discord.gg/WzejVAx'" class="info-modal-discord-link">
-						<img src="images/discord.png" class="info-modal-discord" />
-						{{ discordName }}
-					</a>
-				</div>
-				<div>
-					<a href="https://discord.gg/WzejVAx" class="info-modal-discord-link">
-						<img src="images/discord.png" class="info-modal-discord" />
-						The Paper Pilot Community
-					</a>
-				</div>
-				<div>
-					<a href="https://discord.gg/F3xveHV" class="info-modal-discord-link">
-						<img src="images/discord.png" class="info-modal-discord" />
-						The Modding Tree
-					</a>
-				</div>
-				<div>
-					<a href="https://discord.gg/wwQfgPa" class="info-modal-discord-link">
-						<img src="images/discord.png" class="info-modal-discord" />
-						Jacorb's Games
-					</a>
-				</div>
-				<br>
-				<div>Time Played: {{ timePlayed }}</div>
-				<div v-if="hotkeys">
-					<br>
-					<h4>Hotkeys</h4>
-					<div v-for="key in hotkeys" :key="key.key">
-						{{ key.key }}: {{ key.description }}
-					</div>
-				</div>
-			</div>
-		</template>
-	</Modal>
+    <Modal :show="show" @close="$emit('closeDialog', 'Info')">
+        <template v-slot:header>
+            <div class="info-modal-header">
+                <img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
+                <div class="info-modal-title">
+                    <h2>{{ title }}</h2>
+                    <h4>v{{ versionNumber }}: {{ versionTitle }}</h4>
+                </div>
+            </div>
+        </template>
+        <template v-slot:body="{ shown }">
+            <div v-if="shown">
+                <div v-if="author">By {{ author }}</div>
+                <div>
+                    Made in TMT-X, by thepaperpilot with inspiration from Acameada, Jacorb, and
+                    Aarex
+                </div>
+                <br />
+                <div class="link" @click="$emit('openDialog', 'Changelog')">
+                    Changelog
+                </div>
+                <br />
+                <div>
+                    <a
+                        :href="discordLink"
+                        v-if="discordLink !== 'https://discord.gg/WzejVAx'"
+                        class="info-modal-discord-link"
+                    >
+                        <img src="images/discord.png" class="info-modal-discord" />
+                        {{ discordName }}
+                    </a>
+                </div>
+                <div>
+                    <a href="https://discord.gg/WzejVAx" class="info-modal-discord-link">
+                        <img src="images/discord.png" class="info-modal-discord" />
+                        The Paper Pilot Community
+                    </a>
+                </div>
+                <div>
+                    <a href="https://discord.gg/F3xveHV" class="info-modal-discord-link">
+                        <img src="images/discord.png" class="info-modal-discord" />
+                        The Modding Tree
+                    </a>
+                </div>
+                <div>
+                    <a href="https://discord.gg/wwQfgPa" class="info-modal-discord-link">
+                        <img src="images/discord.png" class="info-modal-discord" />
+                        Jacorb's Games
+                    </a>
+                </div>
+                <br />
+                <div>Time Played: {{ timePlayed }}</div>
+                <div v-if="hotkeys">
+                    <br />
+                    <h4>Hotkeys</h4>
+                    <div v-for="key in hotkeys" :key="key.key">
+                        {{ key.key }}: {{ key.description }}
+                    </div>
+                </div>
+            </div>
+        </template>
+    </Modal>
 </template>
 
-<script>
-import modInfo from '../../data/modInfo.json';
-import { formatTime } from '../../util/bignum';
-import { hotkeys } from '../../game/layers';
-import player from '../../game/player';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import { hotkeys } from "@/game/layers";
+import player from "@/game/player";
+import { formatTime } from "@/util/bignum";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'Info',
-	data() {
-		const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = modInfo;
-		return { title, logo, author, discordName, discordLink, versionNumber, versionTitle };
-	},
-	props: {
-		show: Boolean
-	},
-	emits: [ 'closeDialog', 'openDialog' ],
-	computed: {
-		timePlayed() {
-			return formatTime(player.timePlayed);
-		},
-		hotkeys() {
-			return Object.keys(hotkeys).reduce((acc, curr) => {
-				if (hotkeys[curr].unlocked !== false) {
-					acc[curr] = hotkeys[curr];
-				}
-				return acc;
-			}, {});
-		}
-	}
-};
+export default defineComponent({
+    name: "Info",
+    data() {
+        const {
+            title,
+            logo,
+            author,
+            discordName,
+            discordLink,
+            versionNumber,
+            versionTitle
+        } = modInfo;
+        return {
+            title,
+            logo,
+            author,
+            discordName,
+            discordLink,
+            versionNumber,
+            versionTitle
+        };
+    },
+    props: {
+        show: Boolean
+    },
+    emits: ["closeDialog", "openDialog"],
+    computed: {
+        timePlayed() {
+            return formatTime(player.timePlayed);
+        },
+        hotkeys() {
+            return hotkeys.filter(hotkey => hotkey.unlocked);
+        }
+    }
+});
 </script>
 
 <style scoped>
 .info-modal-header {
-	display: flex;
+    display: flex;
     margin: -20px;
     margin-bottom: 0;
     background: var(--secondary-background);
@@ -100,29 +117,29 @@ export default {
 }
 
 .info-modal-header * {
-	margin: 0;
+    margin: 0;
 }
 
 .info-modal-logo {
-	height: 4em;
+    height: 4em;
     width: 4em;
 }
 
 .info-modal-title {
-	display: flex;
-	flex-direction: column;
-	padding: 10px 0;
+    display: flex;
+    flex-direction: column;
+    padding: 10px 0;
     margin-left: 10px;
 }
 
 .info-modal-discord-link {
-	display: flex;
-	align-items: center;
+    display: flex;
+    align-items: center;
 }
 
 .info-modal-discord {
-	height: 2em;
-	margin: 0;
-	margin-right: 4px;
+    height: 2em;
+    margin: 0;
+    margin-right: 4px;
 }
 </style>
diff --git a/src/components/system/LayerProvider.vue b/src/components/system/LayerProvider.vue
index d919e01..3bc6632 100644
--- a/src/components/system/LayerProvider.vue
+++ b/src/components/system/LayerProvider.vue
@@ -1,24 +1,25 @@
 <template>
-	<slot />
+    <slot />
 </template>
 
-<script>
-export default {
-	name: 'LayerProvider',
-	provide() {
-		return {
-			'tab': {
-				layer: this.layer,
-				index: this.index
-			}
-		};
-	},
-	props: {
-		layer: String,
-		index: Number
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "LayerProvider",
+    provide() {
+        return {
+            tab: {
+                layer: this.layer,
+                index: this.index
+            }
+        };
+    },
+    props: {
+        layer: String,
+        index: Number
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/system/LayerTab.vue b/src/components/system/LayerTab.vue
index 91021a6..4f37440 100644
--- a/src/components/system/LayerTab.vue
+++ b/src/components/system/LayerTab.vue
@@ -1,175 +1,206 @@
 <template>
-	<LayerProvider :layer="layer" :index="index">
-		<div class="layer-container">
-			<button v-if="index > 0 && allowGoBack && !minimized" class="goBack" @click="goBack(index)">←</button>
-			<button class="layer-tab minimized" v-if="minimized" @click="toggleMinimized"><div>{{ name }}</div></button>
-			<div class="layer-tab" :style="style" :class="{ hasSubtabs: subtabs }" v-else>
-				<branches>
-					<sticky v-if="subtabs" class="subtabs-container" :class="{ floating, firstTab: firstTab || !allowGoBack, minimizable }">
-						<div class="subtabs">
-							<tab-button v-for="(subtab, id) in subtabs" @selectTab="selectSubtab(id)" :key="id"
-								:activeTab="id === activeSubtab" :options="subtab" :text="id" />
-						</div>
-					</sticky>
-					<component v-if="display" :is="display" />
-					<default-layer-tab v-else />
-				</branches>
-			</div>
-			<button v-if="minimizable" class="minimize" @click="toggleMinimized">▼</button>
-		</div>
-	</LayerProvider>
+    <LayerProvider :layer="layer" :index="index">
+        <div class="layer-container">
+            <button
+                v-if="index > 0 && allowGoBack && !minimized"
+                class="goBack"
+                @click="goBack(index)"
+            >
+                ←
+            </button>
+            <button class="layer-tab minimized" v-if="minimized" @click="toggleMinimized">
+                <div>{{ name }}</div>
+            </button>
+            <div class="layer-tab" :style="style" :class="{ hasSubtabs: subtabs }" v-else>
+                <branches>
+                    <sticky
+                        v-if="subtabs"
+                        class="subtabs-container"
+                        :class="{
+                            floating,
+                            firstTab: firstTab || !allowGoBack,
+                            minimizable
+                        }"
+                    >
+                        <div class="subtabs">
+                            <tab-button
+                                v-for="(subtab, id) in subtabs"
+                                @selectTab="selectSubtab(id)"
+                                :key="id"
+                                :activeTab="id === activeSubtab"
+                                :options="subtab"
+                                :text="id"
+                            />
+                        </div>
+                    </sticky>
+                    <component v-if="display" :is="display" />
+                    <default-layer-tab v-else />
+                </branches>
+            </div>
+            <button v-if="minimizable" class="minimize" @click="toggleMinimized">
+                ▼
+            </button>
+        </div>
+    </LayerProvider>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
-import { isPlainObject } from '../../util/common';
-import modInfo from '../../data/modInfo.json';
-import themes from '../../data/themes';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import themes from "@/data/themes";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Subtab } from "@/typings/features/subtab";
+import { coerceComponent } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'layer-tab',
-	props: {
-		layer: String,
-		index: Number,
-		forceFirstTab: Boolean,
-		minimizable: Boolean,
-		tab: Function
-	},
-	data() {
-		return { allowGoBack: modInfo.allowGoBack };
-	},
-	computed: {
-		minimized() {
-			return this.minimizable && player.minimized[this.layer];
-		},
-		name() {
-			return layers[this.layer].name;
-		},
-		floating() {
-			return themes[player.theme].floatingTabs;
-		},
-		style() {
-			const style = [];
-			if (layers[this.layer].style) {
-				style.push(layers[this.layer].style);
-			}
-			if (layers[this.layer].activeSubtab?.style) {
-				style.push(layers[this.layer].activeSubtab.style);
-			}
-			return style;
-		},
-		display() {
-			if (layers[this.layer].activeSubtab?.display) {
-				return coerceComponent(layers[this.layer].activeSubtab.display);
-			}
-			if (layers[this.layer].display) {
-				return coerceComponent(layers[this.layer].display);
-			}
-			return null;
-		},
-		subtabs() {
-			if (layers[this.layer].subtabs) {
-				return Object.entries(layers[this.layer].subtabs)
-					.filter(subtab => isPlainObject(subtab[1]) && subtab[1].unlocked !== false)
-					.reduce((acc, curr) => {
-						acc[curr[0]] = curr[1];
-						return acc;
-					}, {});
-			}
-			return null;
-		},
-		activeSubtab() {
-			return layers[this.layer].activeSubtab?.id;
-		},
-		firstTab() {
-			if (this.forceFirstTab != undefined) {
-				return this.forceFirstTab;
-			}
-			return this.index === 0;
-		}
-	},
-	watch: {
-		minimized(newValue) {
-			if (this.tab == undefined) {
-				return;
-			}
-			const tab = this.tab();
-			if (tab != undefined) {
-				if (newValue) {
-					tab.style.flexGrow = 0;
-					tab.style.flexShrink = 0;
-					tab.style.width = "60px";
-					tab.style.minWidth = tab.style.flexBasis = null;
-					tab.style.margin = 0;
-				} else {
-					tab.style.flexGrow = null;
-					tab.style.flexShrink = null;
-					tab.style.width = null;
-					tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
-					tab.style.margin = null;
-				}
-			}
-		}
-	},
-	mounted() {
-		if (this.tab == undefined) {
-			return;
-		}
-		const tab = this.tab();
-		if (tab != undefined) {
-			if (this.minimized) {
-				tab.style.flexGrow = 0;
-				tab.style.flexShrink = 0;
-				tab.style.width = "60px";
-				tab.style.minWidth = tab.style.flexBasis = null;
-				tab.style.margin = 0;
-			} else {
-				tab.style.flexGrow = null;
-				tab.style.flexShrink = null;
-				tab.style.width = null;
-				tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
-				tab.style.margin = null;
-			}
-		} else {
-			this.$nextTick(this.mounted);
-		}
-	},
-	methods: {
-		selectSubtab(subtab) {
-			player.subtabs[this.layer].mainTabs = subtab;
-		},
-		toggleMinimized() {
-			player.minimized[this.layer] = !player.minimized[this.layer];
-		},
-		goBack(index) {
-			player.tabs = player.tabs.slice(0, index);
-		}
-	}
-};
+export default defineComponent({
+    name: "layer-tab",
+    props: {
+        layer: {
+            type: String,
+            required: true
+        },
+        index: Number,
+        forceFirstTab: Boolean,
+        minimizable: Boolean,
+        tab: Function
+    },
+    data() {
+        return { allowGoBack: modInfo.allowGoBack };
+    },
+    computed: {
+        minimized(): boolean {
+            return this.minimizable && player.minimized[this.layer];
+        },
+        name(): string {
+            return layers[this.layer].name || this.layer;
+        },
+        floating(): boolean {
+            return themes[player.theme].floatingTabs;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            const style = [];
+            if (layers[this.layer].style) {
+                style.push(layers[this.layer].style);
+            }
+            if (layers[this.layer].activeSubtab?.style) {
+                style.push(layers[this.layer].activeSubtab!.style);
+            }
+            return style;
+        },
+        display(): Component | string | null {
+            if (layers[this.layer].activeSubtab?.display) {
+                return coerceComponent(layers[this.layer].activeSubtab!.display!);
+            }
+            if (layers[this.layer].display) {
+                return coerceComponent(layers[this.layer].display!);
+            }
+            return null;
+        },
+        subtabs(): Record<string, Subtab> | null {
+            if (layers[this.layer].subtabs) {
+                return Object.entries(layers[this.layer].subtabs!)
+                    .filter(subtab => subtab[1].unlocked !== false)
+                    .reduce((acc: Record<string, Subtab>, curr: [string, Subtab]) => {
+                        acc[curr[0]] = curr[1];
+                        return acc;
+                    }, {});
+            }
+            return null;
+        },
+        activeSubtab(): string | undefined {
+            return layers[this.layer].activeSubtab?.id;
+        },
+        firstTab(): boolean {
+            if (this.forceFirstTab != undefined) {
+                return this.forceFirstTab;
+            }
+            return this.index === 0;
+        }
+    },
+    watch: {
+        minimized(newValue) {
+            if (this.tab == undefined) {
+                return;
+            }
+            const tab = this.tab();
+            if (tab != undefined) {
+                if (newValue) {
+                    tab.style.flexGrow = 0;
+                    tab.style.flexShrink = 0;
+                    tab.style.width = "60px";
+                    tab.style.minWidth = tab.style.flexBasis = null;
+                    tab.style.margin = 0;
+                } else {
+                    tab.style.flexGrow = null;
+                    tab.style.flexShrink = null;
+                    tab.style.width = null;
+                    tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
+                    tab.style.margin = null;
+                }
+            }
+        }
+    },
+    mounted() {
+        this.setup();
+    },
+    methods: {
+        setup() {
+            if (this.tab == undefined) {
+                return;
+            }
+            const tab = this.tab();
+            if (tab != undefined) {
+                if (this.minimized) {
+                    tab.style.flexGrow = 0;
+                    tab.style.flexShrink = 0;
+                    tab.style.width = "60px";
+                    tab.style.minWidth = tab.style.flexBasis = null;
+                    tab.style.margin = 0;
+                } else {
+                    tab.style.flexGrow = null;
+                    tab.style.flexShrink = null;
+                    tab.style.width = null;
+                    tab.style.minWidth = tab.style.flexBasis = `${layers[this.layer].minWidth}px`;
+                    tab.style.margin = null;
+                }
+            } else {
+                this.$nextTick(this.setup);
+            }
+        },
+        selectSubtab(subtab: string) {
+            player.subtabs[this.layer].mainTabs = subtab;
+        },
+        toggleMinimized() {
+            player.minimized[this.layer] = !player.minimized[this.layer];
+        },
+        goBack(index: number) {
+            player.tabs = player.tabs.slice(0, index);
+        }
+    }
+});
 </script>
 
 <style scoped>
 .layer-container {
-	min-width: 100%;
-	min-height: 100%;
-	margin: 0;
+    min-width: 100%;
+    min-height: 100%;
+    margin: 0;
     flex-grow: 1;
     display: flex;
 }
 
 .layer-tab:not(.minimized) {
-	padding-top: 20px;
-	padding-bottom: 20px;
-	min-height: 100%;
+    padding-top: 20px;
+    padding-bottom: 20px;
+    min-height: 100%;
     flex-grow: 1;
     text-align: center;
     position: relative;
 }
 
 .inner-tab > .layer-container > .layer-tab:not(.minimized) {
-	padding-top: 50px;
+    padding-top: 50px;
 }
 
 .layer-tab.minimized {
@@ -191,18 +222,18 @@ export default {
 
 .layer-tab.minimized div {
     margin: 0;
-	writing-mode: vertical-rl;
+    writing-mode: vertical-rl;
     padding-left: 10px;
     width: 50px;
 }
 
 .inner-tab > .layer-container > .layer-tab:not(.minimized) {
-	margin: -50px -10px;
-	padding: 50px 10px;
+    margin: -50px -10px;
+    padding: 50px 10px;
 }
 
 .layer-tab .subtabs {
-	margin-bottom: 24px;
+    margin-bottom: 24px;
     display: flex;
     flex-flow: wrap;
     padding-right: 60px;
@@ -210,38 +241,38 @@ export default {
 }
 
 .subtabs-container:not(.floating) {
-	border-top: solid 4px var(--separator);
-	border-bottom: solid 4px var(--separator);
+    border-top: solid 4px var(--separator);
+    border-bottom: solid 4px var(--separator);
 }
 
 .subtabs-container:not(.floating) .subtabs {
-	width: calc(100% + 14px);
-	margin-left: -7px;
-	margin-right: -7px;
-	box-sizing: border-box;
-	text-align: left;
-	padding-left: 14px;
-	margin-bottom: -4px;
+    width: calc(100% + 14px);
+    margin-left: -7px;
+    margin-right: -7px;
+    box-sizing: border-box;
+    text-align: left;
+    padding-left: 14px;
+    margin-bottom: -4px;
 }
 
 .subtabs-container.floating .subtabs {
-	justify-content: center;
-	margin-top: -25px;
+    justify-content: center;
+    margin-top: -25px;
 }
 
 .modal-body .layer-tab {
-	padding-bottom: 0;
+    padding-bottom: 0;
 }
 
 .modal-body .layer-tab:not(.hasSubtabs) {
-	padding-top: 0;
+    padding-top: 0;
 }
 
 .modal-body .subtabs {
-	width: 100%;
-	margin-left: 0;
-	margin-right: 0;
-	padding-left: 0;
+    width: 100%;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 0;
 }
 
 .subtabs-container:not(.floating).firstTab .subtabs {
@@ -250,7 +281,7 @@ export default {
 }
 
 .subtabs-container:not(.floating):first-child {
-	border-top: 0;
+    border-top: 0;
 }
 
 .subtabs-container.minimizable:not(.floating):first-child {
@@ -258,11 +289,11 @@ export default {
 }
 
 .subtabs-container:not(.floating):first-child .subtabs {
-	margin-top: -50px;
+    margin-top: -50px;
 }
 
 .subtabs-container:not(.floating):not(.firstTab) .subtabs {
-	padding-left: 70px;
+    padding-left: 70px;
 }
 
 .minimize {
@@ -283,7 +314,7 @@ export default {
 }
 
 .minimized + .minimize {
-	transform: rotate(-90deg);
+    transform: rotate(-90deg);
     top: 10px;
 }
 
diff --git a/src/components/system/Microtab.vue b/src/components/system/Microtab.vue
index 77f7dc0..8ad5d42 100644
--- a/src/components/system/Microtab.vue
+++ b/src/components/system/Microtab.vue
@@ -1,83 +1,104 @@
 <template>
-	<div v-if="microtabs" class="microtabs">
-		<LayerProvider :layer="layer || tab.layer" :index="tab.index">
-			<div v-if="microtabs" class="tabs" :class="{ floating }">
-				<tab-button v-for="(microtab, id) in microtabs" @selectTab="selectMicrotab(id)" :key="id"
-					:activeTab="id === activeMicrotab.id" :options="microtab" :text="id" />
-			</div>
-			<layer-tab v-if="embed" :layer="embed" />
-			<component v-else :is="display" />
-		</LayerProvider>
-	</div>
+    <div v-if="microtabs" class="microtabs">
+        <LayerProvider :layer="layer" :index="tab.index">
+            <div v-if="microtabs" class="tabs" :class="{ floating }">
+                <tab-button
+                    v-for="(microtab, id) in microtabs"
+                    @selectTab="selectMicrotab(id)"
+                    :key="id"
+                    :activeTab="id === activeMicrotab?.id"
+                    :options="microtab"
+                    :text="id"
+                />
+            </div>
+            <template v-if="activeMicrotab">
+                <layer-tab v-if="embed" :layer="embed" />
+                <component v-else :is="display" />
+            </template>
+        </LayerProvider>
+    </div>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
-import themes from '../../data/themes';
+<script lang="ts">
+import themes from "@/data/themes";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Microtab, MicrotabFamily } from "@/typings/features/subtab";
+import { coerceComponent, InjectLayerMixin } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'microtab',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		family: String,
-		id: String
-	},
-	computed: {
-		floating() {
-			return themes[player.theme].floatingTabs;
-		},
-		tabFamily() {
-			return layers[this.layer || this.tab.layer].microtabs[this.family];
-		},
-		microtabs() {
-			return Object.keys(this.tabFamily)
-				.filter(microtab =>
-					microtab !== 'activeMicrotab' && this.tabFamily[microtab].isProxy && this.tabFamily[microtab].unlocked !== false)
-				.reduce((acc, curr) => {
-					acc[curr] = this.tabFamily[curr];
-					return acc;
-				}, {});
-		},
-		activeMicrotab() {
-			return this.id != undefined ? this.tabFamily[this.id] : this.tabFamily.activeMicrotab;
-		},
-		embed() {
-			return this.activeMicrotab.embedLayer;
-		},
-		display() {
-			return coerceComponent(this.activeMicrotab.display);
-		}
-	},
-	methods: {
-		selectMicrotab(tab) {
-			player.subtabs[this.layer || this.tab.layer][this.family] = tab;
-		}
-	}
-};
+export default defineComponent({
+    name: "microtab",
+    mixins: [InjectLayerMixin],
+    props: {
+        family: {
+            type: String,
+            required: true
+        },
+        id: {
+            type: String,
+            required: true
+        }
+    },
+    computed: {
+        floating() {
+            return themes[player.theme].floatingTabs;
+        },
+        tabFamily(): MicrotabFamily {
+            return layers[this.layer].microtabs![this.family];
+        },
+        microtabs(): Record<string, Microtab> {
+            return Object.keys(this.tabFamily.data)
+                .filter(
+                    microtab =>
+                        microtab !== "activeMicrotab" &&
+                        this.tabFamily.data[microtab].isProxy &&
+                        this.tabFamily.data[microtab].unlocked !== false
+                )
+                .reduce((acc: Record<string, Microtab>, curr) => {
+                    acc[curr] = this.tabFamily.data[curr];
+                    return acc;
+                }, {});
+        },
+        activeMicrotab(): Microtab | undefined {
+            return this.id != undefined
+                ? this.tabFamily.data[this.id]
+                : this.tabFamily.activeMicrotab;
+        },
+        embed(): string | undefined {
+            return this.activeMicrotab!.embedLayer;
+        },
+        display(): Component | string | undefined {
+            return this.activeMicrotab!.display && coerceComponent(this.activeMicrotab!.display!);
+        }
+    },
+    methods: {
+        selectMicrotab(tab: string) {
+            player.subtabs[this.layer][this.family] = tab;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .microtabs {
-	margin: var(--feature-margin) -11px;
-	position: relative;
-	border: solid 4px var(--separator);
+    margin: var(--feature-margin) -11px;
+    position: relative;
+    border: solid 4px var(--separator);
 }
 
 .tabs:not(.floating) {
-	text-align: left;
-	border-bottom: inherit;
-	border-width: 4px;
-	box-sizing: border-box;
-	height: 50px;
+    text-align: left;
+    border-bottom: inherit;
+    border-width: 4px;
+    box-sizing: border-box;
+    height: 50px;
 }
 </style>
 
 <style>
 .microtabs .sticky {
-	margin-left: unset !important;
-	margin-right: unset !important;
+    margin-left: unset !important;
+    margin-right: unset !important;
 }
 </style>
diff --git a/src/components/system/Modal.vue b/src/components/system/Modal.vue
index 7f89802..7e79f76 100644
--- a/src/components/system/Modal.vue
+++ b/src/components/system/Modal.vue
@@ -1,92 +1,106 @@
 <template>
-	<teleport to="#modal-root">
-		<transition name="modal" @before-enter="setAnimating(true)" @after-leave="setAnimating(false)">
-			<div class="modal-mask" v-show="show" v-on:pointerdown.self="$emit('close')" v-bind="$attrs">
-				<div class="modal-wrapper">
-					<div class="modal-container">
-						<div class="modal-header">
-							<slot name="header" :shown="isVisible">
-								default header
-							</slot>
-						</div>
-						<div class="modal-body">
-							<branches>
-								<slot name="body" :shown="isVisible">
-									default body
-								</slot>
-							</branches>
-						</div>
-						<div class="modal-footer">
-							<slot name="footer" :shown="isVisible">
-								<div class="modal-default-footer">
-									<div class="modal-default-flex-grow"></div>
-									<button class="button modal-default-button" @click="$emit('close')">
-										Close
-									</button>
-								</div>
-							</slot>
-						</div>
-					</div>
-				</div>
-			</div>
-		</transition>
-	</teleport>
+    <teleport to="#modal-root">
+        <transition
+            name="modal"
+            @before-enter="setAnimating(true)"
+            @after-leave="setAnimating(false)"
+        >
+            <div
+                class="modal-mask"
+                v-show="show"
+                v-on:pointerdown.self="$emit('close')"
+                v-bind="$attrs"
+            >
+                <div class="modal-wrapper">
+                    <div class="modal-container">
+                        <div class="modal-header">
+                            <slot name="header" :shown="isVisible">
+                                default header
+                            </slot>
+                        </div>
+                        <div class="modal-body">
+                            <branches>
+                                <slot name="body" :shown="isVisible">
+                                    default body
+                                </slot>
+                            </branches>
+                        </div>
+                        <div class="modal-footer">
+                            <slot name="footer" :shown="isVisible">
+                                <div class="modal-default-footer">
+                                    <div class="modal-default-flex-grow"></div>
+                                    <button
+                                        class="button modal-default-button"
+                                        @click="$emit('close')"
+                                    >
+                                        Close
+                                    </button>
+                                </div>
+                            </slot>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </transition>
+    </teleport>
 </template>
 
-<script>
-export default {
-	name: 'Modal',
-	data() {
-		return {
-			isAnimating: false
-		}
-	},
-	props: {
-		show: Boolean
-	},
-	emits: [ 'close' ],
-	computed: {
-		isVisible() {
-			return this.show || this.isAnimating;
-		}
-	},
-	methods: {
-		setAnimating(isAnimating) {
-			this.isAnimating = isAnimating;
-		}
-	}
-}
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "Modal",
+    data() {
+        return {
+            isAnimating: false
+        };
+    },
+    props: {
+        show: Boolean
+    },
+    emits: ["close"],
+    computed: {
+        isVisible(): boolean {
+            return this.show || this.isAnimating;
+        }
+    },
+    methods: {
+        setAnimating(isAnimating: boolean) {
+            this.isAnimating = isAnimating;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .modal-mask {
-	position: fixed;
-	z-index: 9998;
-	top: 0;
-	left: 0;
-	bottom: 0;
-	right: 0;
-	background-color: rgba(0, 0, 0, 0.5);
-	transition: opacity 0.3s ease;
+    position: fixed;
+    z-index: 9998;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    transition: opacity 0.3s ease;
 }
 
 .modal-wrapper {
-	position: absolute;
-	left: 50%;
-	top: 50%;
-	transform: translate(-50%, -50%);
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
 }
 
 .modal-container {
-	width: 640px;
-	max-width: 95vw;
+    width: 640px;
+    max-width: 95vw;
     max-height: 95vh;
-	background-color: var(--background);
-	padding: 20px;
-	border-radius: 5px;
-	transition: all 0.3s ease;
-	text-align: left;
-	border: var(--modal-border);
+    background-color: var(--background);
+    padding: 20px;
+    border-radius: 5px;
+    transition: all 0.3s ease;
+    text-align: left;
+    border: var(--modal-border);
     box-sizing: border-box;
     display: flex;
     flex-direction: column;
@@ -97,7 +111,7 @@ export default {
 }
 
 .modal-body {
-	margin: 20px 0;
+    margin: 20px 0;
     width: 100%;
     overflow-y: auto;
     overflow-x: hidden;
@@ -108,24 +122,24 @@ export default {
 }
 
 .modal-default-footer {
-	display: flex;
+    display: flex;
 }
 
 .modal-default-flex-grow {
-	flex-grow: 1;
+    flex-grow: 1;
 }
 
 .modal-enter-from {
-	opacity: 0;
+    opacity: 0;
 }
 
 .modal-leave-active {
-	opacity: 0;
+    opacity: 0;
 }
 
 .modal-enter-from .modal-container,
 .modal-leave-active .modal-container {
-	-webkit-transform: scale(1.1);
-	transform: scale(1.1);
+    -webkit-transform: scale(1.1);
+    transform: scale(1.1);
 }
 </style>
diff --git a/src/components/system/NaNScreen.vue b/src/components/system/NaNScreen.vue
index e6ab1d0..fecc869 100644
--- a/src/components/system/NaNScreen.vue
+++ b/src/components/system/NaNScreen.vue
@@ -1,115 +1,137 @@
 <template>
-	<Modal :show="hasNaN" v-bind="$attrs">
-		<template v-slot:header>
-			<div class="nan-modal-header">
-				<h2>NaN value detected!</h2>
-			</div>
-		</template>
-		<template v-slot:body>
-			<div>Attempted to assign "{{ path }}" to NaN (previously {{ format(previous) }}). Auto-saving has been {{ autosave ? 'enabled' : 'disabled' }}. Check the console for more details, and consider sharing it with the developers on discord.</div>
-			<br>
-			<div>
-				<a :href="discordLink" class="nan-modal-discord-link">
-					<img src="images/discord.png" class="nan-modal-discord" />
-					{{ discordName }}
-				</a>
-			</div>
-			<br>
-			<Toggle title="Autosave" :value="autosave" @change="setAutosave" />
-			<Toggle title="Pause game" :value="paused" @change="togglePaused" />
-		</template>
-		<template v-slot:footer>
-			<div class="nan-footer">
-				<button @click="toggleSavesManager" class="button">Open Saves Manager</button>
-				<button @click="setZero" class="button">Set to 0</button>
-				<button @click="setOne" class="button">Set to 1</button>
-				<button @click="setPrev" class="button" v-if="previous && previous.neq(0) && previous.neq(1)">Set to previous</button>
-				<button @click="ignore" class="button danger">Ignore</button>
-			</div>
-		</template>
-	</Modal>
-	<SavesManager :show="showSaves" @closeDialog="toggleSavesManager" />
+    <Modal :show="hasNaN" v-bind="$attrs">
+        <template v-slot:header>
+            <div class="nan-modal-header">
+                <h2>NaN value detected!</h2>
+            </div>
+        </template>
+        <template v-slot:body>
+            <div>
+                Attempted to assign "{{ path }}" to NaN (previously {{ format(previous) }}).
+                Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console for
+                more details, and consider sharing it with the developers on discord.
+            </div>
+            <br />
+            <div>
+                <a :href="discordLink" class="nan-modal-discord-link">
+                    <img src="images/discord.png" class="nan-modal-discord" />
+                    {{ discordName }}
+                </a>
+            </div>
+            <br />
+            <Toggle title="Autosave" :value="autosave" @change="setAutosave" />
+            <Toggle title="Pause game" :value="paused" @change="togglePaused" />
+        </template>
+        <template v-slot:footer>
+            <div class="nan-footer">
+                <button @click="toggleSavesManager" class="button">
+                    Open Saves Manager
+                </button>
+                <button @click="setZero" class="button">Set to 0</button>
+                <button @click="setOne" class="button">Set to 1</button>
+                <button
+                    @click="setPrev"
+                    class="button"
+                    v-if="previous && previous.neq(0) && previous.neq(1)"
+                >
+                    Set to previous
+                </button>
+                <button @click="ignore" class="button danger">Ignore</button>
+            </div>
+        </template>
+    </Modal>
+    <SavesManager :show="showSaves" @closeDialog="toggleSavesManager" />
 </template>
 
-<script>
-import modInfo from '../../data/modInfo.json';
-import Decimal, { format } from '../../util/bignum';
-import { mapState } from '../../util/vue';
-import player from '../../game/player';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import player from "@/game/player";
+import Decimal, { format } from "@/util/bignum";
+import { mapState } from "@/util/vue";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'NaNScreen',
-	data() {
-		const { discordName, discordLink } = modInfo;
-		return { discordName, discordLink, format, showSaves: false };
-	},
-	computed: {
-		...mapState([ 'hasNaN', 'autosave' ]),
-		path() {
-			return player.NaNPath.join('.');
-		},
-		previous() {
-			return player.NaNReceiver?.[this.property];
-		},
-		paused() {
-			return player.devSpeed === 0;
-		},
-		property() {
-			return player.NaNPath.slice(-1)[0];
-		}
-	},
-	methods: {
-		setZero() {
-			player.NaNReceiver[this.property] = new Decimal(0);
-			player.hasNaN = false;
-		},
-		setOne() {
-			player.NaNReceiver[this.property] = new Decimal(1);
-			player.hasNaN = false;
-		},
-		setPrev() {
-			player.hasNaN = false;
-		},
-		ignore() {
-			player.NaNReceiver[this.property] = new Decimal(NaN);
-			player.hasNaN = false;
-		},
-		setAutosave(autosave) {
-			player.autosave = autosave;
-		},
-		toggleSavesManager() {
-			this.showSaves = !this.showSaves;
-		},
-		togglePaused() {
-			player.devSpeed = this.paused ? 1 : 0;
-		}
-	}
-};
+export default defineComponent({
+    name: "NaNScreen",
+    data() {
+        const { discordName, discordLink } = modInfo;
+        return { discordName, discordLink, format, showSaves: false };
+    },
+    computed: {
+        ...mapState(["hasNaN", "autosave"]),
+        path(): string | undefined {
+            return player.NaNPath?.join(".");
+        },
+        previous(): any {
+            if (player.NaNReceiver && this.property) {
+                return player.NaNReceiver[this.property];
+            }
+            return null;
+        },
+        paused() {
+            return player.devSpeed === 0;
+        },
+        property(): string | undefined {
+            return player.NaNPath?.slice(-1)[0];
+        }
+    },
+    methods: {
+        setZero() {
+            if (player.NaNReceiver && this.property) {
+                player.NaNReceiver[this.property] = new Decimal(0);
+                player.hasNaN = false;
+            }
+        },
+        setOne() {
+            if (player.NaNReceiver && this.property) {
+                player.NaNReceiver[this.property] = new Decimal(1);
+                player.hasNaN = false;
+            }
+        },
+        setPrev() {
+            player.hasNaN = false;
+        },
+        ignore() {
+            if (player.NaNReceiver && this.property) {
+                player.NaNReceiver[this.property] = new Decimal(NaN);
+                player.hasNaN = false;
+            }
+        },
+        setAutosave(autosave: boolean) {
+            player.autosave = autosave;
+        },
+        toggleSavesManager() {
+            this.showSaves = !this.showSaves;
+        },
+        togglePaused() {
+            player.devSpeed = this.paused ? 1 : 0;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .nan-modal-header {
-	padding: 10px 0;
+    padding: 10px 0;
     margin-left: 10px;
 }
 
 .nan-footer {
-	display: flex;
+    display: flex;
     justify-content: flex-end;
 }
 
 .nan-footer button {
-	margin: 0 10px;
+    margin: 0 10px;
 }
 
 .nan-modal-discord-link {
-	display: flex;
-	align-items: center;
+    display: flex;
+    align-items: center;
 }
 
 .nan-modal-discord {
-	height: 2em;
-	margin: 0;
-	margin-right: 4px;
+    height: 2em;
+    margin: 0;
+    margin-right: 4px;
 }
 </style>
diff --git a/src/components/system/Nav.vue b/src/components/system/Nav.vue
index 4a195eb..8e54b17 100644
--- a/src/components/system/Nav.vue
+++ b/src/components/system/Nav.vue
@@ -1,146 +1,174 @@
 <template>
-	<div class="nav" v-if="useHeader" v-bind="$attrs">
-		<img v-if="banner" :src="banner" height="100%" :alt="title" />
-		<div v-else class="title">{{ title }}</div>
-		<div @click="openDialog('Changelog')" class="version-container">
-			<tooltip display="Changelog" bottom class="version"><span>v{{ version }}</span></tooltip>
-		</div>
-		<div style="flex-grow: 1; cursor: unset;"></div>
-		<div class="discord">
-			<img src="images/discord.png" @click="window.open(discordLink, 'mywindow')" />
-			<ul class="discord-links">
-				<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
-					<a :href="discordLink" target="_blank">{{ discordName }}</a>
-				</li>
-				<li><a href="https://discord.gg/WzejVAx" target="_blank">The Paper Pilot Community</a></li>
-				<li><a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a></li>
-				<li><a href="http://discord.gg/wwQfgPa" target="_blank">Jacorb's Games</a></li>
-			</ul>
-		</div>
-		<div @click="openDialog('Info')">
-			<tooltip display="<span>Info</span>" bottom class="info"><span>i</span></tooltip>
-		</div>
-		<div @click="openDialog('Saves')">
-			<tooltip display="Saves" bottom class="saves" xoffset="-20px">
-				<span class="material-icons">library_books</span>
-			</tooltip>
-		</div>
-		<div @click="openDialog('Options')">
-			<tooltip display="<span>Options</span>" bottom class="options" xoffset="-70px">
-				<img src="images/options_wheel.png" />
-			</tooltip>
-		</div>
-	</div>
-	<div v-else class="overlay-nav" v-bind="$attrs">
-		<div @click="openDialog('Changelog')" class="version-container">
-			<tooltip display="Changelog" right xoffset="25%" class="version"><span>v{{ version }}</span></tooltip>
-		</div>
-		<div @click="openDialog('Saves')">
-			<tooltip display="Saves" right class="saves"><span class="material-icons">library_books</span></tooltip>
-		</div>
-		<div @click="openDialog('Options')">
-			<tooltip display="<span>Options</span>" right class="options"><img src="images/options_wheel.png" /></tooltip>
-		</div>
-		<div @click="openDialog('Info')">
-			<tooltip display="<span>Info</span>" right class="info"><span>i</span></tooltip>
-		</div>
-		<div class="discord">
-			<img src="images/discord.png" @click="openDiscord" />
-			<ul class="discord-links">
-				<li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
-					<a :href="discordLink" target="_blank">{{ discordName }}</a>
-				</li>
-				<li><a href="https://discord.gg/WzejVAx" target="_blank">The Paper Pilot Community</a></li>
-				<li><a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a></li>
-				<li><a href="http://discord.gg/wwQfgPa" target="_blank">Jacorb's Games</a></li>
-			</ul>
-		</div>
-	</div>
-	<Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
-	<SavesManager :show="showSaves" @closeDialog="closeDialog" />
-	<Options :show="showOptions" @closeDialog="closeDialog" />
+    <div class="nav" v-if="useHeader" v-bind="$attrs">
+        <img v-if="banner" :src="banner" height="100%" :alt="title" />
+        <div v-else class="title">{{ title }}</div>
+        <div @click="openDialog('Changelog')" class="version-container">
+            <tooltip display="Changelog" bottom class="version"
+                ><span>v{{ version }}</span></tooltip
+            >
+        </div>
+        <div style="flex-grow: 1; cursor: unset;"></div>
+        <div class="discord">
+            <img src="images/discord.png" @click="window.open(discordLink, 'mywindow')" />
+            <ul class="discord-links">
+                <li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
+                    <a :href="discordLink" target="_blank">{{ discordName }}</a>
+                </li>
+                <li>
+                    <a href="https://discord.gg/WzejVAx" target="_blank"
+                        >The Paper Pilot Community</a
+                    >
+                </li>
+                <li>
+                    <a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
+                </li>
+                <li>
+                    <a href="http://discord.gg/wwQfgPa" target="_blank">Jacorb's Games</a>
+                </li>
+            </ul>
+        </div>
+        <div @click="openDialog('Info')">
+            <tooltip display="<span>Info</span>" bottom class="info"><span>i</span></tooltip>
+        </div>
+        <div @click="openDialog('Saves')">
+            <tooltip display="Saves" bottom class="saves" xoffset="-20px">
+                <span class="material-icons">library_books</span>
+            </tooltip>
+        </div>
+        <div @click="openDialog('Options')">
+            <tooltip display="<span>Options</span>" bottom class="options" xoffset="-70px">
+                <img src="images/options_wheel.png" />
+            </tooltip>
+        </div>
+    </div>
+    <div v-else class="overlay-nav" v-bind="$attrs">
+        <div @click="openDialog('Changelog')" class="version-container">
+            <tooltip display="Changelog" right xoffset="25%" class="version"
+                ><span>v{{ version }}</span></tooltip
+            >
+        </div>
+        <div @click="openDialog('Saves')">
+            <tooltip display="Saves" right class="saves"
+                ><span class="material-icons">library_books</span></tooltip
+            >
+        </div>
+        <div @click="openDialog('Options')">
+            <tooltip display="<span>Options</span>" right class="options"
+                ><img src="images/options_wheel.png"
+            /></tooltip>
+        </div>
+        <div @click="openDialog('Info')">
+            <tooltip display="<span>Info</span>" right class="info"><span>i</span></tooltip>
+        </div>
+        <div class="discord">
+            <img src="images/discord.png" @click="openDiscord" />
+            <ul class="discord-links">
+                <li v-if="discordLink !== 'https://discord.gg/WzejVAx'">
+                    <a :href="discordLink" target="_blank">{{ discordName }}</a>
+                </li>
+                <li>
+                    <a href="https://discord.gg/WzejVAx" target="_blank"
+                        >The Paper Pilot Community</a
+                    >
+                </li>
+                <li>
+                    <a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
+                </li>
+                <li>
+                    <a href="http://discord.gg/wwQfgPa" target="_blank">Jacorb's Games</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+    <Info :show="showInfo" @openDialog="openDialog" @closeDialog="closeDialog" />
+    <SavesManager :show="showSaves" @closeDialog="closeDialog" />
+    <Options :show="showOptions" @closeDialog="closeDialog" />
 </template>
 
-<script>
-import modInfo from '../../data/modInfo';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'Nav',
-	data() {
-		return {
-			useHeader: modInfo.useHeader,
-			banner: modInfo.banner,
-			title: modInfo.title,
-			discordName: modInfo.discordName,
-			discordLink: modInfo.discordLink,
-			version: modInfo.versionNumber,
-			showInfo: false,
-			showSaves: false,
-			showOptions: false,
-			showChangelog: false
-		}
-	},
-	methods: {
-		openDiscord() {
-			window.open(this.discordLink, 'mywindow');
-		},
-		openDialog(dialog) {
-			this[`show${dialog}`] = true;
-		},
-		closeDialog(dialog) {
-			this[`show${dialog}`] = false;
-		}
-	}
-};
+type modals = "Info" | "Saves" | "Options" | "Changelog";
+type showModals = "showInfo" | "showSaves" | "showOptions" | "showChangelog";
+
+export default defineComponent({
+    name: "Nav",
+    data() {
+        return {
+            useHeader: modInfo.useHeader,
+            banner: modInfo.banner,
+            title: modInfo.title,
+            discordName: modInfo.discordName,
+            discordLink: modInfo.discordLink,
+            version: modInfo.versionNumber,
+            showInfo: false,
+            showSaves: false,
+            showOptions: false,
+            showChangelog: false
+        };
+    },
+    methods: {
+        openDiscord() {
+            window.open(this.discordLink, "mywindow");
+        },
+        openDialog(dialog: modals) {
+            this[`show${dialog}` as showModals] = true;
+        },
+        closeDialog(dialog: modals) {
+            this[`show${dialog}` as showModals] = false;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .nav {
-	background-color: var(--secondary-background);
-	display: flex;
-	left: 0;
-	right: 0;
-	top: 0;
-	height: 46px;
-	width: 100%;
-	border-bottom: 4px solid var(--separator);
+    background-color: var(--secondary-background);
+    display: flex;
+    left: 0;
+    right: 0;
+    top: 0;
+    height: 46px;
+    width: 100%;
+    border-bottom: 4px solid var(--separator);
 }
 
 .nav > * {
     height: 46px;
     width: 46px;
     display: flex;
-	cursor: pointer;
+    cursor: pointer;
 }
 
 .overlay-nav {
-	position: absolute;
-	top: 10px;
-	left: 10px;
-	display: flex;
-	flex-direction: column;
-	z-index: 1;
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    display: flex;
+    flex-direction: column;
+    z-index: 1;
 }
 
 .overlay-nav > * {
     height: 50px;
     width: 50px;
     display: flex;
-	cursor: pointer;
-	margin: 0;
+    cursor: pointer;
+    margin: 0;
     align-items: center;
     justify-content: center;
 }
 
 .title {
-	font-size: 36px;
-	text-align: left;
-	margin-left: 12px;
-	cursor: unset;
+    font-size: 36px;
+    text-align: left;
+    margin-left: 12px;
+    cursor: unset;
 }
 
 .nav > .title {
-	width: unset;
+    width: unset;
 }
 
 .nav .saves,
@@ -149,36 +177,36 @@ export default {
 }
 
 .tooltip-container {
-	width: 100%;
-	height: 100%;
+    width: 100%;
+    height: 100%;
     display: flex;
 }
 
 .overlay-nav .discord {
-	position: relative;
+    position: relative;
 }
 
 .discord img {
-	width: 100%;
-	height: 100%;
+    width: 100%;
+    height: 100%;
 }
 
 .discord-links {
-	position: fixed;
-	top: 45px;
-	padding: 20px;
-	right: -280px;
-	width: 200px;
-	transition: right .25s ease;
-	background: var(--secondary-background);
-	z-index: 10;
+    position: fixed;
+    top: 45px;
+    padding: 20px;
+    right: -280px;
+    width: 200px;
+    transition: right 0.25s ease;
+    background: var(--secondary-background);
+    z-index: 10;
 }
 
 .overlay-nav .discord-links {
-	position: absolute;
+    position: absolute;
     left: -280px;
     right: unset;
-    transition: left .25s ease;
+    transition: left 0.25s ease;
 }
 
 .overlay-nav .discord:hover .discord-links {
@@ -186,7 +214,7 @@ export default {
 }
 
 .discord-links li {
-	margin-bottom: 4px;
+    margin-bottom: 4px;
 }
 
 .discord-links li:first-child {
@@ -194,38 +222,36 @@ export default {
 }
 
 *:not(.overlay-nav) .discord:hover .discord-links {
-	right: 0;
+    right: 0;
 }
 
 .info {
-	font-size: 30px;
-	color: var(--link);
-	line-height: 14px;
+    font-size: 30px;
+    color: var(--link);
+    line-height: 14px;
 }
 
 .info:hover span {
-	transform: scale(1.2, 1.2);
-	text-shadow: 5px 0 10px var(--link),
-		-3px 0 12px var(--link);
+    transform: scale(1.2, 1.2);
+    text-shadow: 5px 0 10px var(--link), -3px 0 12px var(--link);
 }
 
 .saves span {
-	font-size: 36px;
+    font-size: 36px;
 }
 
 .saves:hover span {
-	transform: scale(1.2, 1.2);
-	text-shadow: 5px 0 10px var(--color),
-		-3px 0 12px var(--color);
+    transform: scale(1.2, 1.2);
+    text-shadow: 5px 0 10px var(--color), -3px 0 12px var(--color);
 }
 
 .options img {
-	width: 100%;
-	height: 100%;
+    width: 100%;
+    height: 100%;
 }
 
 .options:hover img {
-	transform: rotate(360deg);
+    transform: rotate(360deg);
 }
 
 .nav .version-container {
@@ -236,17 +262,17 @@ export default {
 }
 
 .overlay-nav .version-container {
-	width: unset;
-	height: 25px;
+    width: unset;
+    height: 25px;
 }
 
 .version {
-	color: var(--points);
+    color: var(--points);
 }
 
 .version:hover span {
-	transform-origin: 0% 50%;
-	transform: scale(1.2, 1.2);
-	text-shadow: 5px 0 10px var(--points), -3px 0 12px var(--points);
+    transform-origin: 0% 50%;
+    transform: scale(1.2, 1.2);
+    text-shadow: 5px 0 10px var(--points), -3px 0 12px var(--points);
 }
 </style>
diff --git a/src/components/system/Options.vue b/src/components/system/Options.vue
index bb8c81b..bf11ba2 100644
--- a/src/components/system/Options.vue
+++ b/src/components/system/Options.vue
@@ -1,66 +1,93 @@
 <template>
-	<Modal :show="show" @close="$emit('closeDialog', 'Options')">
-		<template v-slot:header>
-			<div class="header">
-				<h2>Options</h2>
-			</div>
-		</template>
-		<template v-slot:body>
-			<Select title="Theme" :options="themes" :value="theme" @change="setTheme" default="classic" />
-			<Select title="Show Milestones" :options="msDisplayOptions" :value="msDisplay" @change="setMSDisplay" default="all" />
-			<Toggle title="Offline Production" :value="offlineProd" @change="toggleOption('offlineProd')" />
-			<Toggle title="Autosave" :value="autosave" @change="toggleOption('autosave')" />
-			<Toggle title="Pause game" :value="paused" @change="togglePaused" />
-			<Toggle title="Show TPS" :value="showTPS" @change="toggleOption('showTPS')" />
-			<Toggle title="Hide Maxed Challenges" :value="hideChallenges" @change="toggleOption('hideChallenges')" />
-		</template>
-	</Modal>
+    <Modal :show="show" @close="$emit('closeDialog', 'Options')">
+        <template v-slot:header>
+            <div class="header">
+                <h2>Options</h2>
+            </div>
+        </template>
+        <template v-slot:body>
+            <Select
+                title="Theme"
+                :options="themes"
+                :value="theme"
+                @change="setTheme"
+                default="classic"
+            />
+            <Select
+                title="Show Milestones"
+                :options="msDisplayOptions"
+                :value="msDisplay"
+                @change="setMSDisplay"
+                default="all"
+            />
+            <Toggle
+                title="Offline Production"
+                :value="offlineProd"
+                @change="toggleOption('offlineProd')"
+            />
+            <Toggle title="Autosave" :value="autosave" @change="toggleOption('autosave')" />
+            <Toggle title="Pause game" :value="paused" @change="togglePaused" />
+            <Toggle title="Show TPS" :value="showTPS" @change="toggleOption('showTPS')" />
+            <Toggle
+                title="Hide Maxed Challenges"
+                :value="hideChallenges"
+                @change="toggleOption('hideChallenges')"
+            />
+        </template>
+    </Modal>
 </template>
 
-<script>
-import themes from '../../data/themes';
-import { camelToTitle } from '../../util/common';
-import { mapState } from '../../util/vue';
-import player from '../../game/player';
+<script lang="ts">
+import { defineComponent } from "vue";
+import themes, { Themes } from "@/data/themes";
+import { camelToTitle } from "@/util/common";
+import { mapState } from "@/util/vue";
+import player from "@/game/player";
+import { MilestoneDisplay } from "@/game/enums";
 
-export default {
-	name: 'Options',
-	props: {
-		show: Boolean
-	},
-	emits: [ 'closeDialog' ],
-	data() {
-		return {
-			themes: Object.keys(themes).map(theme => ({ label: camelToTitle(theme), value: theme })),
-			msDisplayOptions: [ "all", "last", "configurable", "incomplete", "none" ]
-				.map(option => ({ label: camelToTitle(option), value: option }))
-		}
-	},
-	computed: {
-		...mapState([ "autosave", "offlineProd", "showTPS", "hideChallenges", "theme", "msDisplay" ]),
-		paused() {
-			return player.devSpeed === 0;
-		}
-	},
-	methods: {
-		toggleOption(option) {
-			player[option] = !player[option];
-		},
-		setTheme(theme) {
-			player.theme = theme;
-		},
-		setMSDisplay(msDisplay) {
-			player.msDisplay = msDisplay;
-		},
-		togglePaused() {
-			player.devSpeed = this.paused ? 1 : 0;
-		}
-	}
-};
+export default defineComponent({
+    name: "Options",
+    props: {
+        show: Boolean
+    },
+    emits: ["closeDialog"],
+    data() {
+        return {
+            themes: Object.keys(themes).map(theme => ({
+                label: camelToTitle(theme),
+                value: theme
+            })),
+            msDisplayOptions: ["all", "last", "configurable", "incomplete", "none"].map(option => ({
+                label: camelToTitle(option),
+                value: option
+            }))
+        };
+    },
+    computed: {
+        ...mapState(["autosave", "offlineProd", "showTPS", "hideChallenges", "theme", "msDisplay"]),
+        paused() {
+            return player.devSpeed === 0;
+        }
+    },
+    methods: {
+        toggleOption(option: string) {
+            player[option] = !player[option];
+        },
+        setTheme(theme: Themes) {
+            player.theme = theme;
+        },
+        setMSDisplay(msDisplay: MilestoneDisplay) {
+            player.msDisplay = msDisplay;
+        },
+        togglePaused() {
+            player.devSpeed = this.paused ? 1 : 0;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .header {
-	margin-bottom: -10px;
+    margin-bottom: -10px;
 }
 </style>
diff --git a/src/components/system/Resource.vue b/src/components/system/Resource.vue
index b5c89d1..07efe45 100644
--- a/src/components/system/Resource.vue
+++ b/src/components/system/Resource.vue
@@ -1,18 +1,19 @@
 <template>
-	<h2 v-bind:style="{ color, 'text-shadow': '0px 0px 10px ' + color }">
-		{{ amount }}
-	</h2>
+    <h2 v-bind:style="{ color, 'text-shadow': '0px 0px 10px ' + color }">
+        {{ amount }}
+    </h2>
 </template>
 
-<script>
-export default {
-	name: 'resource',
-	props: {
-		color: String,
-		amount: String
-	}
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "resource",
+    props: {
+        color: String,
+        amount: String
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/system/Row.vue b/src/components/system/Row.vue
index 8695b2a..cebce4f 100644
--- a/src/components/system/Row.vue
+++ b/src/components/system/Row.vue
@@ -1,13 +1,15 @@
 <template>
-     <div class="table">
-          <div class="row">
-               <slot />
-          </div>
-     </div>
+    <div class="table">
+        <div class="row">
+            <slot />
+        </div>
+    </div>
 </template>
 
-<script>
-export default {
-     name: 'row'
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "row"
+});
 </script>
diff --git a/src/components/system/Save.vue b/src/components/system/Save.vue
index 478f443..cb83e25 100644
--- a/src/components/system/Save.vue
+++ b/src/components/system/Save.vue
@@ -1,102 +1,127 @@
 <template>
-		<div class="save" :class="{ active }">
-			<div class='handle material-icons'>drag_handle</div>
-			<div class="actions" v-if="!editing">
-				<feedback-button @click="$emit('export')" class="button" left v-if="save.error == undefined && !confirming">
-					<span class="material-icons">content_paste</span>
-				</feedback-button>
-				<button @click="$emit('duplicate')" class="button" v-if="save.error == undefined && !confirming">
-					<span class="material-icons">content_copy</span>
-				</button>
-				<button @click="toggleEditing" class="button" v-if="save.error == undefined && !confirming">
-					<span class="material-icons">edit</span>
-				</button>
-				<danger-button :disabled="active" @click="$emit('delete')" @confirmingChanged="confirmingChanged">
-					<span class="material-icons" style="margin: -2px">delete</span>
-				</danger-button>
-			</div>
-			<div class="actions" v-else>
-				<button @click="changeName" class="button">
-					<span class="material-icons">check</span>
-				</button>
-				<button @click="toggleEditing" class="button">
-					<span class="material-icons">close</span>
-				</button>
-			</div>
-			<div class="details" v-if="save.error == undefined && !editing">
-				<button class="button open" @click="$emit('open')">
-					<h3>{{ save.name }}</h3>
-				</button>
-				<span class="save-version">v{{ save.modVersion }}</span><br>
-				<div>Last played {{ dateFormat.format(time) }}</div>
-			</div>
-			<div class="details" v-else-if="save.error == undefined && editing">
-				<TextField v-model="newName" class="editname" @submit="changeName" />
-			</div>
-			<div v-else class="details error">
-				Error: Failed to load save with id {{ save.id }}
-			</div>
-		</div>
+    <div class="save" :class="{ active }">
+        <div class="handle material-icons">drag_handle</div>
+        <div class="actions" v-if="!editing">
+            <feedback-button
+                @click="$emit('export')"
+                class="button"
+                left
+                v-if="save.error == undefined && !confirming"
+            >
+                <span class="material-icons">content_paste</span>
+            </feedback-button>
+            <button
+                @click="$emit('duplicate')"
+                class="button"
+                v-if="save.error == undefined && !confirming"
+            >
+                <span class="material-icons">content_copy</span>
+            </button>
+            <button
+                @click="toggleEditing"
+                class="button"
+                v-if="save.error == undefined && !confirming"
+            >
+                <span class="material-icons">edit</span>
+            </button>
+            <danger-button
+                :disabled="active"
+                @click="$emit('delete')"
+                @confirmingChanged="confirmingChanged"
+            >
+                <span class="material-icons" style="margin: -2px">delete</span>
+            </danger-button>
+        </div>
+        <div class="actions" v-else>
+            <button @click="changeName" class="button">
+                <span class="material-icons">check</span>
+            </button>
+            <button @click="toggleEditing" class="button">
+                <span class="material-icons">close</span>
+            </button>
+        </div>
+        <div class="details" v-if="save.error == undefined && !editing">
+            <button class="button open" @click="$emit('open')">
+                <h3>{{ save.name }}</h3>
+            </button>
+            <span class="save-version">v{{ save.modVersion }}</span
+            ><br />
+            <div v-if="time">Last played {{ dateFormat.format(time) }}</div>
+        </div>
+        <div class="details" v-else-if="save.error == undefined && editing">
+            <TextField v-model="newName" class="editname" @submit="changeName" />
+        </div>
+        <div v-else class="details error">Error: Failed to load save with id {{ save.id }}</div>
+    </div>
 </template>
 
-<script>
-import player from '../../game/player';
+<script lang="ts">
+import player from "@/game/player";
+import { PlayerData } from "@/typings/player";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'save',
-	props: {
-		save: Object
-	},
-	emits: [ 'export', 'open', 'duplicate', 'delete', 'editSave' ],
-	data() {
-		return {
-			dateFormat: new Intl.DateTimeFormat('en-US', {
-				year: 'numeric', month: 'numeric', day: 'numeric',
-				hour: 'numeric', minute: 'numeric', second: 'numeric',
-			}),
-			editing: false,
-			confirming: false,
-			newName: ""
-		};
-	},
-	computed: {
-		active() {
-			return this.save.id === player.id;
-		},
-		time() {
-			return this.active ? player.time : this.save.time;
-		}
-	},
-	methods: {
-		confirmingChanged(confirming) {
-			this.confirming = confirming;
-		},
-		toggleEditing() {
-			this.newName = this.save.name;
-			this.editing = !this.editing;
-		},
-		changeName() {
-			this.$emit('editSave', this.newName);
-			this.editing = false;
-		}
-	}
-};
+export default defineComponent({
+    name: "save",
+    props: {
+        save: {
+            type: Object as PropType<Partial<PlayerData>>,
+            required: true
+        }
+    },
+    emits: ["export", "open", "duplicate", "delete", "editSave"],
+    data() {
+        return {
+            dateFormat: new Intl.DateTimeFormat("en-US", {
+                year: "numeric",
+                month: "numeric",
+                day: "numeric",
+                hour: "numeric",
+                minute: "numeric",
+                second: "numeric"
+            }),
+            editing: false,
+            confirming: false,
+            newName: ""
+        };
+    },
+    computed: {
+        active(): boolean {
+            return this.save.id === player.id;
+        },
+        time(): number | undefined {
+            return this.active ? player.time : this.save.time;
+        }
+    },
+    methods: {
+        confirmingChanged(confirming: boolean) {
+            this.confirming = confirming;
+        },
+        toggleEditing() {
+            this.newName = this.save.name || "";
+            this.editing = !this.editing;
+        },
+        changeName() {
+            this.$emit("editSave", this.newName);
+            this.editing = false;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .save {
-	position: relative;
-	border: solid 4px var(--separator);
-	padding: 4px;
-	background: var(--secondary-background);
-	margin: var(--feature-margin);
-	display: flex;
+    position: relative;
+    border: solid 4px var(--separator);
+    padding: 4px;
+    background: var(--secondary-background);
+    margin: var(--feature-margin);
+    display: flex;
     align-items: center;
     min-height: 30px;
 }
 
 .save.active {
-	border-color: var(--bought);
+    border-color: var(--bought);
 }
 
 .open {
@@ -106,66 +131,66 @@ export default {
 }
 
 .handle {
-	flex-grow: 0;
-	margin-right: 8px;
-	margin-left: 0;
-	cursor: pointer;
+    flex-grow: 0;
+    margin-right: 8px;
+    margin-left: 0;
+    cursor: pointer;
 }
 
 .details {
-	margin: 0;
+    margin: 0;
     flex-grow: 1;
     margin-right: 80px;
 }
 
 .error {
-    font-size: .8em;
+    font-size: 0.8em;
     color: var(--danger);
 }
 
 .save-version {
-	margin-left: 4px;
-	font-size: .7em;
-	opacity: .7;
+    margin-left: 4px;
+    font-size: 0.7em;
+    opacity: 0.7;
 }
 
 .actions {
-	position: absolute;
-	top: 0;
-	bottom: 0;
-	right: 4px;
-	display: flex;
-	padding: 4px;
-	background: inherit;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 4px;
+    display: flex;
+    padding: 4px;
+    background: inherit;
     z-index: 1;
 }
 
 .editname {
-	margin: 0;
+    margin: 0;
 }
 </style>
 
 <style>
 .save button {
-	transition-duration: 0s;
+    transition-duration: 0s;
 }
 
 .save .actions button {
-	display: flex;
-	font-size: 1.2em;
+    display: flex;
+    font-size: 1.2em;
 }
 
 .save .actions button .material-icons {
-	font-size: unset;
+    font-size: unset;
 }
 
 .save .button.danger {
-	display: flex;
-	align-items: center;
-	padding: 4px;
+    display: flex;
+    align-items: center;
+    padding: 4px;
 }
 
 .save .field {
-	margin: 0;
+    margin: 0;
 }
 </style>
diff --git a/src/components/system/SavesManager.vue b/src/components/system/SavesManager.vue
index c741fa3..bfbcb5a 100644
--- a/src/components/system/SavesManager.vue
+++ b/src/components/system/SavesManager.vue
@@ -1,204 +1,290 @@
 <template>
-	<Modal :show="show" @close="$emit('closeDialog', 'Saves')">
-		<template v-slot:header>
-			<h2>Saves Manager</h2>
-		</template>
-		<template v-slot:body v-sortable="{ update, handle: '.handle' }">
-			<save v-for="(save, index) in saves" :key="index" :save="save" @open="openSave(save.id)" @export="exportSave(save.id)"
-				@editSave="name => editSave(save.id, name)" @duplicate="duplicateSave(save.id)" @delete="deleteSave(save.id)" />
-		</template>
-		<template v-slot:footer>
-			<div class="modal-footer">
-				<TextField :value="saveToImport" @submit="importSave" @input="importSave"
-					title="Import Save" placeholder="Paste your save here!" :class="{ importingFailed }" />
-				<div class="field">
-					<span class="field-title">Create Save</span>
-					<div class="field-buttons">
-						<button class="button" @click="newSave">New Game</button>
-						<Select v-if="Object.keys(bank).length > 0" :options="bank" closeOnSelect
-							@change="newFromPreset" placeholder="Select preset" class="presets" :value="[]" />
-					</div>
-				</div>
-				<div class="footer">
-					<div style="flex-grow: 1"></div>
-					<button class="button modal-default-button" @click="$emit('closeDialog', 'Saves')">
-						Close
-					</button>
-				</div>
-			</div>
-		</template>
-	</Modal>
+    <Modal :show="show" @close="$emit('closeDialog', 'Saves')">
+        <template v-slot:header>
+            <h2>Saves Manager</h2>
+        </template>
+        <template v-slot:body v-sortable="{ update, handle: '.handle' }">
+            <save
+                v-for="(save, index) in saves"
+                :key="index"
+                :save="save"
+                @open="openSave(save.id)"
+                @export="exportSave(save.id)"
+                @editSave="name => editSave(save.id, name)"
+                @duplicate="duplicateSave(save.id)"
+                @delete="deleteSave(save.id)"
+            />
+        </template>
+        <template v-slot:footer>
+            <div class="modal-footer">
+                <TextField
+                    :value="saveToImport"
+                    @submit="importSave"
+                    @input="importSave"
+                    title="Import Save"
+                    placeholder="Paste your save here!"
+                    :class="{ importingFailed }"
+                />
+                <div class="field">
+                    <span class="field-title">Create Save</span>
+                    <div class="field-buttons">
+                        <button class="button" @click="newSave">New Game</button>
+                        <Select
+                            v-if="Object.keys(bank).length > 0"
+                            :options="bank"
+                            closeOnSelect
+                            @change="newFromPreset"
+                            placeholder="Select preset"
+                            class="presets"
+                            :value="[]"
+                        />
+                    </div>
+                </div>
+                <div class="footer">
+                    <div style="flex-grow: 1"></div>
+                    <button
+                        class="button modal-default-button"
+                        @click="$emit('closeDialog', 'Saves')"
+                    >
+                        Close
+                    </button>
+                </div>
+            </div>
+        </template>
+    </Modal>
 </template>
 
-<script>
-import { newSave, getUniqueID, loadSave, save } from '../../util/save';
-import player from '../../game/player';
-import modInfo from '../../data/modInfo.json';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import player from "@/game/player";
+import { PlayerData } from "@/typings/player";
+import { getUniqueID, loadSave, newSave, save } from "@/util/save";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'SavesManager',
-	props: {
-		show: Boolean
-	},
-	emits: [ 'closeDialog' ],
-	data() {
-		let bankContext = require.context('raw-loader!../../../saves', true, /\.txt$/);
-		let bank = bankContext.keys().reduce((acc, curr) => {
-			// .slice(2, -4) strips the leading ./ and the trailing .txt
-			acc.push({ label: curr.slice(2, -4), value: bankContext(curr).default });
-			return acc;
-		}, []);
-		return {
-			importingFailed: false,
-			saves: {}, // Gets populated when the modal is opened
-			saveToImport: "",
-			bank
-		};
-	},
-	watch: {
-		show(newValue) {
-			if (newValue) {
-				this.loadSaveData();
-			}
-		}
-	},
-	methods: {
-		loadSaveData() {
-			try {
-				const { saves } = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-				this.saves = saves.reduce((acc, curr) => {
-					try {
-						acc[curr] = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(curr)))));
-						acc[curr].id = curr;
-					} catch(error) {
-						console.warn(`Can't load save with id "${curr}"`, error);
-						acc[curr] = { error, id: curr };
-					}
-					return acc;
-				}, {});
-			} catch(e) {
-				this.saves = { [ player.id ]: player };
-				const modData = { active: player.id, saves: [ player.id ] };
-				localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-			}
-		},
-		exportSave(id) {
-			let saveToExport;
-			if (player.id === id) {
-				save();
-				saveToExport = player.saveToExport;
-			} else {
-				saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))));
-			}
+export default defineComponent({
+    name: "SavesManager",
+    props: {
+        show: Boolean
+    },
+    emits: ["closeDialog"],
+    data() {
+        let bankContext = require.context("raw-loader!../../../saves", true, /\.txt$/);
+        let bank = bankContext
+            .keys()
+            .reduce((acc: Array<{ label: string; value: string }>, curr) => {
+                // .slice(2, -4) strips the leading ./ and the trailing .txt
+                acc.push({
+                    label: curr.slice(2, -4),
+                    value: bankContext(curr).default
+                });
+                return acc;
+            }, []);
+        return {
+            importingFailed: false,
+            saves: {}, // Gets populated when the modal is opened
+            saveToImport: "",
+            bank
+        } as {
+            importingFailed: boolean;
+            saves: Record<string, Partial<PlayerData>>;
+            saveToImport: string;
+            bank: Array<{ label: string; value: string }>;
+        };
+    },
+    watch: {
+        show(newValue) {
+            if (newValue) {
+                this.loadSaveData();
+            }
+        }
+    },
+    methods: {
+        loadSaveData() {
+            try {
+                const { saves } = JSON.parse(
+                    decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+                );
+                this.saves = saves.reduce(
+                    (acc: Record<string, Partial<PlayerData>>, curr: string) => {
+                        try {
+                            acc[curr] = JSON.parse(
+                                decodeURIComponent(escape(atob(localStorage.getItem(curr)!)))
+                            );
+                            acc[curr].id = curr;
+                        } catch (error) {
+                            console.warn(`Can't load save with id "${curr}"`, error);
+                            acc[curr] = { error, id: curr };
+                        }
+                        return acc;
+                    },
+                    {}
+                );
+            } catch (e) {
+                this.saves = { [player.id]: player };
+                const modData = { active: player.id, saves: [player.id] };
+                localStorage.setItem(
+                    modInfo.id,
+                    btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+                );
+            }
+        },
+        exportSave(id: string) {
+            let saveToExport;
+            if (player.id === id) {
+                save();
+                saveToExport = player.saveToExport;
+            } else {
+                saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))));
+            }
 
-			// Put on clipboard. Using the clipboard API asks for permissions and stuff
-			const el = document.createElement("textarea");
-			el.value = saveToExport;
-			document.body.appendChild(el);
-			el.select();
-			el.setSelectionRange(0, 99999);
-			document.execCommand("copy");
-			document.body.removeChild(el);
-		},
-		duplicateSave(id) {
-			if (player.id === id) {
-				save();
-			}
+            // Put on clipboard. Using the clipboard API asks for permissions and stuff
+            const el = document.createElement("textarea");
+            el.value = saveToExport;
+            document.body.appendChild(el);
+            el.select();
+            el.setSelectionRange(0, 99999);
+            document.execCommand("copy");
+            document.body.removeChild(el);
+        },
+        duplicateSave(id: string) {
+            if (player.id === id) {
+                save();
+            }
 
-			const playerData = { ...this.saves[id], id: getUniqueID() };
-			localStorage.setItem(playerData.id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
+            const playerData = { ...this.saves[id], id: getUniqueID() };
+            localStorage.setItem(
+                playerData.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
+            );
 
-			const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-			modData.saves.push(playerData.id);
-			localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-			this.saves[playerData.id] = playerData;
-		},
-		deleteSave(id) {
-			const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-			modData.saves = modData.saves.filter(save => save !== id);
-			localStorage.removeItem(id);
-			localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-			delete this.saves[id];
-		},
-		openSave(id) {
-			this.saves[player.id].time = player.time;
-			loadSave(this.saves[id]);
-			const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-			modData.active = id;
-			localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-		},
-		async newSave() {
-			const playerData = await newSave();
-			this.saves[playerData.id] = playerData;
-		},
-		newFromPreset(preset) {
-			const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
-			playerData.id = getUniqueID();
-			localStorage.setItem(playerData.id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
+            const modData = JSON.parse(
+                decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+            );
+            modData.saves.push(playerData.id);
+            localStorage.setItem(
+                modInfo.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+            );
+            this.saves[playerData.id] = playerData;
+        },
+        deleteSave(id: string) {
+            const modData = JSON.parse(
+                decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+            );
+            modData.saves = modData.saves.filter((save: string) => save !== id);
+            localStorage.removeItem(id);
+            localStorage.setItem(
+                modInfo.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+            );
+            delete this.saves[id];
+        },
+        openSave(id: string) {
+            this.saves[player.id].time = player.time;
+            loadSave(this.saves[id]);
+            const modData = JSON.parse(
+                decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+            );
+            modData.active = id;
+            localStorage.setItem(
+                modInfo.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+            );
+        },
+        newSave() {
+            const playerData = newSave();
+            this.saves[playerData.id] = playerData;
+        },
+        newFromPreset(preset: string) {
+            const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
+            playerData.id = getUniqueID();
+            localStorage.setItem(
+                playerData.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
+            );
 
-			const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-			modData.saves.push(playerData.id);
-			localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-			this.saves[playerData.id] = playerData;
-		},
-		editSave(id, newName) {
-			this.saves[id].name = newName;
-			if (player.id === id) {
-				player.name = newName;
-				save();
-			} else {
-				localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id])))));
-			}
-		},
-		importSave(text) {
-			this.saveToImport = text;
-			if (text) {
-				this.$nextTick(() => {
-					try {
-						const playerData = JSON.parse(decodeURIComponent(escape(atob(text))));
-						const id = getUniqueID();
-						playerData.id = id;
-						localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
-						this.saves[id] = playerData;
-						this.saveToImport = "";
-						this.importingFailed = false;
+            const modData = JSON.parse(
+                decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+            );
+            modData.saves.push(playerData.id);
+            localStorage.setItem(
+                modInfo.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+            );
+            this.saves[playerData.id] = playerData;
+        },
+        editSave(id: string, newName: string) {
+            this.saves[id].name = newName;
+            if (player.id === id) {
+                player.name = newName;
+                save();
+            } else {
+                localStorage.setItem(
+                    id,
+                    btoa(unescape(encodeURIComponent(JSON.stringify(this.saves[id]))))
+                );
+            }
+        },
+        importSave(text: string) {
+            this.saveToImport = text;
+            if (text) {
+                this.$nextTick(() => {
+                    try {
+                        const playerData = JSON.parse(decodeURIComponent(escape(atob(text))));
+                        const id = getUniqueID();
+                        playerData.id = id;
+                        localStorage.setItem(
+                            id,
+                            btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
+                        );
+                        this.saves[id] = playerData;
+                        this.saveToImport = "";
+                        this.importingFailed = false;
 
-						const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-						modData.saves.push(id);
-						localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-					} catch (e) {
-						this.importingFailed = true;
-					}
-				});
-			} else {
-				this.importingFailed = false;
-			}
-		},
-		update(e) {
-			this.saves.splice(e.newIndex, 0, this.saves.splive(e.oldIndex, 1)[0]);
-
-			const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-			modData.saves.splice(e.newIndex, 0, modData.saves.splice(e.oldIndex, 1)[0]);
-			localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-		}
-	}
-};
+                        const modData = JSON.parse(
+                            decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+                        );
+                        modData.saves.push(id);
+                        localStorage.setItem(
+                            modInfo.id,
+                            btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+                        );
+                    } catch (e) {
+                        this.importingFailed = true;
+                    }
+                });
+            } else {
+                this.importingFailed = false;
+            }
+        },
+        update(e: { newIndex: number; oldIndex: number }) {
+            const modData = JSON.parse(
+                decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))
+            );
+            modData.saves.splice(e.newIndex, 0, modData.saves.splice(e.oldIndex, 1)[0]);
+            localStorage.setItem(
+                modInfo.id,
+                btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+            );
+        }
+    }
+});
 </script>
 
 <style scoped>
 .field form,
 .field .field-title,
 .field .field-buttons {
-	margin: 0;
+    margin: 0;
 }
 
 .field-buttons {
-	display: flex;
+    display: flex;
 }
 
 .field-buttons .field {
-	margin: 0;
-	margin-left: 8px;
+    margin: 0;
+    margin-left: 8px;
 }
 
 .modal-footer {
@@ -206,25 +292,21 @@ export default {
 }
 
 .footer {
-	display: flex;
-	margin-top: 20px;
+    display: flex;
+    margin-top: 20px;
 }
 </style>
 
 <style>
 .importingFailed input {
-	color: red;
+    color: red;
 }
 
 .field-buttons .v-select {
-	width: 220px;
+    width: 220px;
 }
 
-.presets .vue-dropdown {
-
-}
-
-.presets .vue-select[aria-expanded='true'] vue-dropdown {
-	visibility: hidden;
+.presets .vue-select[aria-expanded="true"] vue-dropdown {
+    visibility: hidden;
 }
 </style>
diff --git a/src/components/system/Spacer.vue b/src/components/system/Spacer.vue
index 2832ebb..fbe1e81 100644
--- a/src/components/system/Spacer.vue
+++ b/src/components/system/Spacer.vue
@@ -1,21 +1,23 @@
 <template>
-     <div :style="{ width: spacingWidth, height: spacingHeight }"></div>
+    <div :style="{ width: spacingWidth, height: spacingHeight }"></div>
 </template>
 
-<script>
-export default {
-     name: 'spacer',
-     props: {
-          width: String,
-          height: String
-     },
-     computed: {
-          spacingWidth() {
-               return this.width || '8px';
-          },
-          spacingHeight() {
-               return this.height || '17px';
-          }
-     }
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "spacer",
+    props: {
+        width: String,
+        height: String
+    },
+    computed: {
+        spacingWidth(): string {
+            return this.width || "8px";
+        },
+        spacingHeight(): string {
+            return this.height || "17px";
+        }
+    }
+});
 </script>
diff --git a/src/components/system/Sticky.vue b/src/components/system/Sticky.vue
index 7caf0b5..325a11d 100644
--- a/src/components/system/Sticky.vue
+++ b/src/components/system/Sticky.vue
@@ -1,64 +1,73 @@
 <template>
-	<div class="sticky" :style="{ top }" ref="sticky" data-v-sticky>
-		<slot />
-	</div>
+    <div class="sticky" :style="{ top }" ref="sticky" data-v-sticky>
+        <slot />
+    </div>
 </template>
 
-<script>
-export default {
-	name: 'sticky',
-	data() {
-		return {
-			top: 0,
-			observer: null
-		};
-	},
-	mounted() {
-		this.$nextTick(() => {
-			if (this.$refs.sticky == undefined) {
-				this.$nextTick(this.mounted);
-			} else {
-				this.updateTop();
-				this.observer = new ResizeObserver(this.updateTop);
-				this.observer.observe(this.$refs.sticky.parentElement);
-			}
-		});
-	},
-	methods: {
-		updateTop() {
-			let el = this.$refs.sticky;
-			if (el == undefined) {
-				return;
-			}
+<script lang="ts">
+import { defineComponent } from "vue";
 
-			let top = 0;
-			while (el.previousSibling) {
-				if (el.previousSibling.dataset && 'vSticky' in el.previousSibling.dataset) {
-					top += el.previousSibling.offsetHeight;
-				}
-				el = el.previousSibling;
-			}
-			this.top = top + "px";
-		}
-	}
-};
+export default defineComponent({
+    name: "sticky",
+    data() {
+        return {
+            top: "0",
+            observer: null
+        } as {
+            top: string;
+            observer: ResizeObserver | null;
+        };
+    },
+    mounted() {
+        this.setup();
+    },
+    methods: {
+        setup() {
+            this.$nextTick(() => {
+                if (this.$refs.sticky == undefined) {
+                    this.$nextTick(this.setup);
+                } else {
+                    this.updateTop();
+                    this.observer = new ResizeObserver(this.updateTop);
+                    this.observer.observe((this.$refs.sticky as HTMLElement).parentElement!);
+                }
+            });
+        },
+        updateTop() {
+            let el = this.$refs.sticky as HTMLElement;
+            if (el == undefined) {
+                return;
+            }
+
+            let top = 0;
+            while (el.previousSibling) {
+                const sibling = el.previousSibling as HTMLElement;
+                if (sibling.dataset && "vSticky" in sibling.dataset) {
+                    top += sibling.offsetHeight;
+                }
+                el = sibling;
+            }
+            this.top = top + "px";
+        }
+    }
+});
 </script>
 
 <style scoped>
 .sticky {
-	position: sticky;
-	background: var(--background);
-	margin-left: -7px;
-	margin-right: -7px;
-	padding-left: 7px;
-	padding-right: 7px;
-	z-index: 3;
+    position: sticky;
+    background: var(--background);
+    margin-left: -7px;
+    margin-right: -7px;
+    padding-left: 7px;
+    padding-right: 7px;
+    z-index: 3;
 }
 
 .modal-body .sticky {
-	margin-left: 0;
-	margin-right: 0;
-	padding-left: 0;
-	padding-right: 0;
+    margin-left: 0;
+    margin-right: 0;
+    padding-left: 0;
+    padding-right: 0;
 }
-</style>
\ No newline at end of file
+</style>
diff --git a/src/components/system/Subtab.vue b/src/components/system/Subtab.vue
deleted file mode 100644
index 7a9402a..0000000
--- a/src/components/system/Subtab.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-	<LayerProvider :layer="layer || tab.layer" :index="tab.index">
-		<component :is="display" />
-	</LayerProvider>
-</template>
-
-<script>
-import { layers } from '../../game/layers';
-import { coerceComponent } from '../../util/vue';
-
-export default {
-	name: 'subtab',
-	inject: [ 'tab' ],
-	props: {
-		layer: String,
-		id: String
-	},
-	computed: {
-		display() {
-			return coerceComponent(layers[this.layer || this.tab.layer].subtabs[this.id].display);
-		}
-	}
-};
-</script>
-
-<style scoped>
-</style>
diff --git a/src/components/system/TPS.vue b/src/components/system/TPS.vue
index 6f489d8..3715629 100644
--- a/src/components/system/TPS.vue
+++ b/src/components/system/TPS.vue
@@ -1,28 +1,32 @@
 <template>
-	<div class="tpsDisplay" v-if="tps !== 'NaN'">
-		TPS: {{ tps }}
-	</div>
+    <div class="tpsDisplay" v-if="tps !== 'NaN'">TPS: {{ tps }}</div>
 </template>
 
-<script>
-import Decimal, { formatWhole } from '../../util/bignum';
-import player from '../../game/player';
+<script lang="ts">
+import player from "@/game/player";
+import Decimal, { formatWhole } from "@/util/bignum";
+import { defineComponent } from "vue";
 
-export default {
-	name: 'TPS',
-	computed: {
-		tps() {
-			return formatWhole(Decimal.div(player.lastTenTicks.length, player.lastTenTicks.reduce((acc, curr) => acc + curr, 0)))
-		}
-	}
-};
+export default defineComponent({
+    name: "TPS",
+    computed: {
+        tps() {
+            return formatWhole(
+                Decimal.div(
+                    player.lastTenTicks.length,
+                    player.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
+                )
+            );
+        }
+    }
+});
 </script>
 
 <style scoped>
 .tpsDisplay {
-	position: absolute;
-	left: 10px;
-	bottom: 10px;
-	z-index: 100;
+    position: absolute;
+    left: 10px;
+    bottom: 10px;
+    z-index: 100;
 }
 </style>
diff --git a/src/components/system/TabButton.vue b/src/components/system/TabButton.vue
index 04ce714..522ac27 100644
--- a/src/components/system/TabButton.vue
+++ b/src/components/system/TabButton.vue
@@ -1,50 +1,70 @@
 <template>
-	<button @click="$emit('selectTab')" class="tabButton" :style="style"
-		:class="{ notify: options.notify, resetNotify: options.resetNotify, floating, activeTab }">
-		{{ text }}
-	</button>
+    <button
+        @click="$emit('selectTab')"
+        class="tabButton"
+        :style="style"
+        :class="{
+            notify: options.notify,
+            resetNotify: options.resetNotify,
+            floating,
+            activeTab
+        }"
+    >
+        {{ text }}
+    </button>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import themes from '../../data/themes';
+<script lang="ts">
+import themes from "@/data/themes";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Subtab } from "@/typings/features/subtab";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'tab-button',
-	props: {
-		layer: String,
-		text: String,
-		options: Object,
-		activeTab: Boolean
-	},
-	emits: [ 'selectTab' ],
-	inject: [ 'tab' ],
-	computed: {
-		floating() {
-			return themes[player.theme].floatingTabs;
-		},
-		style() {
-			return [
-				(this.floating || this.activeTab) && { 'border-color': layers[this.layer || this.tab.layer].color },
-				layers[this.layer || this.tab.layer].componentStyles?.['tab-button'],
-				this.options.resetNotify && this.options.glowColor &&
-					{
-						boxShadow: this.floating ?
-							`-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 8px ${this.options.glowColor}` :
-							`0px 10px 7px -10px ${this.options.glowColor}`
-					},
-				this.options.notify && this.options.glowColor &&
-					{
-						boxShadow: this.floating ?
-							`-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 20px ${this.options.glowColor}` :
-							`0px 15px 7px -10px ${this.options.glowColor}`
-					},
-				this.options.buttonStyle
-			];
-		}
-	}
-};
+export default defineComponent({
+    name: "tab-button",
+    mixins: [InjectLayerMixin],
+    props: {
+        text: String,
+        options: {
+            type: Object as PropType<Subtab>,
+            required: true
+        },
+        activeTab: Boolean
+    },
+    emits: ["selectTab"],
+    computed: {
+        floating(): boolean {
+            return themes[player.theme].floatingTabs;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.floating || this.activeTab
+                    ? {
+                          borderColor: layers[this.layer].color
+                      }
+                    : undefined,
+                layers[this.layer].componentStyles?.["tab-button"],
+                this.options.resetNotify && this.options.glowColor
+                    ? {
+                          boxShadow: this.floating
+                              ? `-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 8px ${this.options.glowColor}`
+                              : `0px 10px 7px -10px ${this.options.glowColor}`
+                      }
+                    : undefined,
+                this.options.notify && this.options.glowColor
+                    ? {
+                          boxShadow: this.floating
+                              ? `-2px -4px 4px rgba(0, 0, 0, 0) inset, 0 0 20px ${this.options.glowColor}`
+                              : `0px 15px 7px -10px ${this.options.glowColor}`
+                      }
+                    : undefined,
+                this.options.buttonStyle
+            ];
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -66,17 +86,17 @@ export default {
 }
 
 .tabButton:not(.floating) {
-	height: 50px;
-	margin: 0;
-	border-left: none;
-	border-right: none;
-	border-top: none;
-	border-bottom-width: 4px;
-	border-radius: 0;
-	transform: unset;
+    height: 50px;
+    margin: 0;
+    border-left: none;
+    border-right: none;
+    border-top: none;
+    border-bottom-width: 4px;
+    border-radius: 0;
+    transform: unset;
 }
 
 .tabButton:not(.floating):not(.activeTab) {
-	border-bottom-color: transparent;
+    border-bottom-color: transparent;
 }
 </style>
diff --git a/src/components/system/Tabs.vue b/src/components/system/Tabs.vue
index b7582e4..94fc880 100644
--- a/src/components/system/Tabs.vue
+++ b/src/components/system/Tabs.vue
@@ -1,45 +1,60 @@
 <template>
-	<div class="tabs-container">
-		<div v-for="(tab, index) in tabs" :key="index" class="tab" :ref="`tab-${index}`">
-			<Nav v-if="index === 0 && !useHeader" />
-			<div class="inner-tab">
-				<LayerProvider :layer="tab" :index="index" v-if="tab in components && components[tab]">
-					<component :is="components[tab]" />
-				</LayerProvider>
-				<layer-tab :layer="tab" :index="index" v-else-if="tab in components" :minimizable="true"
-					:tab="() => $refs[`tab-${index}`]" />
-				<component :is="tab" :index="index" v-else />
-			</div>
-			<div class="separator" v-if="index !== tabs.length - 1"></div>
-		</div>
-	</div>
+    <div class="tabs-container">
+        <div v-for="(tab, index) in tabs" :key="index" class="tab" :ref="`tab-${index}`">
+            <Nav v-if="index === 0 && !useHeader" />
+            <div class="inner-tab">
+                <LayerProvider
+                    :layer="tab"
+                    :index="index"
+                    v-if="tab in components && components[tab]"
+                >
+                    <component :is="components[tab]" />
+                </LayerProvider>
+                <layer-tab
+                    :layer="tab"
+                    :index="index"
+                    v-else-if="tab in components"
+                    :minimizable="true"
+                    :tab="() => $refs[`tab-${index}`]"
+                />
+                <component :is="tab" :index="index" v-else />
+            </div>
+            <div class="separator" v-if="index !== tabs.length - 1"></div>
+        </div>
+    </div>
 </template>
 
-<script>
-import modInfo from '../../data/modInfo.json';
-import { layers } from '../../game/layers';
-import { mapState } from '../../util/vue';
+<script lang="ts">
+import modInfo from "@/data/modInfo.json";
+import { layers } from "@/game/layers";
+import { coerceComponent, mapState } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'Tabs',
-	data() {
-		return { useHeader: modInfo.useHeader };
-	},
-	computed: {
-		...mapState([ 'tabs' ]),
-		components() {
-			return Object.keys(layers).reduce((acc, curr) => {
-				acc[curr] = layers[curr].component || false;
-				return acc;
-			}, {});
-		}
-	}
-};
+export default defineComponent({
+    name: "Tabs",
+    data() {
+        return { useHeader: modInfo.useHeader };
+    },
+    computed: {
+        ...mapState(["tabs"]),
+        components() {
+            return Object.keys(layers).reduce(
+                (acc: Record<string, Component | string | false>, curr) => {
+                    acc[curr] =
+                        (layers[curr].component && coerceComponent(layers[curr].component!)) ||
+                        false;
+                    return acc;
+                },
+                {}
+            );
+        }
+    }
+});
 </script>
 
 <style scoped>
 .tabs-container {
-	width: 100vw;
+    width: 100vw;
     flex-grow: 1;
     overflow-x: auto;
     overflow-y: hidden;
@@ -70,12 +85,12 @@ export default {
 }
 
 .separator {
-	position: absolute;
-	right: -3px;
-	top: 0;
-	bottom: 0;
-	width: 6px;
-	background: var(--separator);
+    position: absolute;
+    right: -3px;
+    top: 0;
+    bottom: 0;
+    width: 6px;
+    background: var(--separator);
     z-index: 1;
 }
 </style>
@@ -89,6 +104,6 @@ export default {
 }
 
 .tab .modal-body hr {
-	margin: 7px 0;
+    margin: 7px 0;
 }
 </style>
diff --git a/src/components/system/Tooltip.vue b/src/components/system/Tooltip.vue
index 4a46f58..f2b2ae7 100644
--- a/src/components/system/Tooltip.vue
+++ b/src/components/system/Tooltip.vue
@@ -1,66 +1,84 @@
 <template>
-	<div class="tooltip-container" :class="{ shown }" @mouseenter="setHover(true)" @mouseleave="setHover(false)">
-		<slot />
-		<transition name="fade">
-			<div v-if="shown" class="tooltip" :class="{ top, left, right, bottom }"
-				:style="{ '--xoffset': xoffset || '0px', '--yoffset': yoffset || '0px' }">
-				<component :is="tooltipDisplay" />
-			</div>
-		</transition>
-	</div>
+    <div
+        class="tooltip-container"
+        :class="{ shown }"
+        @mouseenter="setHover(true)"
+        @mouseleave="setHover(false)"
+    >
+        <slot />
+        <transition name="fade">
+            <div
+                v-if="shown"
+                class="tooltip"
+                :class="{ top, left, right, bottom }"
+                :style="{
+                    '--xoffset': xoffset || '0px',
+                    '--yoffset': yoffset || '0px'
+                }"
+            >
+                <component :is="tooltipDisplay" />
+            </div>
+        </transition>
+    </div>
 </template>
 
-<script>
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { CoercableComponent } from "@/typings/component";
+import { coerceComponent } from "@/util/vue";
+import { Component, defineComponent, PropType } from "vue";
 
-export default {
-	name: 'tooltip',
-	data() {
-		return {
-			hover: false
-		};
-	},
-	props: {
-		force: Boolean,
-		display: String,
-		top: Boolean,
-		left: Boolean,
-		right: Boolean,
-		bottom: Boolean,
-		xoffset: String,
-		yoffset: String
-	},
-	computed: {
-		tooltipDisplay() {
-			return coerceComponent(this.display);
-		},
-		shown() {
-			return this.force || this.hover;
-		}
-	},
-	provide: {
-		tab() {
-			return {};
-		}
-	},
-	methods: {
-		setHover(hover) {
-			this.hover = hover;
-		}
-	}
-};
+export default defineComponent({
+    name: "tooltip",
+    data() {
+        return {
+            hover: false
+        };
+    },
+    props: {
+        force: Boolean,
+        display: {
+            type: [String, Object] as PropType<CoercableComponent>,
+            required: true
+        },
+        top: Boolean,
+        left: Boolean,
+        right: Boolean,
+        bottom: Boolean,
+        xoffset: String,
+        yoffset: String
+    },
+    computed: {
+        tooltipDisplay(): Component | string {
+            return coerceComponent(this.display);
+        },
+        shown(): boolean {
+            return this.force || this.hover;
+        }
+    },
+    provide: {
+        tab() {
+            return {};
+        }
+    },
+    methods: {
+        setHover(hover: boolean) {
+            this.hover = hover;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .tooltip-container {
     position: relative;
-	--xoffset: 0px;
-	--yoffset: 0px;
+    --xoffset: 0px;
+    --yoffset: 0px;
 }
 
-.tooltip, .tooltip::after {
-	pointer-events: none;
-	position: absolute;
+.tooltip,
+.tooltip::after {
+    pointer-events: none;
+    position: absolute;
 }
 
 .tooltip {
@@ -76,80 +94,81 @@ export default {
     border-radius: 3px;
     background-color: var(--background-tooltip);
     color: var(--points);
-	z-index: 100 !important;
+    z-index: 100 !important;
 }
 
 .shown {
-	z-index: 10;
+    z-index: 10;
 }
 
-.fade-enter, .fade-leave-to {
+.fade-enter,
+.fade-leave-to {
     opacity: 0;
 }
 
 .tooltip::after {
-	content: " ";
-	position: absolute;
-	top: 100%;
-	bottom: 100%;
-	left: calc(50% - var(--xoffset));
-	width: 0;
-	margin-left: -5px;
-	border-width: 5px;
-	border-style: solid;
-	border-color: var(--background-tooltip) transparent transparent transparent;
+    content: " ";
+    position: absolute;
+    top: 100%;
+    bottom: 100%;
+    left: calc(50% - var(--xoffset));
+    width: 0;
+    margin-left: -5px;
+    border-width: 5px;
+    border-style: solid;
+    border-color: var(--background-tooltip) transparent transparent transparent;
 }
 
 .tooltip.left,
 .side-nodes .tooltip:not(.right):not(.bottom):not(.top) {
-	bottom: calc(50% + var(--yoffset));
-	left: unset;
-	right: calc(100% + var(--xoffset));
-	margin-bottom: unset;
-	margin-right: 5px;
+    bottom: calc(50% + var(--yoffset));
+    left: unset;
+    right: calc(100% + var(--xoffset));
+    margin-bottom: unset;
+    margin-right: 5px;
     transform: translateY(50%);
 }
 
 .tooltip.left::after,
 .side-nodes .tooltip:not(.right):not(.bottom):not(.top)::after {
-	top: calc(50% + var(--yoffset));
-	bottom: unset;
-	left: 100%;
-	right: 100%;
-	margin-left: unset;
-	margin-top: -5px;
-	border-color: transparent transparent transparent var(--background-tooltip);
+    top: calc(50% + var(--yoffset));
+    bottom: unset;
+    left: 100%;
+    right: 100%;
+    margin-left: unset;
+    margin-top: -5px;
+    border-color: transparent transparent transparent var(--background-tooltip);
 }
 
 .tooltip.right {
-	bottom: calc(50% + var(--yoffset));
-	left: calc(100% + var(--xoffset));
-	margin-bottom: unset;
-	margin-left: 5px;
+    bottom: calc(50% + var(--yoffset));
+    left: calc(100% + var(--xoffset));
+    margin-bottom: unset;
+    margin-left: 5px;
     transform: translateY(50%);
 }
 
 .tooltip.right::after {
-	top: calc(50% + var(--yoffset));
-	left: 0;
-	right: 100%;
-	margin-left: -10px;
-	margin-top: -5px;
-	border-color: transparent var(--background-tooltip) transparent transparent;
+    top: calc(50% + var(--yoffset));
+    left: 0;
+    right: 100%;
+    margin-left: -10px;
+    margin-top: -5px;
+    border-color: transparent var(--background-tooltip) transparent transparent;
 }
 
 .tooltip.bottom {
-	top: calc(100% + var(--yoffset));
-	bottom: unset;
-	left: calc(50% + var(--xoffset));
-	margin-bottom: unset;
-	margin-top: 5px;
+    top: calc(100% + var(--yoffset));
+    bottom: unset;
+    left: calc(50% + var(--xoffset));
+    margin-bottom: unset;
+    margin-top: 5px;
     transform: translateX(-50%);
 }
 
 .tooltip.bottom::after {
-	top: 0;
-	margin-top: -10px;
-	border-color: transparent transparent var(--background-tooltip) transparent;
+    top: 0;
+    margin-top: -10px;
+    border-color: transparent transparent var(--background-tooltip) transparent;
 }
 </style>
diff --git a/src/components/system/VerticalRule.vue b/src/components/system/VerticalRule.vue
index c2b732d..99377b1 100644
--- a/src/components/system/VerticalRule.vue
+++ b/src/components/system/VerticalRule.vue
@@ -1,21 +1,23 @@
 <template>
-     <div class="vr" :style="{ height }"></div>
+    <div class="vr" :style="{ height }"></div>
 </template>
 
-<script>
-export default {
-     name: 'vr',
-     props: {
-          height: String
-     }
-};
+<script lang="ts">
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "vr",
+    props: {
+        height: String
+    }
+});
 </script>
 
 <style scoped>
 .vr {
-     width: 4px;
-     background: var(--separator);
-     height: 100%;
-     margin: 0 7px;
+    width: 4px;
+    background: var(--separator);
+    height: 100%;
+    margin: 0 7px;
 }
 </style>
diff --git a/src/components/tree/BranchLine.vue b/src/components/tree/BranchLine.vue
index 9ceac3c..21fe68c 100644
--- a/src/components/tree/BranchLine.vue
+++ b/src/components/tree/BranchLine.vue
@@ -1,48 +1,66 @@
 <template>
-	<line :stroke="stroke" :stroke-width="strokeWidth" v-bind="typeof options === 'string' ? [] : options"
-		:x1="startPosition.x" :y1="startPosition.y" :x2="endPosition.x" :y2="endPosition.y" />
+    <line
+        :stroke="stroke"
+        :stroke-width="strokeWidth"
+        v-bind="typeof options === 'string' ? [] : options"
+        :x1="startPosition.x"
+        :y1="startPosition.y"
+        :x2="endPosition.x"
+        :y2="endPosition.y"
+    />
 </template>
 
-<script>
-export default {
-	name: 'branch-line',
-	props: {
-		options: [ String, Object ],
-		startNode: Object,
-		endNode: Object
-	},
-	computed: {
-		stroke() {
-			if (typeof this.options === 'string' || !('stroke' in this.options)) {
-				return 'white';
-			}
-			return this.options.stroke;
-		},
-		strokeWidth() {
-			if (typeof this.options === 'string' || !('stroke-width' in this.options)) {
-				return '15px';
-			}
-			return this.options['stroke-width'];
-		},
-		startPosition() {
-			const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
-			if (typeof this.options !== 'string' && 'startOffset' in this.options) {
-				position.x += this.options.startOffset.x || 0;
-				position.y += this.options.startOffset.y || 0;
-			}
-			return position;
-		},
-		endPosition() {
-			const position = { x: this.endNode.x || 0, y: this.endNode.y || 0 };
-			if (typeof this.options !== 'string' && 'endOffset' in this.options) {
-				position.x += this.options.endOffset.x || 0;
-				position.y += this.options.endOffset.y || 0;
-			}
-			return position;
-		}
-	}
-};
+<script lang="ts">
+import { BranchNode, BranchOptions, Position } from "@/typings/branches";
+import { defineComponent, PropType } from "vue";
+
+export default defineComponent({
+    name: "branch-line",
+    props: {
+        options: {
+            type: [String, Object] as PropType<string | BranchOptions>,
+            required: true
+        },
+        startNode: {
+            type: Object as PropType<BranchNode>,
+            required: true
+        },
+        endNode: {
+            type: Object as PropType<BranchNode>,
+            required: true
+        }
+    },
+    computed: {
+        stroke(): string {
+            if (typeof this.options === "string" || !("stroke" in this.options)) {
+                return "white";
+            }
+            return this.options.stroke!;
+        },
+        strokeWidth(): string {
+            if (typeof this.options === "string" || !("stroke-width" in this.options)) {
+                return "15px";
+            }
+            return this.options["stroke-width"]!;
+        },
+        startPosition(): Position {
+            const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
+            if (typeof this.options !== "string" && "startOffset" in this.options) {
+                position.x += this.options.startOffset?.x || 0;
+                position.y += this.options.startOffset?.y || 0;
+            }
+            return position;
+        },
+        endPosition(): Position {
+            const position = { x: this.endNode.x || 0, y: this.endNode.y || 0 };
+            if (typeof this.options !== "string" && "endOffset" in this.options) {
+                position.x += this.options.endOffset?.x || 0;
+                position.y += this.options.endOffset?.y || 0;
+            }
+            return position;
+        }
+    }
+});
 </script>
 
-<style scoped>
-</style>
+<style scoped></style>
diff --git a/src/components/tree/BranchNode.vue b/src/components/tree/BranchNode.vue
index 9318e20..f2a63e4 100644
--- a/src/components/tree/BranchNode.vue
+++ b/src/components/tree/BranchNode.vue
@@ -1,67 +1,115 @@
 <template>
-	<div class="branch"></div>
+    <div class="branch"></div>
 </template>
 
-<script>
-export default {
-	name: 'branch-node',
-	props: {
-		featureType: String,
-		id: [ Number, String ],
-		branches: Array
-	},
-	inject: [ 'registerNode', 'unregisterNode', 'registerBranch', 'unregisterBranch' ],
-	mounted() {
-		const id = `${this.featureType}@${this.id}`;
-		if (this.registerNode) {
-			this.registerNode(id, this);
-		}
-		if (this.registerBranch) {
-			this.branches?.map(this.handleBranch).forEach(branch => this.registerBranch(id, branch));
-		}
-	},
-	beforeUnmount() {
-		const id = `${this.featureType}@${this.id}`;
-		if (this.unregisterNode) {
-			this.unregisterNode(id);
-		}
-		if (this.unregisterBranch) {
-			this.branches?.map(this.handleBranch).forEach(branch => this.unregisterBranch(id, branch));
-		}
-	},
-	watch: {
-		featureType(newValue, oldValue) {
-			if (this.registerNode && this.unregisterNode) {
-				this.unregisterNode(`${oldValue}@${this.id}`);
-				this.registerNode(`${newValue}@${this.id}`, this);
-			}
-		},
-		id(newValue, oldValue) {
-			if (this.registerNode && this.unregisterNode) {
-				this.unregisterNode(`${this.featureType}@${oldValue}`);
-				this.registerNode(`${this.featureType}@${newValue}`, this);
-			}
-		},
-		branches(newValue, oldValue) {
-			if (this.registerBranch && this.unregisterBranch) {
-				const id = `${this.featureType}@${this.id}`;
-				oldValue?.map(this.handleBranch).forEach(branch => this.unregisterBranch(id, branch));
-				newValue?.map(this.handleBranch).forEach(branch => this.registerBranch(id, branch));
-			}
-		}
-	},
-	methods: {
-		handleBranch(branch) {
-			if (typeof branch === 'string') {
-				return branch.includes('@') ? branch : `${this.featureType}@${branch}`;
-			}
-			if (!branch.target?.includes('@')) {
-				return { ...branch, target: `${branch.featureType || this.featureType}@${branch.target}` };
-			}
-			return branch;
-		}
-	}
-};
+<script lang="ts">
+import { BranchOptions } from "@/typings/branches";
+import { ComponentPublicInstance, defineComponent, PropType } from "vue";
+
+// Annoying work-around for injected functions not appearing on `this`
+// Also requires those annoying 3 lines in any function that uses this
+type BranchInjectedComponent<T extends ComponentPublicInstance> = {
+    registerNode?: (id: string, component: ComponentPublicInstance) => void;
+    unregisterNode?: (id: string) => void;
+    registerBranch?: (start: string, options: string | BranchOptions) => void;
+    unregisterBranch?: (start: string, options: string | BranchOptions) => void;
+} & T;
+
+export default defineComponent({
+    name: "branch-node",
+    props: {
+        featureType: {
+            type: String,
+            required: true
+        },
+        id: {
+            type: [Number, String],
+            required: true
+        },
+        branches: Array as PropType<Array<string | BranchOptions>>
+    },
+    inject: ["registerNode", "unregisterNode", "registerBranch", "unregisterBranch"],
+    mounted() {
+        const id = `${this.featureType}@${this.id}`;
+        // eslint-disable-next-line @typescript-eslint/no-this-alias
+        const _this = this;
+        const injectedThis = this as BranchInjectedComponent<typeof _this>;
+        if (injectedThis.registerNode) {
+            injectedThis.registerNode(id, this);
+        }
+        if (injectedThis.registerBranch) {
+            this.branches
+                ?.map(this.handleBranch)
+                .forEach(branch => injectedThis.registerBranch!(id, branch));
+        }
+    },
+    beforeUnmount() {
+        const id = `${this.featureType}@${this.id}`;
+        // eslint-disable-next-line @typescript-eslint/no-this-alias
+        const _this = this;
+        const injectedThis = this as BranchInjectedComponent<typeof _this>;
+        if (injectedThis.unregisterNode) {
+            injectedThis.unregisterNode(id);
+        }
+        if (injectedThis.unregisterBranch) {
+            this.branches
+                ?.map(this.handleBranch)
+                .forEach(branch => injectedThis.unregisterBranch!(id, branch));
+        }
+    },
+    watch: {
+        featureType(newValue, oldValue) {
+            // eslint-disable-next-line @typescript-eslint/no-this-alias
+            const _this = this;
+            const injectedThis = this as BranchInjectedComponent<typeof _this>;
+            if (injectedThis.registerNode && injectedThis.unregisterNode) {
+                injectedThis.unregisterNode(`${oldValue}@${this.id}`);
+                injectedThis.registerNode(`${newValue}@${this.id}`, this);
+            }
+        },
+        id(newValue, oldValue) {
+            // eslint-disable-next-line @typescript-eslint/no-this-alias
+            const _this = this;
+            const injectedThis = this as BranchInjectedComponent<typeof _this>;
+            if (injectedThis.registerNode && injectedThis.unregisterNode) {
+                injectedThis.unregisterNode(`${this.featureType}@${oldValue}`);
+                injectedThis.registerNode(`${this.featureType}@${newValue}`, this);
+            }
+        },
+        branches(newValue, oldValue) {
+            // eslint-disable-next-line @typescript-eslint/no-this-alias
+            const _this = this;
+            const injectedThis = this as BranchInjectedComponent<typeof _this>;
+            if (injectedThis.registerBranch && injectedThis.unregisterBranch) {
+                const id = `${this.featureType}@${this.id}`;
+                oldValue
+                    ?.map(this.handleBranch)
+                    .forEach((branch: string | BranchOptions) =>
+                        injectedThis.unregisterBranch!(id, branch)
+                    );
+                newValue
+                    ?.map(this.handleBranch)
+                    .forEach((branch: string | BranchOptions) =>
+                        injectedThis.registerBranch!(id, branch)
+                    );
+            }
+        }
+    },
+    methods: {
+        handleBranch(branch: string | BranchOptions) {
+            if (typeof branch === "string") {
+                return branch.includes("@") ? branch : `${this.featureType}@${branch}`;
+            }
+            if (!branch.target?.includes("@")) {
+                return {
+                    ...branch,
+                    target: `${branch.featureType || this.featureType}@${branch.target}`
+                };
+            }
+            return branch;
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -70,8 +118,8 @@ export default {
     z-index: -10;
     top: 0;
     left: 0;
-	width: 100%;
-	height: 100%;
-	pointer-events: none;
+    width: 100%;
+    height: 100%;
+    pointer-events: none;
 }
 </style>
diff --git a/src/components/tree/Branches.vue b/src/components/tree/Branches.vue
index aff864d..9c763b6 100644
--- a/src/components/tree/Branches.vue
+++ b/src/components/tree/Branches.vue
@@ -1,95 +1,118 @@
 <template>
-	<slot />
-	<div ref="resizeListener" class="resize-listener" />
-	<svg v-bind="$attrs">
-		<branch-line v-for="(branch, index) in branches" :key="index"
-			:startNode="nodes[branch.start]" :endNode="nodes[branch.end]" :options="branch.options" />
-	</svg>
+    <slot />
+    <div ref="resizeListener" class="resize-listener" />
+    <svg v-bind="$attrs">
+        <branch-line
+            v-for="(branch, index) in branches"
+            :key="index"
+            :startNode="nodes[branch.start]"
+            :endNode="nodes[branch.end]"
+            :options="branch.options"
+        />
+    </svg>
 </template>
 
-<script>
+<script lang="ts">
+import { BranchLink, BranchNode, BranchOptions } from "@/typings/branches";
+import { ComponentPublicInstance, defineComponent } from "vue";
+
 const observerOptions = {
-	attributes: true,
-	childList: true,
-	subtree: true
+    attributes: true,
+    childList: true,
+    subtree: true
 };
 
-export default {
-	name: 'branches',
-	data() {
-		return {
-			observer: new MutationObserver(this.updateNodes),
-			resizeObserver: new ResizeObserver(this.updateNodes),
-			nodes: {},
-			links: []
-		};
-	},
-	mounted() {
-		// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
-		this.resizeObserver.observe(this.$refs.resizeListener);
-		this.updateNodes();
-	},
-	provide() {
-		return {
-			registerNode: this.registerNode,
-			unregisterNode: this.unregisterNode,
-			registerBranch: this.registerBranch,
-			unregisterBranch: this.unregisterBranch
-		};
-	},
-	computed: {
-		branches() {
-			return this.links.filter(link => link.start in this.nodes && link.end in this.nodes &&
-				this.nodes[link.start].x != undefined && this.nodes[link.start].y != undefined &&
-				this.nodes[link.end].x != undefined && this.nodes[link.end].y != undefined);
-		}
-	},
-	methods: {
-		updateNodes() {
-			if (this.$refs.resizeListener != undefined) {
-				const containerRect = this.$refs.resizeListener.getBoundingClientRect();
-				Object.keys(this.nodes).forEach(id => this.updateNode(id, containerRect));
-			}
-		},
-		updateNode(id, containerRect) {
-			const linkStartRect = this.nodes[id].element.getBoundingClientRect();
-			this.nodes[id].x = linkStartRect.x + linkStartRect.width / 2 - containerRect.x;
-			this.nodes[id].y = linkStartRect.y + linkStartRect.height / 2 - containerRect.y;
-		},
-		registerNode(id, component) {
-			const element = component.$el.parentElement;
-			this.nodes[id] = { component, element };
-			this.observer.observe(element, observerOptions);
-			this.$nextTick(() => {
-				if (this.$refs.resizeListener != undefined) {
-					this.updateNode(id, this.$refs.resizeListener.getBoundingClientRect());
-				}
-			});
-		},
-		unregisterNode(id) {
-			delete this.nodes[id];
-		},
-		registerBranch(start, options) {
-			const end = typeof options === 'string' ? options : options.target;
-			this.links.push({ start, end, options });
-		},
-		unregisterBranch(start, options) {
-			const index = this.links.findIndex(l => l.start === start && l.options === options);
-			this.links.splice(index, 1);
-		}
-	}
-};
+export default defineComponent({
+    name: "branches",
+    data() {
+        return {
+            observer: new MutationObserver(this.updateNodes as (...args: unknown[]) => void),
+            resizeObserver: new ResizeObserver(this.updateNodes as (...args: unknown[]) => void),
+            nodes: {},
+            links: []
+        } as {
+            observer: MutationObserver;
+            resizeObserver: ResizeObserver;
+            nodes: Record<string, BranchNode>;
+            links: Array<BranchLink>;
+        };
+    },
+    mounted() {
+        // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
+        this.resizeObserver.observe(this.$refs.resizeListener as HTMLElement);
+        this.updateNodes();
+    },
+    provide() {
+        return {
+            registerNode: this.registerNode,
+            unregisterNode: this.unregisterNode,
+            registerBranch: this.registerBranch,
+            unregisterBranch: this.unregisterBranch
+        };
+    },
+    computed: {
+        branches(): Array<BranchLink> {
+            return this.links.filter(
+                link =>
+                    link.start in this.nodes &&
+                    link.end in this.nodes &&
+                    this.nodes[link.start].x != undefined &&
+                    this.nodes[link.start].y != undefined &&
+                    this.nodes[link.end].x != undefined &&
+                    this.nodes[link.end].y != undefined
+            );
+        }
+    },
+    methods: {
+        updateNodes() {
+            if (this.$refs.resizeListener != undefined) {
+                const containerRect = (this.$refs
+                    .resizeListener as HTMLElement).getBoundingClientRect();
+                Object.keys(this.nodes).forEach(id => this.updateNode(id, containerRect));
+            }
+        },
+        updateNode(id: string, containerRect: DOMRect) {
+            const linkStartRect = this.nodes[id].element.getBoundingClientRect();
+            this.nodes[id].x = linkStartRect.x + linkStartRect.width / 2 - containerRect.x;
+            this.nodes[id].y = linkStartRect.y + linkStartRect.height / 2 - containerRect.y;
+        },
+        registerNode(id: string, component: ComponentPublicInstance) {
+            const element = component.$el.parentElement;
+            this.nodes[id] = { component, element };
+            this.observer.observe(element, observerOptions);
+            this.$nextTick(() => {
+                if (this.$refs.resizeListener != undefined) {
+                    this.updateNode(
+                        id,
+                        (this.$refs.resizeListener as HTMLElement).getBoundingClientRect()
+                    );
+                }
+            });
+        },
+        unregisterNode(id: string) {
+            delete this.nodes[id];
+        },
+        registerBranch(start: string, options: string | BranchOptions) {
+            const end = typeof options === "string" ? options : options.target;
+            this.links.push({ start, end: end!, options });
+        },
+        unregisterBranch(start: string, options: string | BranchOptions) {
+            const index = this.links.findIndex(l => l.start === start && l.options === options);
+            this.links.splice(index, 1);
+        }
+    }
+});
 </script>
 
 <style scoped>
 svg,
 .resize-listener {
-	position: absolute;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	z-index: -10;
-	pointer-events: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: -10;
+    pointer-events: none;
 }
 </style>
diff --git a/src/components/tree/Tree.vue b/src/components/tree/Tree.vue
index b3b0872..3a522ef 100644
--- a/src/components/tree/Tree.vue
+++ b/src/components/tree/Tree.vue
@@ -1,75 +1,102 @@
 <template>
-	<span class="row" v-for="(row, index) in rows" :key="index">
-		<tree-node v-for="(node, nodeIndex) in row" :key="nodeIndex" :id="node" @show-modal="openModal" :append="append" />
-	</span>
-	<span class="side-nodes" v-if="rows.side">
-		<tree-node v-for="(node, nodeIndex) in rows.side" :key="nodeIndex" :id="node" @show-modal="openModal" :append="append" small />
-	</span>
-	<modal :show="showModal" @close="closeModal">
-		<template v-slot:header><h2 v-if="modalHeader">{{ modalHeader }}</h2></template>
-		<template v-slot:body><layer-tab v-if="modal" :layer="modal" :index="tab.index" :forceFirstTab="true" /></template>
-	</modal>
+    <span class="row" v-for="(row, index) in rows" :key="index">
+        <tree-node
+            v-for="(node, nodeIndex) in row"
+            :key="nodeIndex"
+            :id="node"
+            @show-modal="openModal"
+            :append="append"
+        />
+    </span>
+    <span class="side-nodes" v-if="rows.side">
+        <tree-node
+            v-for="(node, nodeIndex) in rows.side"
+            :key="nodeIndex"
+            :id="node"
+            @show-modal="openModal"
+            :append="append"
+            small
+        />
+    </span>
+    <modal :show="showModal" @close="closeModal">
+        <template v-slot:header
+            ><h2 v-if="modalHeader">{{ modalHeader }}</h2></template
+        >
+        <template v-slot:body
+            ><layer-tab v-if="modal" :layer="modal" :index="tab.index" :forceFirstTab="true"
+        /></template>
+    </modal>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import { defineComponent, PropType } from "vue";
 
-export default {
-	name: 'tree',
-	data() {
-		return {
-			showModal: false,
-			modal: null
-		};
-	},
-	props: {
-		nodes: Array,
-		append: Boolean
-	},
-	inject: [ 'tab' ],
-	computed: {
-		modalHeader() {
-			if (this.modal == null) {
-				return null;
-			}
-			return layers[this.modal].name;
-		},
-		rows() {
-			if (this.nodes != undefined) {
-				return this.nodes;
-			}
-			const rows = Object.keys(layers).reduce((acc, curr) => {
-				if (!(layers[curr].displayRow in acc)) {
-					acc[layers[curr].displayRow] = [];
-				}
-				if (layers[curr].position != undefined) {
-					acc[layers[curr].displayRow][layers[curr].position] = curr;
-				} else {
-					acc[layers[curr].displayRow].push(curr);
-				}
-				return acc;
-			}, []);
-			return Object.keys(rows).reduce((acc, curr) => {
-				acc[curr] = rows[curr].filter(layer => layer);
-				return acc;
-			}, []);
-		}
-	},
-	methods: {
-		openModal(id) {
-			this.showModal = true;
-			this.modal = id;
-		},
-		closeModal() {
-			this.showModal = false;
-		}
-	}
-};
+export default defineComponent({
+    name: "tree",
+    data() {
+        return {
+            showModal: false,
+            modal: null
+        } as {
+            showModal: boolean;
+            modal: string | null;
+        };
+    },
+    props: {
+        nodes: Object as PropType<Record<string, Array<string | number>>>,
+        append: Boolean
+    },
+    inject: ["tab"],
+    computed: {
+        modalHeader(): string | null {
+            if (this.modal == null) {
+                return null;
+            }
+            return layers[this.modal].name || this.modal;
+        },
+        rows(): Record<string | number, Array<string | number>> {
+            if (this.nodes != undefined) {
+                return this.nodes;
+            }
+            const rows = Object.keys(layers).reduce(
+                (acc: Record<string | number, Array<string | number>>, curr) => {
+                    if (!(layers[curr].displayRow in acc)) {
+                        acc[layers[curr].displayRow] = [];
+                    }
+                    if (layers[curr].position != undefined) {
+                        acc[layers[curr].displayRow][layers[curr].position!] = curr;
+                    } else {
+                        acc[layers[curr].displayRow].push(curr);
+                    }
+                    return acc;
+                },
+                {}
+            );
+            return Object.keys(rows).reduce(
+                (acc: Record<string | number, Array<string | number>>, curr) => {
+                    acc[curr] = rows[curr].filter(layer => layer);
+                    return acc;
+                },
+                {}
+            );
+        }
+    },
+    methods: {
+        openModal(id: string) {
+            this.showModal = true;
+            this.modal = id;
+        },
+        closeModal() {
+            this.showModal = false;
+        }
+    }
+});
 </script>
 
 <style scoped>
 .row {
-	margin: 50px auto;
+    margin: 50px auto;
 }
 
 .side-nodes {
diff --git a/src/components/tree/TreeNode.vue b/src/components/tree/TreeNode.vue
index 5317468..db57c9c 100644
--- a/src/components/tree/TreeNode.vue
+++ b/src/components/tree/TreeNode.vue
@@ -1,112 +1,130 @@
 <template>
-	<tooltip :display="tooltip" :force="forceTooltip" :class="{
-			ghost: layer.layerShown === 'ghost',
-			treeNode: true,
-			[id]: true,
-			hidden: !layer.layerShown,
-			locked: !unlocked,
-			notify: layer.notify && unlocked,
-			resetNotify: layer.resetNotify,
-			can: unlocked,
-			small
-		}">
-		<LayerProvider :index="tab.index" :layer="id">
-			<button v-if="layer.shown" @click="clickTab" :style="style" :disabled="!unlocked">
-				<component :is="display" />
-				<branch-node :branches="layer.branches" :id="id" featureType="tree-node" />
-			</button>
-			<mark-node :mark="layer.mark" />
-		</LayerProvider>
-	</tooltip>
+    <tooltip
+        :display="tooltip"
+        :force="forceTooltip"
+        :class="{
+            ghost: layer.layerShown === 'ghost',
+            treeNode: true,
+            [id]: true,
+            hidden: !layer.layerShown,
+            locked: !unlocked,
+            notify: layer.notify && unlocked,
+            resetNotify: layer.resetNotify,
+            can: unlocked,
+            small
+        }"
+    >
+        <LayerProvider :index="tab.index" :layer="id">
+            <button v-if="layer.shown" @click="clickTab" :style="style" :disabled="!unlocked">
+                <component :is="display" />
+                <branch-node :branches="layer.branches" :id="id" featureType="tree-node" />
+            </button>
+            <mark-node :mark="layer.mark" />
+        </LayerProvider>
+    </tooltip>
 </template>
 
-<script>
-import { layers } from '../../game/layers';
-import player from '../../game/player';
-import { coerceComponent } from '../../util/vue';
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { CoercableComponent } from "@/typings/component";
+import { Layer } from "@/typings/layer";
+import { coerceComponent } from "@/util/vue";
+import { Component, defineComponent } from "vue";
 
-export default {
-	name: 'tree-node',
-	props: {
-		id: [ String, Number ],
-		small: Boolean,
-		append: Boolean
-	},
-	emits: [ 'show-modal' ],
-	inject: [ 'tab' ],
-	computed: {
-		layer() {
-			return layers[this.id];
-		},
-		unlocked() {
-			if (this.layer.canClick != undefined) {
-				return this.layer.canClick;
-			}
-			return this.layer.unlocked;
-		},
-		style() {
-			return [
-				this.unlocked ? { backgroundColor: this.layer.color } : null,
-				this.layer.notify && this.unlocked ?
-					{ boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${this.layer.trueGlowColor}` } : null,
-				this.layer.nodeStyle
-			];
-		},
-		display() {
-			if (this.layer.display != undefined) {
-				return coerceComponent(this.layer.display);
-			} else if (this.layer.image != undefined) {
-				return coerceComponent(`<img src=${this.layer.image}/>`);
-			} else {
-				return coerceComponent(this.layer.symbol);
-			}
-		},
-		forceTooltip() {
-			return player[this.id].forceTooltip;
-		},
-		tooltip() {
-			if (this.layer.canClick != undefined) {
-				if (this.layer.canClick) {
-					return this.layer.tooltip || 'I am a button!';
-				} else {
-					return this.layer.tooltipLocked || this.layer.tooltip || 'I am a button!';
-				}
-			}
-			if (player[this.id].unlocked) {
-				return this.layer.tooltip || `{{ formatWhole(player.${this.id}.points) }} {{ layers.${this.id}.resource }}`;
-			} else {
-				return this.layer.tooltipLocked ||
-					`Reach {{ formatWhole(layers.${this.id}.requires) }} {{ layers.${this.id}.baseResource }} to unlock (You have {{ formatWhole(layers.${this.id}.baseAmount) }} {{ layers.${this.id}.baseResource }})`;
-			}
-		},
-		components() {
-			return Object.keys(layers).reduce((acc, curr) => {
-				acc[curr] = layers[curr].component || false;
-				return acc;
-			}, {});
-		}
-	},
-	methods: {
-		clickTab(e) {
-			if (e.shiftKey) {
-				player[this.id].forceTooltip = !player[this.id].forceTooltip;
-			} else if (this.layer.click != undefined) {
-				this.layer.click();
-			} else if (this.layer.modal) {
-				this.$emit('show-modal', this.id);
-			} else if (this.append) {
-				if (player.tabs.includes(this.id)) {
-					const index = player.tabs.lastIndexOf(this.id);
-					player.tabs = [...player.tabs.slice(0, index), ...player.tabs.slice(index + 1)];
-				} else {
-					player.tabs = [...player.tabs, this.id];
-				}
-			} else {
-				player.tabs = [...player.tabs.slice(0, this.tab.index + 1), this.id];
-			}
-		}
-	}
-};
+export default defineComponent({
+    name: "tree-node",
+    props: {
+        id: {
+            type: [String, Number],
+            required: true
+        },
+        small: Boolean,
+        append: Boolean
+    },
+    emits: ["show-modal"],
+    inject: ["tab"],
+    computed: {
+        layer(): Layer {
+            return layers[this.id];
+        },
+        unlocked(): boolean {
+            if (this.layer.canClick != undefined) {
+                return this.layer.canClick;
+            }
+            return this.layer.unlocked;
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                this.unlocked ? { backgroundColor: this.layer.color } : undefined,
+                this.layer.notify && this.unlocked
+                    ? {
+                          boxShadow: `-4px -4px 4px rgba(0, 0, 0, 0.25) inset, 0 0 20px ${this.layer.trueGlowColor}`
+                      }
+                    : undefined,
+                this.layer.nodeStyle
+            ];
+        },
+        display(): Component | string {
+            if (this.layer.display != undefined) {
+                return coerceComponent(this.layer.display);
+            } else if (this.layer.image != undefined) {
+                return coerceComponent(`<img src=${this.layer.image}/>`);
+            } else {
+                return coerceComponent(this.layer.symbol);
+            }
+        },
+        forceTooltip(): boolean {
+            return player.layers[this.id].forceTooltip === true;
+        },
+        tooltip(): CoercableComponent {
+            if (this.layer.canClick != undefined) {
+                if (this.layer.canClick) {
+                    return this.layer.tooltip || "I am a button!";
+                } else {
+                    return this.layer.tooltipLocked || this.layer.tooltip || "I am a button!";
+                }
+            }
+            if (player.layers[this.id].unlocked) {
+                return (
+                    this.layer.tooltip ||
+                    `{{ formatWhole(player.${this.id}.points) }} {{ layers.${this.id}.resource }}`
+                );
+            } else {
+                return (
+                    this.layer.tooltipLocked ||
+                    `Reach {{ formatWhole(layers.${this.id}.requires) }} {{ layers.${this.id}.baseResource }} to unlock (You have {{ formatWhole(layers.${this.id}.baseAmount) }} {{ layers.${this.id}.baseResource }})`
+                );
+            }
+        }
+    },
+    methods: {
+        clickTab(e: MouseEvent) {
+            if (e.shiftKey) {
+                player.layers[this.id].forceTooltip = !player.layers[this.id].forceTooltip;
+            } else if (this.layer.click != undefined) {
+                this.layer.click();
+            } else if (this.layer.modal) {
+                this.$emit("show-modal", this.id);
+            } else if (this.append) {
+                if (player.tabs.includes(this.id.toString())) {
+                    const index = player.tabs.lastIndexOf(this.id.toString());
+                    player.tabs = [...player.tabs.slice(0, index), ...player.tabs.slice(index + 1)];
+                } else {
+                    player.tabs = [...player.tabs, this.id.toString()];
+                }
+            } else {
+                player.tabs = [
+                    ...player.tabs.slice(
+                        0,
+                        ((this as unknown) as { tab: { index: number } }).tab.index + 1
+                    ),
+                    this.id.toString()
+                ];
+            }
+        }
+    }
+});
 </script>
 
 <style scoped>
@@ -119,8 +137,8 @@ export default {
 }
 
 .treeNode button {
-	width: 100%;
-	height: 100%;
+    width: 100%;
+    height: 100%;
     border: 2px solid rgba(0, 0, 0, 0.125);
     border-radius: inherit;
     font-size: 40px;
@@ -140,7 +158,7 @@ export default {
 }
 
 .ghost {
-	visibility: hidden;
-	pointer-events: none;
+    visibility: hidden;
+    pointer-events: none;
 }
 </style>
diff --git a/src/data/layers/aca/a.js b/src/data/layers/aca/a.js
deleted file mode 100644
index 871bb58..0000000
--- a/src/data/layers/aca/a.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import Decimal from '../../../util/bignum';
-import player from '../../../game/player';
-
-export default {
-    id: "a",
-    startData() { return {
-        unlocked: true,
-        points: new Decimal(0),
-    }},
-    color: "yellow",
-    modal: true,
-    name: "Achievements",
-    resource: "achievement power",
-    row: "side",
-    tooltip() { // Optional, tooltip displays when the layer is locked
-        return ("Achievements")
-    },
-    achievementPopups: true,
-    achievements: {
-        11: {
-            image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
-            name: "Get me!",
-            done() {return true}, // This one is a freebie
-            goalTooltip: "How did this happen?", // Shows when achievement is not completed
-            doneTooltip: "You did it!", // Showed when the achievement is completed
-        },
-        12: {
-            name: "Impossible!",
-            done() {return false},
-            goalTooltip: "Mwahahaha!", // Shows when achievement is not completed
-            doneTooltip: "HOW????", // Showed when the achievement is completed
-            style: {'color': '#04e050'},
-        },
-        13: {
-            name: "EIEIO",
-            done() {return player.f.points.gte(1)},
-            tooltip: "Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).", // Showed when the achievement is completed
-            onComplete() {console.log("Bork bork bork!")}
-        },
-    },
-    midsection: "<grid id='test' />",
-    grids: {
-        test: {
-            maxRows: 3,
-            rows: 2,
-            cols: 2,
-            getStartData(cell) {
-                return cell
-            },
-            getUnlocked() { // Default
-                return true
-            },
-            getCanClick() {
-                return player.points.eq(10)
-            },
-            getStyle(cell, data) {
-                return {'background-color': '#'+ (data*1234%999999)}
-            },
-            click() { // Don't forget onHold
-                this.data++
-            },
-            getTitle(cell) {
-                let direction;
-                if (cell === '101') {
-                    direction = 'top';
-                } else if (cell === '102') {
-                    direction = 'bottom';
-                } else if (cell === '201') {
-                    direction = 'left';
-                } else if (cell === '202') {
-                    direction = 'right';
-                }
-                return `<tooltip display='${JSON.stringify(this.style)}' ${direction}>
-                    <h3>Gridable #${cell}</h3>
-                </tooltip>`
-            },
-            getDisplay(cell, data) {
-                return data
-            },
-        }
-    },
-}
diff --git a/src/data/layers/aca/a.ts b/src/data/layers/aca/a.ts
new file mode 100644
index 0000000..a18fdab
--- /dev/null
+++ b/src/data/layers/aca/a.ts
@@ -0,0 +1,103 @@
+/* eslint-disable */
+import player from "@/game/player";
+import { GridCell } from "@/typings/features/grid";
+import { RawLayer } from "@/typings/layer";
+import Decimal from "@/util/bignum";
+
+export default {
+  id: "a",
+  startData() {
+    return {
+      unlocked: true,
+      points: new Decimal(0)
+    };
+  },
+  color: "yellow",
+  modal: true,
+  name: "Achievements",
+  resource: "achievement power",
+  row: "side",
+  tooltip() {
+    // Optional, tooltip displays when the layer is locked
+    return "Achievements";
+  },
+  achievementPopups: true,
+  achievements: {
+    data: {
+      11: {
+        image: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
+        name: "Get me!",
+        done() {
+          return true;
+        }, // This one is a freebie
+        goalTooltip: "How did this happen?", // Shows when achievement is not completed
+        doneTooltip: "You did it!" // Showed when the achievement is completed
+      },
+      12: {
+        name: "Impossible!",
+        done() {
+          return false;
+        },
+        goalTooltip: "Mwahahaha!", // Shows when achievement is not completed
+        doneTooltip: "HOW????", // Showed when the achievement is completed
+        style: { color: "#04e050" }
+      },
+      13: {
+        name: "EIEIO",
+        done() {
+          return player.layers.f.points.gte(1);
+        },
+        tooltip:
+          "Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).", // Showed when the achievement is completed
+        onComplete() {
+          console.log("Bork bork bork!");
+        }
+      }
+    }
+  },
+  midsection: "<grid id='test' />",
+  grids: {
+    data: {
+      test: {
+        maxRows: 3,
+        rows: 2,
+        cols: 2,
+        getStartData(cell: string) {
+          return cell;
+        },
+        getUnlocked() {
+          // Default
+          return true;
+        },
+        getCanClick() {
+          return player.points.eq(10);
+        },
+        getStyle(cell) {
+          return { backgroundColor: "#" + ((Number((this[cell] as GridCell).data) * 1234) % 999999) };
+        },
+        click(cell) {
+          // Don't forget onHold
+          (this[cell] as GridCell).data = ((this[cell] as GridCell).data as number) + 1;
+        },
+        getTitle(cell) {
+          let direction;
+          if (cell === "101") {
+            direction = "top";
+          } else if (cell === "102") {
+            direction = "bottom";
+          } else if (cell === "201") {
+            direction = "left";
+          } else if (cell === "202") {
+            direction = "right";
+          }
+          return `<tooltip display='${JSON.stringify(this.style)}' ${direction}>
+                        <h3>Gridable #${cell}</h3>
+                    </tooltip>`;
+        },
+        getDisplay(cell) {
+          return (this[cell] as GridCell).data;
+        }
+      }
+    }
+  }
+} as RawLayer;
diff --git a/src/data/layers/aca/c.js b/src/data/layers/aca/c.js
deleted file mode 100644
index 97d869f..0000000
--- a/src/data/layers/aca/c.js
+++ /dev/null
@@ -1,399 +0,0 @@
-import Decimal, { format, formatWhole } from '../../../util/bignum';
-import player from '../../../game/player';
-import { layers } from '../../../game/layers';
-import { hasUpgrade, hasMilestone, getBuyableAmount, setBuyableAmount, upgradeEffect, buyableEffect, challengeCompletions } from '../../../util/features';
-import { resetLayer, resetLayerData } from '../../../util/layers';
-import { UP, RIGHT } from '../../../util/vue';
-
-const tmp = layers;
-
-export default {
-	id: "c", // This is assigned automatically, both to the layer and all upgrades, etc. Shown here so you know about it
-	name: "Candies", // This is optional, only used in a few places, If absent it just uses the layer id.
-	symbol: "C", // This appears on the layer's node. Default is the id with the first letter capitalized
-	position: 0, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order
-	startData() { return {
-		unlocked: true,
-		points: new Decimal(0),
-		best: new Decimal(0),
-		total: new Decimal(0),
-		beep: false,
-		thingy: "pointy",
-		otherThingy: 10,
-		spentOnBuyables: new Decimal(0)
-	}},
-	minWidth: 800,
-	color: "#4BDC13",
-	requires: new Decimal(10), // Can be a function that takes requirement increases into account
-	resource: "lollipops", // Name of prestige currency
-	baseResource: "points", // Name of resource prestige is based on
-	baseAmount() {return player.points}, // Get the current amount of baseResource
-	type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
-	exponent: 0.5, // Prestige currency exponent
-	base: 5, // Only needed for static layers, base of the formula (b^(x^exp))
-	roundUpCost: false, // True if the cost needs to be rounded up (use when baseResource is static?)
-
-	// For normal layers, gain beyond [softcap] points is put to the [softcapPower]th power
-	softcap: new Decimal(1e100),
-	softcapPower: new Decimal(0.5),
-	canBuyMax() {}, // Only needed for static layers with buy max
-	gainMult() { // Calculate the multiplier for main currency from bonuses
-		let mult = new Decimal(1)
-		if (hasUpgrade(this.layer, 166)) mult = mult.times(2) // These upgrades don't exist
-		if (hasUpgrade(this.layer, 120)) mult = mult.times(upgradeEffect(this.layer, 120))
-		return mult
-	},
-	gainExp() { // Calculate the exponent on main currency from bonuses
-		return new Decimal(1)
-	},
-	row: 0, // Row the layer is in on the tree (0 is the first row)
-	effect() {
-		return { // Formulas for any boosts inherent to resources in the layer. Can return a single value instead of an object if there is just one effect
-		waffleBoost: Decimal.pow(player[this.layer].points, 0.2),
-		icecreamCap: (player[this.layer].points * 10)
-	}},
-	effectDisplay() { // Optional text to describe the effects
-		let eff = this.effect;
-		const waffleBoost = eff.waffleBoost.times(buyableEffect(this.layer, 11).first)
-		return "which are boosting waffles by "+format(waffleBoost)+" and increasing the Ice Cream cap by "+format(eff.icecreamCap)
-	},
-	infoboxes:{
-		coolInfo: {
-			title: "Lore",
-			titleStyle: {'color': '#FE0000'},
-			body: "DEEP LORE!",
-			bodyStyle: {'background-color': "#0000EE"}
-		}
-	},
-	milestones: {
-		0: {requirementDisplay: "3 Lollipops",
-			done() {return player[this.layer].best.gte(3)}, // Used to determine when to give the milestone
-			effectDisplay: "Unlock the next milestone",
-		},
-		1: {requirementDisplay: "4 Lollipops",
-			unlocked() {return hasMilestone(this.layer, 0)},
-			done() {return player[this.layer].best.gte(4)},
-			effectDisplay: "You can toggle beep and boop (which do nothing)",
-			optionsDisplay: `
-				<div style="display: flex; justify-content: center">
-					<Toggle :value="player.c.beep" @change="value => player.c.beep = value" />
-					<Toggle :value="player.f.boop" @change="value => player.f.boop = value" />
-				</div>
-			`,
-			style() {
-				if(hasMilestone(this.layer, this.id)) return {
-					'background-color': '#1111DD'
-			}},
-
-			},
-	},
-	challenges: {
-
-		11: {
-			name: "Fun",
-			completionLimit: 3,
-			challengeDescription() {return "Makes the game 0% harder<br>"+challengeCompletions(this.layer, this.id) + "/" + this.completionLimit + " completions"},
-			unlocked() { return player[this.layer].best.gt(0) },
-			goalDescription: 'Have 20 points I guess',
-			canComplete() {
-				return player.points.gte(20)
-			},
-			rewardEffect() {
-				let ret = player[this.layer].points.add(1).tetrate(0.02)
-				return ret;
-			},
-			rewardDisplay() { return format(this.rewardEffect)+"x" },
-			countsAs: [12, 21], // Use this for if a challenge includes the effects of other challenges. Being in this challenge "counts as" being in these.
-			rewardDescription: "Says hi",
-			onComplete() {console.log("hiii")}, // Called when you successfully complete the challenge
-			onEnter() {console.log("So challenging")},
-			onExit() {console.log("Sweet freedom!")},
-
-		},
-	},
-	upgrades: {
-
-		11: {
-			title: "Generator of Genericness",
-			description: "Gain 1 Point every second.",
-			cost: new Decimal(1),
-			unlocked() { return player[this.layer].unlocked }, // The upgrade is only visible when this is true
-		},
-		12: {
-			description: "Point generation is faster based on your unspent Lollipops.",
-			cost: new Decimal(1),
-			unlocked() { return (hasUpgrade(this.layer, 11))},
-			effect() { // Calculate bonuses from the upgrade. Can return a single value or an object with multiple values
-				let ret = player[this.layer].points.add(1).pow(player[this.layer].upgrades.includes(24)?1.1:(player[this.layer].upgrades.includes(14)?0.75:0.5))
-				if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000")
-				return ret;
-			},
-			effectDisplay() { return format(this.effect)+"x" }, // Add formatting to the effect
-		},
-		13: {
-			unlocked() { return (hasUpgrade(this.layer, 12))},
-			onPurchase() { // This function triggers when the upgrade is purchased
-				player[this.layer].unlockOrder = 0
-			},
-			style() {
-				if (hasUpgrade(this.layer, this.id)) return {
-				'background-color': '#1111dd'
-				}
-				else if (!this.canAfford) {
-					return {
-						'background-color': '#dd1111'
-					}
-				} // Otherwise use the default
-			},
-			canAfford(){return player.points.lte(7)},
-			pay(){player.points = player.points.add(7)},
-			fullDisplay: "Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab."
-		},
-		22: {
-			title: "This upgrade doesn't exist",
-			description: "Or does it?.",
-			currencyLocation() {return player[this.layer].buyables}, // The object in player data that the currency is contained in
-			currencyDisplayName: "exhancers", // Use if using a nonstandard currency
-			currencyInternalName: 11, // Use if using a nonstandard currency
-
-			cost: new Decimal(3),
-			unlocked() { return player[this.layer].unlocked }, // The upgrade is only visible when this is true
-		},
-	},
-	buyables: {
-		showRespec: true,
-		respec() { // Optional, reset things and give back your currency. Having this function makes a respec button appear
-			player[this.layer].points = player[this.layer].points.add(player[this.layer].spentOnBuyables) // A built-in thing to keep track of this but only keeps a single value
-			this.reset();
-			resetLayer(this.layer, true) // Force a reset
-		},
-		respecText: "Respec Thingies", // Text on Respec button, optional
-		respecMessage: "Are you sure? Respeccing these doesn't accomplish much.",
-		11: {
-			title: "Exhancers", // Optional, displayed at the top in a larger font
-			cost() { // cost for buying xth buyable, can be an object if there are multiple currencies
-				let x = this.amount;
-				if (x.gte(25)) x = x.pow(2).div(25)
-				let cost = Decimal.pow(2, x.pow(1.5))
-				return cost.floor()
-			},
-			effect() { // Effects of owning x of the items, x is a decimal
-				let x = this.amount;
-				let eff = {}
-				if (x.gte(0)) eff.first = Decimal.pow(25, x.pow(1.1))
-				else eff.first = Decimal.pow(1/25, x.times(-1).pow(1.1))
-
-				if (x.gte(0)) eff.second = x.pow(0.8)
-				else eff.second = x.times(-1).pow(0.8).times(-1)
-				return eff;
-			},
-			display() { // Everything else displayed in the buyable button after the title
-				let data = tmp[this.layer].buyables[this.id]
-				return "Cost: " + format(data.cost) + " lollipops\n\
-				Amount: " + player[this.layer].buyables[this.id] + "/4\n\
-				Adds + " + format(data.effect.first) + " things and multiplies stuff by " + format(data.effect.second)
-			},
-			unlocked() { return player[this.layer].unlocked },
-			canAfford() {
-				return player[this.layer].points.gte(tmp[this.layer].buyables[this.id].cost)},
-			buy() {
-				let cost = tmp[this.layer].buyables[this.id].cost
-				player[this.layer].points = player[this.layer].points.sub(cost)
-				player[this.layer].buyables[this.id] = player[this.layer].buyables[this.id].add(1)
-				player[this.layer].spentOnBuyables = player[this.layer].spentOnBuyables.add(cost) // This is a built-in system that you can use for respeccing but it only works with a single Decimal value
-			},
-			buyMax() {}, // You'll have to handle this yourself if you want
-			style: {'height':'222px'},
-			purchaseLimit: new Decimal(4),
-			sellOne() {
-				let amount = getBuyableAmount(this.layer, this.id)
-				if (amount.lte(0)) return // Only sell one if there is at least one
-				setBuyableAmount(this.layer, this.id, amount.sub(1))
-				player[this.layer].points = player[this.layer].points.add(this.cost)
-			},
-		},
-	},
-	doReset(resettingLayer){ // Triggers when this layer is being reset, along with the layer doing the resetting. Not triggered by lower layers resetting, but is by layers on the same row.
-		if(layers[resettingLayer].row > this.row) resetLayerData(this.layer, ["points"]) // This is actually the default behavior
-	},
-	automate() {
-	}, // Do any automation inherent to this layer if appropriate
-	resetsNothing() {return false},
-	onPrestige() {
-		return
-	}, // Useful for if you gain secondary resources or have other interesting things happen to this layer when you reset it. You gain the currency after this function ends.
-
-	hotkeys: [
-		{key: "c", description: "reset for lollipops or whatever", press(){if (layers[this.layer].canReset) resetLayer(this.layer)}},
-		{key: "ctrl+c", description: "respec things", press(){layers[this.layer].buyables.respec()}, unlocked() {return hasUpgrade('c', '22')}}  ,
-	],
-	increaseUnlockOrder: [], // Array of layer names to have their order increased when this one is first unlocked
-
-	microtabs: {
-		stuff: {
-			first: {
-				display: `
-					<upgrades />
-					<div>confirmed</div>`
-			},
-			second: {
-				embedLayer: "f"
-			},
-		},
-		otherStuff: {
-			// There could be another set of microtabs here
-		}
-	},
-
-	bars: {
-		longBoi: {
-			fillStyle: {'background-color' : "#FFFFFF"},
-			baseStyle: {'background-color' : "#696969"},
-			textStyle: {'color': '#04e050'},
-
-			borderStyle() {return {}},
-			direction: RIGHT,
-			width: 300,
-			height: 30,
-			progress() {
-				return (player.points.add(1).log(10).div(10)).toNumber()
-			},
-			display() {
-				return format(player.points) + " / 1e10 points"
-			},
-			unlocked: true,
-
-		},
-		tallBoi: {
-			fillStyle: {'background-color' : "#4BEC13"},
-			baseStyle: {'background-color' : "#000000"},
-			textStyle: {'text-shadow': '0px 0px 2px #000000'},
-
-			borderStyle() {return {'border-width': "7px"}},
-			direction: UP,
-			width: 50,
-			height: 200,
-			progress() {
-				return player.points.div(100)
-			},
-			display() {
-				return formatWhole((player.points.div(1)).min(100)) + "%"
-			},
-			unlocked: true,
-
-		},
-		flatBoi: {
-			fillStyle: {'background-color' : "#FE0102"},
-			baseStyle: {'background-color' : "#222222"},
-			textStyle: {'text-shadow': '0px 0px 2px #000000'},
-
-			borderStyle() {return {}},
-			direction: UP,
-			width: 100,
-			height: 30,
-			progress() {
-				return player.c.points.div(50)
-			},
-			unlocked: true,
-
-		},
-	},
-
-	// Optional, lets you format the tab yourself by listing components. You can create your own components in v.js.
-	subtabs: {
-		"main tab": {
-			buttonStyle() {return  {'color': 'orange'}},
-			notify: true,
-			display: `
-				<main-display />
-				<sticky><prestige-button /></sticky>
-				<resource-display />
-				<spacer height="5px" />
-				<button onclick='console.log("yeet")'>'HI'</button>
-				<div>Name your points!</div>
-				<TextField :value="player.c.thingy" @input="value => player.c.thingy = value" :field="false" />
-				<sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">I have {{ format(player.points) }} {{ player.c.thingy }} points!</sticky>
-				<hr />
-				<milestones />
-				<spacer />
-				<upgrades />
-				<challenges />`,
-			glowColor: "blue",
-
-		},
-		thingies: {
-			resetNotify: true,
-			style() {return  {'background-color': '#222222', '--background': '#222222'}},
-			buttonStyle() {return {'border-color': 'orange'}},
-			display: `
-				<buyables />
-				<spacer />
-				<row style="width: 600px; height: 350px; background-color: green; border-style: solid;">
-					<Toggle :value="player.c.beep" @change="value => player.c.beep = value" />
-					<spacer width="30px" height="10px" />
-					<div>Beep</div>
-					<spacer />
-					<vr height="200px"/>
-					<column>
-						<prestige-button style="width: 150px; height: 80px" />
-						<prestige-button style="width: 100px; height: 150px" />
-					</column>
-				</row>
-				<spacer />
-				<img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" />`
-		},
-		jail: {
-			display: `
-				<infobox id="coolInfo" />
-				<bar id="longBoi" />
-				<spacer />
-				<row>
-					<column style="background-color: #555555; padding: 15px">
-						<div style="color: teal">Sugar level:</div><spacer /><bar id="tallBoi" />
-					</column>
-					<spacer />
-					<column>
-						<div>idk</div>
-						<spacer width="0" height="50px" />
-						<bar id="flatBoi" />
-					</column>
-				</row>
-				<spacer />
-				<div>It's jail because "bars"! So funny! Ha ha!</div>
-				<tree :nodes="[['f', 'c'], ['g', 'spook', 'h']]" />`
-		},
-		illuminati: {
-			unlocked() {return (hasUpgrade("c", 13))},
-			display: `
-				<h1> C O N F I R M E D </h1>
-				<spacer />
-				<microtab family="stuff" style="width: 660px; height: 370px; background-color: brown; --background: brown; border: solid white; margin: auto" />
-				<div>Adjust how many points H gives you!</div>
-				<Slider :value="player.c.otherThingy" @change="value => player.c.otherThingy = value" :min="1" :max="30" />`
-		}
-
-	},
-	style() {return {
-		//'background-color': '#3325CC'
-	}},
-	nodeStyle() {return { // Style on the layer node
-		'color': '#3325CC',
-		'text-decoration': 'underline'
-	}},
-	glowColor: "orange", // If the node is highlighted, it will be this color (default is red)
-	componentStyles: {
-		"challenge"() {return {'height': '200px'}},
-		"prestige-button"() {return {'color': '#AA66AA'}},
-	},
-	tooltip() { // Optional, tooltip displays when the layer is unlocked
-		let tooltip = "{{ formatWhole(player.c.points) }} {{ layers.c.resource }}";
-		if (player[this.layer].buyables[11].gt(0)) tooltip += "<br><i><br><br><br>{{ formatWhole(player.c.buyables[11]) }} Exhancers</i>"
-		return tooltip
-	},
-	shouldNotify() { // Optional, layer will be highlighted on the tree if true.
-					// Layer will automatically highlight if an upgrade is purchasable.
-		return (player.c.buyables[11] == 1)
-	},
-	mark: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
-	resetDescription: "Melt your points into ",
-};
diff --git a/src/data/layers/aca/c.ts b/src/data/layers/aca/c.ts
new file mode 100644
index 0000000..b291172
--- /dev/null
+++ b/src/data/layers/aca/c.ts
@@ -0,0 +1,573 @@
+/* eslint-disable */
+import { Direction } from "@/game/enums";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { DecimalSource } from "@/lib/break_eternity";
+import { RawLayer } from "@/typings/layer";
+import Decimal, { format, formatWhole } from "@/util/bignum";
+import {
+  buyableEffect,
+  challengeCompletions,
+  getBuyableAmount,
+  hasMilestone,
+  hasUpgrade,
+  setBuyableAmount,
+  upgradeEffect
+} from "@/util/features";
+import { resetLayer, resetLayerData } from "@/util/layers";
+
+export default {
+  id: "c", // This is assigned automatically, both to the layer and all upgrades, etc. Shown here so you know about it
+  name: "Candies", // This is optional, only used in a few places, If absent it just uses the layer id.
+  symbol: "C", // This appears on the layer's node. Default is the id with the first letter capitalized
+  position: 0, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order
+  startData() {
+    return {
+      unlocked: true,
+      points: new Decimal(0),
+      best: new Decimal(0),
+      total: new Decimal(0),
+      beep: false,
+      thingy: "pointy",
+      otherThingy: 10,
+      spentOnBuyables: new Decimal(0)
+    };
+  },
+  minWidth: 800,
+  color: "#4BDC13",
+  requires: new Decimal(10), // Can be a function that takes requirement increases into account
+  resource: "lollipops", // Name of prestige currency
+  baseResource: "points", // Name of resource prestige is based on
+  baseAmount() {
+    return player.points;
+  }, // Get the current amount of baseResource
+  type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
+  exponent: 0.5, // Prestige currency exponent
+  base: 5, // Only needed for static layers, base of the formula (b^(x^exp))
+  roundUpCost: false, // True if the cost needs to be rounded up (use when baseResource is static?)
+
+  // For normal layers, gain beyond [softcap] points is put to the [softcapPower]th power
+  softcap: new Decimal(1e100),
+  softcapPower: new Decimal(0.5),
+  canBuyMax() {}, // Only needed for static layers with buy max
+  gainMult() {
+    // Calculate the multiplier for main currency from bonuses
+    let mult = new Decimal(1);
+    /*
+    if (hasUpgrade(this.layer, 166)) mult = mult.times(2); // These upgrades don't exist
+    if (hasUpgrade(this.layer, 120))
+      mult = mult.times(upgradeEffect(this.layer, 120) as DecimalSource);
+    */
+    return mult;
+  },
+  gainExp() {
+    // Calculate the exponent on main currency from bonuses
+    return new Decimal(1);
+  },
+  row: 0, // Row the layer is in on the tree (0 is the first row)
+  effect() {
+    return {
+      // Formulas for any boosts inherent to resources in the layer. Can return a single value instead of an object if there is just one effect
+      waffleBoost: Decimal.pow(player.layers[this.layer].points, 0.2),
+      icecreamCap: player.layers[this.layer].points.times(10)
+    };
+  },
+  effectDisplay() {
+    // Optional text to describe the effects
+    const eff = this.effect as { waffleBoost: Decimal; icecreamCap: Decimal };
+    const waffleBoost = eff.waffleBoost.times(
+      (buyableEffect(this.layer, 11) as { first: Decimal, second: Decimal }).first
+    );
+    return (
+      "which are boosting waffles by " +
+      format(waffleBoost) +
+      " and increasing the Ice Cream cap by " +
+      format(eff.icecreamCap)
+    );
+  },
+  infoboxes: {
+    data: {
+      coolInfo: {
+        title: "Lore",
+        titleStyle: { color: "#FE0000" },
+        body: "DEEP LORE!",
+        bodyStyle: { "background-color": "#0000EE" }
+      }
+    }
+  },
+  milestones: {
+    data: {
+      0: {
+        requirementDisplay: "3 Lollipops",
+        done() {
+          return (player.layers[this.layer].best as Decimal).gte(3);
+        }, // Used to determine when to give the milestone
+        effectDisplay: "Unlock the next milestone"
+      },
+      1: {
+        requirementDisplay: "4 Lollipops",
+        unlocked() {
+          return hasMilestone(this.layer, 0);
+        },
+        done() {
+          return (player.layers[this.layer].best as Decimal).gte(4);
+        },
+        effectDisplay: "You can toggle beep and boop (which do nothing)",
+        optionsDisplay: `
+					<div style="display: flex; justify-content: center">
+						<Toggle :value="player.layers.c.beep" @change="value => player.layers.c.beep = value" />
+						<Toggle :value="player.layers.f.boop" @change="value => player.layers.f.boop = value" />
+					</div>
+				`,
+        style() {
+          if (hasMilestone(this.layer, this.id))
+            return {
+              backgroundColor: "#1111DD"
+            };
+        }
+      }
+    }
+  },
+  challenges: {
+    data: {
+      11: {
+        name: "Fun",
+        completionLimit: 3,
+        challengeDescription() {
+          return (
+            "Makes the game 0% harder<br>" +
+            challengeCompletions(this.layer, this.id) +
+            "/" +
+            this.completionLimit +
+            " completions"
+          );
+        },
+        unlocked() {
+          return (player.layers[this.layer].best as Decimal).gt(0);
+        },
+        goalDescription: "Have 20 points I guess",
+        canComplete() {
+          return player.points.gte(20);
+        },
+        effect() {
+          const ret = player.layers[this.layer].points.add(1).tetrate(0.02);
+          return ret;
+        },
+        rewardDisplay() {
+          return format(this.effect as Decimal) + "x";
+        },
+        countsAs: [12, 21], // Use this for if a challenge includes the effects of other challenges. Being in this challenge "counts as" being in these.
+        rewardDescription: "Says hi",
+        onComplete() {
+          console.log("hiii");
+        }, // Called when you successfully complete the challenge
+        onEnter() {
+          console.log("So challenging");
+        },
+        onExit() {
+          console.log("Sweet freedom!");
+        }
+      }
+    }
+  },
+  upgrades: {
+    data: {
+      11: {
+        title: "Generator of Genericness",
+        description: "Gain 1 Point every second.",
+        cost: new Decimal(1),
+        unlocked() {
+          return player.layers[this.layer].unlocked;
+        } // The upgrade is only visible when this is true
+      },
+      12: {
+        description:
+          "Point generation is faster based on your unspent Lollipops.",
+        cost: new Decimal(1),
+        unlocked() {
+          return hasUpgrade(this.layer, 11);
+        },
+        effect() {
+          // Calculate bonuses from the upgrade. Can return a single value or an object with multiple values
+          let ret = player.layers[this.layer].points
+            .add(1)
+            .pow(
+              player.layers[this.layer].upgrades!.includes(24)
+                ? 1.1
+                : player.layers[this.layer].upgrades!.includes(14)
+                ? 0.75
+                : 0.5
+            );
+          if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000");
+          return ret;
+        },
+        effectDisplay() {
+          return format(this.effect as Decimal) + "x";
+        } // Add formatting to the effect
+      },
+      13: {
+        unlocked() {
+          return hasUpgrade(this.layer, 12);
+        },
+        onPurchase() {
+          // This function triggers when the upgrade is purchased
+          player.layers[this.layer].unlockOrder = 0;
+        },
+        style() {
+          if (hasUpgrade(this.layer, this.id))
+            return {
+              "background-color": "#1111dd"
+            };
+          else if (!this.canAfford) {
+            return {
+              "background-color": "#dd1111"
+            };
+          } // Otherwise use the default
+        },
+        canAfford() {
+          return player.points.lte(7);
+        },
+        pay() {
+          player.points = player.points.add(7);
+        },
+        fullDisplay:
+          "Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab."
+      },
+      22: {
+        title: "This upgrade doesn't exist",
+        description: "Or does it?.",
+        currencyLocation() {
+          return player.layers[this.layer].buyables;
+        }, // The object in player data that the currency is contained in
+        currencyDisplayName: "exhancers", // Use if using a nonstandard currency
+        currencyInternalName: 11, // Use if using a nonstandard currency
+
+        cost: new Decimal(3),
+        unlocked() {
+          return player.layers[this.layer].unlocked;
+        } // The upgrade is only visible when this is true
+      }
+    }
+  },
+  buyables: {
+    showBRespecButton: true,
+    respec() {
+      // Optional, reset things and give back your currency. Having this function makes a respec button appear
+      player.layers[this.layer].points = player.layers[this.layer].points.add(
+        player.layers[this.layer].spentOnBuyables as Decimal
+      ); // A built-in thing to keep track of this but only keeps a single value
+      this.reset();
+      resetLayer(this.layer, true); // Force a reset
+    },
+    respecButtonDisplay: "Respec Thingies", // Text on Respec button, optional
+    respecWarningDisplay:
+      "Are you sure? Respeccing these doesn't accomplish much.",
+    data: {
+      11: {
+        title: "Exhancers", // Optional, displayed at the top in a larger font
+        cost() {
+          // cost for buying xth buyable, can be an object if there are multiple currencies
+          let x = this.amount;
+          if (x.gte(25)) x = x.pow(2).div(25);
+          const cost = Decimal.pow(2, x.pow(1.5));
+          return cost.floor();
+        },
+        effect() {
+          // Effects of owning x of the items, x is a decimal
+          const x = this.amount;
+          const eff = {} as { first?: Decimal; second?: Decimal };
+          if (x.gte(0)) eff.first = Decimal.pow(25, x.pow(1.1));
+          else eff.first = Decimal.pow(1 / 25, x.times(-1).pow(1.1));
+
+          if (x.gte(0)) eff.second = x.pow(0.8);
+          else
+            eff.second = x
+              .times(-1)
+              .pow(0.8)
+              .times(-1);
+          return eff;
+        },
+        display() {
+          // Everything else displayed in the buyable button after the title
+          return (
+            "Cost: " +
+            format(this.cost!) +
+            " lollipops\n\
+					Amount: " +
+            player.layers[this.layer].buyables![this.id] +
+            "/4\n\
+					Adds + " +
+            format((this.effect as { first: Decimal; second: Decimal }).first) +
+            " things and multiplies stuff by " +
+            format((this.effect as { first: Decimal; second: Decimal }).second)
+          );
+        },
+        unlocked() {
+          return player.layers[this.layer].unlocked;
+        },
+        canAfford() {
+          return player.layers[this.layer].points.gte(this.cost!);
+        },
+        buy() {
+          const cost = this.cost!;
+          player.layers[this.layer].points = player.layers[
+            this.layer
+          ].points.sub(cost);
+          player.layers[this.layer].buyables![this.id] = player.layers[
+            this.layer
+          ].buyables![this.id].add(1);
+          player.layers[this.layer].spentOnBuyables = (player.layers[
+            this.layer
+          ].spentOnBuyables as Decimal).add(cost); // This is a built-in system that you can use for respeccing but it only works with a single Decimal value
+        },
+        buyMax() {}, // You'll have to handle this yourself if you want
+        style: { height: "222px" },
+        purchaseLimit: new Decimal(4),
+        sellOne() {
+          const amount = getBuyableAmount(this.layer, this.id)!;
+          if (amount.lte(0)) return; // Only sell one if there is at least one
+          setBuyableAmount(this.layer, this.id, amount.sub(1));
+          player.layers[this.layer].points = player.layers[
+            this.layer
+          ].points.add(this.cost!);
+        }
+      }
+    }
+  },
+  onReset(resettingLayer: string) {
+    // Triggers when this layer is being reset, along with the layer doing the resetting. Not triggered by lower layers resetting, but is by layers on the same row.
+    if (
+      layers[resettingLayer].row != undefined &&
+      this.row != undefined &&
+      layers[resettingLayer].row! > this.row!
+    )
+      resetLayerData(this.layer, ["points"]); // This is actually the default behavior
+  },
+  automate() {}, // Do any automation inherent to this layer if appropriate
+  resetsNothing() {
+    return false;
+  },
+  onPrestige() {
+    return;
+  }, // Useful for if you gain secondary resources or have other interesting things happen to this layer when you reset it. You gain the currency after this function ends.
+
+  hotkeys: [
+    {
+      key: "c",
+      description: "reset for lollipops or whatever",
+      press() {
+        if (layers[this.layer].canReset) resetLayer(this.layer);
+      }
+    },
+    {
+      key: "ctrl+c",
+      description: "respec things",
+      press() {
+        layers[this.layer].buyables!.respec!();
+      },
+      unlocked() {
+        return hasUpgrade("c", "22");
+      }
+    }
+  ],
+  increaseUnlockOrder: [], // Array of layer names to have their order increased when this one is first unlocked
+
+  microtabs: {
+    stuff: {
+      data: {
+        first: {
+          display: `
+						<upgrades />
+						<div>confirmed</div>`
+        },
+        second: {
+          embedLayer: "f"
+        }
+      }
+    },
+    otherStuff: {
+      // There could be another set of microtabs here
+      data: {}
+    }
+  },
+
+  bars: {
+    data: {
+      longBoi: {
+        fillStyle: { "background-color": "#FFFFFF" },
+        baseStyle: { "background-color": "#696969" },
+        textStyle: { color: "#04e050" },
+
+        borderStyle() {
+          return {};
+        },
+        direction: Direction.Right,
+        width: 300,
+        height: 30,
+        progress() {
+          return player.points
+            .add(1)
+            .log(10)
+            .div(10)
+            .toNumber();
+        },
+        display() {
+          return format(player.points) + " / 1e10 points";
+        },
+        unlocked: true
+      },
+      tallBoi: {
+        fillStyle: { "background-color": "#4BEC13" },
+        baseStyle: { "background-color": "#000000" },
+        textStyle: { "text-shadow": "0px 0px 2px #000000" },
+
+        borderStyle() {
+          return { "border-width": "7px" };
+        },
+        direction: Direction.Up,
+        width: 50,
+        height: 200,
+        progress() {
+          return player.points.div(100);
+        },
+        display() {
+          return formatWhole(player.points.div(1).min(100)) + "%";
+        },
+        unlocked: true
+      },
+      flatBoi: {
+        fillStyle: { "background-color": "#FE0102" },
+        baseStyle: { "background-color": "#222222" },
+        textStyle: { "text-shadow": "0px 0px 2px #000000" },
+
+        borderStyle() {
+          return {};
+        },
+        direction: Direction.Up,
+        width: 100,
+        height: 30,
+        progress() {
+          return player.layers.c.points.div(50);
+        },
+        unlocked: true
+      }
+    }
+  },
+
+  // Optional, lets you format the tab yourself by listing components. You can create your own components in v.js.
+  subtabs: {
+    "main tab": {
+      buttonStyle() {
+        return { color: "orange" };
+      },
+      notify: true,
+      display: `
+				<main-display />
+				<sticky><prestige-button /></sticky>
+				<resource-display />
+				<spacer height="5px" />
+				<button onclick='console.log("yeet")'>'HI'</button>
+				<div>Name your points!</div>
+				<TextField :value="player.layers.c.thingy" @input="value => player.layers.c.thingy = value" :field="false" />
+				<sticky style="color: red; font-size: 32px; font-family: Comic Sans MS;">I have {{ format(player.points) }} {{ player.layers.c.thingy }} points!</sticky>
+				<hr />
+				<milestones />
+				<spacer />
+				<upgrades />
+				<challenges />`,
+      glowColor: "blue"
+    },
+    thingies: {
+      resetNotify: true,
+      style() {
+        return { "background-color": "#222222", "--background": "#222222" };
+      },
+      buttonStyle() {
+        return { "border-color": "orange" };
+      },
+      display: `
+				<buyables />
+				<spacer />
+				<row style="width: 600px; height: 350px; background-color: green; border-style: solid;">
+					<Toggle :value="player.layers.c.beep" @change="value => player.layers.c.beep = value" />
+					<spacer width="30px" height="10px" />
+					<div>Beep</div>
+					<spacer />
+					<vr height="200px"/>
+					<column>
+						<prestige-button style="width: 150px; height: 80px" />
+						<prestige-button style="width: 100px; height: 150px" />
+					</column>
+				</row>
+				<spacer />
+				<img src="https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png" />`
+    },
+    jail: {
+      display: `
+				<infobox id="coolInfo" />
+				<bar id="longBoi" />
+				<spacer />
+				<row>
+					<column style="background-color: #555555; padding: 15px">
+						<div style="color: teal">Sugar level:</div><spacer /><bar id="tallBoi" />
+					</column>
+					<spacer />
+					<column>
+						<div>idk</div>
+						<spacer width="0" height="50px" />
+						<bar id="flatBoi" />
+					</column>
+				</row>
+				<spacer />
+				<div>It's jail because "bars"! So funny! Ha ha!</div>
+				<tree :nodes="[['f', 'c'], ['g', 'spook', 'h']]" />`
+    },
+    illuminati: {
+      unlocked() {
+        return hasUpgrade("c", 13);
+      },
+      display: `
+				<h1> C O N F I R M E D </h1>
+				<spacer />
+				<microtab family="stuff" style="width: 660px; height: 370px; background-color: brown; --background: brown; border: solid white; margin: auto" />
+				<div>Adjust how many points H gives you!</div>
+				<Slider :value="player.layers.c.otherThingy" @change="value => player.c.otherThingy = value" :min="1" :max="30" />`
+    }
+  },
+  style() {
+    return {
+      //'background-color': '#3325CC'
+    };
+  },
+  nodeStyle() {
+    return {
+      // Style on the layer node
+      color: "#3325CC",
+      "text-decoration": "underline"
+    };
+  },
+  glowColor: "orange", // If the node is highlighted, it will be this color (default is red)
+  componentStyles: {
+    challenge() {
+      return { height: "200px" };
+    },
+    "prestige-button"() {
+      return { color: "#AA66AA" };
+    }
+  },
+  tooltip() {
+    // Optional, tooltip displays when the layer is unlocked
+    let tooltip = "{{ formatWhole(player.layers.c.points) }} {{ layers.c.resource }}";
+    if (player.layers[this.layer].buyables![11].gt(0))
+      tooltip +=
+        "<br><i><br><br><br>{{ formatWhole(player.layers.c.buyables![11]) }} Exhancers</i>";
+    return tooltip;
+  },
+  shouldNotify() {
+    // Optional, layer will be highlighted on the tree if true.
+    // Layer will automatically highlight if an upgrade is purchasable.
+    return player.layers.c.buyables![11] == new Decimal(1);
+  },
+  mark: "https://unsoftcapped2.github.io/The-Modding-Tree-2/discord.png",
+  resetDescription: "Melt your points into "
+} as RawLayer;
diff --git a/src/data/layers/aca/f.js b/src/data/layers/aca/f.js
deleted file mode 100644
index 9fd0a54..0000000
--- a/src/data/layers/aca/f.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import Decimal, { formatWhole } from '../../../util/bignum';
-import player from '../../../game/player';
-import { layers as tmp } from '../../../game/layers';
-import { getClickableState } from '../../../util/features';
-
-export default {
-    id: "f",
-    infoboxes:{
-        coolInfo: {
-            title: "Lore",
-            titleStyle: {'color': '#FE0000'},
-            body: "DEEP LORE!",
-            bodyStyle: {'background-color': "#0000EE"}
-        }
-    },
-
-    startData() { return {
-        unlocked: false,
-        points: new Decimal(0),
-        boop: false,
-        clickables: {[11]: "Start"}, // Optional default Clickable state
-    }},
-    color: "#FE0102",
-    requires() {return new Decimal(10)},
-    resource: "farm points",
-    baseResource: "points",
-    baseAmount() {return player.points},
-    type: "static",
-    exponent: 0.5,
-    base: 3,
-    roundUpCost: true,
-    canBuyMax() {return false},
-    name: "Farms",
-    //directMult() {return new Decimal(player.c.otherThingy)},
-
-    row: 1,
-    branches: [{ target: "c", 'stroke-width': '25px', 'stroke': 'blue', style: 'filter: blur(5px)' }], // When this layer appears, a branch will appear from this layer to any layers here. Each entry can be a pair consisting of a layer id and a color.
-
-    tooltipLocked() { // Optional, tooltip displays when the layer is locked
-        return ("This weird farmer dinosaur will only see you if you have at least {{layers.f.requires}} points. You only have {{ formatWhole(player.points) }}")
-    },
-    midsection: '<div><br/><img src="https://images.beano.com/store/24ab3094eb95e5373bca1ccd6f330d4406db8d1f517fc4170b32e146f80d?auto=compress%2Cformat&dpr=1&w=390" /><div>Bork Bork!</div></div>',
-    // The following are only currently used for "custom" Prestige type:
-    prestigeButtonDisplay() { //Is secretly HTML
-        if (!this.canBuyMax) return "Hi! I'm a <u>weird dinosaur</u> and I'll give you a Farm Point in exchange for all of your points and lollipops! (At least " + formatWhole(tmp[this.layer].nextAt) + " points)"
-        if (this.canBuyMax) return "Hi! I'm a <u>weird dinosaur</u> and I'll give you <b>" + formatWhole(tmp[this.layer].resetGain) + "</b> Farm Points in exchange for all of your points and lollipops! (You'll get another one at " + formatWhole(tmp[this.layer].nextAtDisp) + " points)"
-    },
-    canReset() {
-        return tmp[this.layer].baseAmount.gte(tmp[this.layer].nextAt)
-    },
-    // This is also non minimal, a Clickable!
-    clickables: {
-
-        masterButtonClick() {
-            if (getClickableState(this.layer, 11) == "Borkened...")
-                player[this.layer].clickables[11] = "Start"
-        },
-        masterButtonDisplay() {return (getClickableState(this.layer, 11) == "Borkened...") ? "Fix the clickable!" : "Does nothing"}, // Text on Respec button, optional
-        11: {
-            title: "Clicky clicky!", // Optional, displayed at the top in a larger font
-            display() { // Everything else displayed in the buyable button after the title
-                let data = getClickableState(this.layer, this.id)
-                return "Current state:<br>" + data
-            },
-            unlocked() { return player[this.layer].unlocked },
-            canClick() {
-                return getClickableState(this.layer, this.id) !== "Borkened..."},
-            click() {
-                switch(getClickableState(this.layer, this.id)){
-                    case "Start":
-                        player[this.layer].clickables[this.id] = "A new state!"
-                        break;
-                    case "A new state!":
-                        player[this.layer].clickables[this.id] = "Keep going!"
-                        break;
-                    case "Keep going!":
-                        player[this.layer].clickables[this.id] = "Maybe that's a bit too far..."
-                        break;
-                    case "Maybe that's a bit too far...":
-                        //makeParticles(coolParticle, 4)
-                        player[this.layer].clickables[this.id] = "Borkened..."
-                        break;
-                    default:
-                        player[this.layer].clickables[this.id] = "Start"
-                        break;
-                }
-            },
-            hold(){
-                console.log("Clickkkkk...")
-            },
-            style() {
-                switch(getClickableState(this.layer, this.id)){
-                    case "Start":
-                        return {'background-color': 'green'}
-                    case "A new state!":
-                        return {'background-color': 'yellow'}
-                    case "Keep going!":
-                        return {'background-color': 'orange'}
-                    case "Maybe that's a bit too far...":
-                        return {'background-color': 'red'}
-                    default:
-                        return {}
-            }},
-        },
-    },
-
-}
diff --git a/src/data/layers/aca/f.ts b/src/data/layers/aca/f.ts
new file mode 100644
index 0000000..95c2bdd
--- /dev/null
+++ b/src/data/layers/aca/f.ts
@@ -0,0 +1,151 @@
+/* eslint-disable */
+import { layers as tmp } from "@/game/layers";
+import player from "@/game/player";
+import { RawLayer } from "@/typings/layer";
+import Decimal, { formatWhole } from "@/util/bignum";
+import { getClickableState } from "@/util/features";
+
+export default {
+  id: "f",
+  infoboxes: {
+    data: {
+      coolInfo: {
+        title: "Lore",
+        titleStyle: { color: "#FE0000" },
+        body: "DEEP LORE!",
+        bodyStyle: { "background-color": "#0000EE" }
+      }
+    }
+  },
+
+  startData() {
+    return {
+      unlocked: false,
+      points: new Decimal(0),
+      boop: false,
+      clickables: { [11]: "Start" } // Optional default Clickable state
+    };
+  },
+  color: "#FE0102",
+  requires() {
+    return new Decimal(10);
+  },
+  resource: "farm points",
+  baseResource: "points",
+  baseAmount() {
+    return player.points;
+  },
+  type: "static",
+  exponent: 0.5,
+  base: 3,
+  roundUpCost: true,
+  canBuyMax() {
+    return false;
+  },
+  name: "Farms",
+  //directMult() {return new Decimal(player.c.otherThingy)},
+
+  row: 1,
+  branches: [
+    {
+      target: "c",
+      "stroke-width": "25px",
+      stroke: "blue",
+      style: "filter: blur(5px)"
+    }
+  ], // When this layer appears, a branch will appear from this layer to any layers here. Each entry can be a pair consisting of a layer id and a color.
+
+  tooltipLocked() {
+    // Optional, tooltip displays when the layer is locked
+    return "This weird farmer dinosaur will only see you if you have at least {{layers.f.requires}} points. You only have {{ formatWhole(player.points) }}";
+  },
+  midsection:
+    '<div><br/><img src="https://images.beano.com/store/24ab3094eb95e5373bca1ccd6f330d4406db8d1f517fc4170b32e146f80d?auto=compress%2Cformat&dpr=1&w=390" /><div>Bork Bork!</div></div>',
+  // The following are only currently used for "custom" Prestige type:
+  prestigeButtonDisplay() {
+    //Is secretly HTML
+    if (!this.canBuyMax)
+      return (
+        "Hi! I'm a <u>weird dinosaur</u> and I'll give you a Farm Point in exchange for all of your points and lollipops! (At least " +
+        formatWhole(tmp[this.layer].nextAt) +
+        " points)"
+      );
+    if (this.canBuyMax)
+      return (
+        "Hi! I'm a <u>weird dinosaur</u> and I'll give you <b>" +
+        formatWhole(tmp[this.layer].resetGain) +
+        "</b> Farm Points in exchange for all of your points and lollipops! (You'll get another one at " +
+        formatWhole(tmp[this.layer].nextAt) +
+        " points)"
+      );
+  },
+  canReset() {
+    return Decimal.gte(tmp[this.layer].baseAmount!, tmp[this.layer].nextAt);
+  },
+  // This is also non minimal, a Clickable!
+  clickables: {
+    masterButtonClick() {
+      if (getClickableState(this.layer, 11) == "Borkened...")
+        player.layers[this.layer].clickables![11] = "Start";
+    },
+    masterButtonDisplay() {
+      return getClickableState(this.layer, 11) == "Borkened..."
+        ? "Fix the clickable!"
+        : "Does nothing";
+    }, // Text on Respec button, optional
+    data: {
+      11: {
+        title: "Clicky clicky!", // Optional, displayed at the top in a larger font
+        display() {
+          // Everything else displayed in the buyable button after the title
+          const data = getClickableState(this.layer, this.id);
+          return "Current state:<br>" + data;
+        },
+        unlocked() {
+          return player.layers[this.layer].unlocked;
+        },
+        canClick() {
+          return getClickableState(this.layer, this.id) !== "Borkened...";
+        },
+        click() {
+          switch (getClickableState(this.layer, this.id)) {
+            case "Start":
+              player.layers[this.layer].clickables![this.id] = "A new state!";
+              break;
+            case "A new state!":
+              player.layers[this.layer].clickables![this.id] = "Keep going!";
+              break;
+            case "Keep going!":
+              player.layers[this.layer].clickables![this.id] =
+                "Maybe that's a bit too far...";
+              break;
+            case "Maybe that's a bit too far...":
+              //makeParticles(coolParticle, 4)
+              player.layers[this.layer].clickables![this.id] = "Borkened...";
+              break;
+            default:
+              player.layers[this.layer].clickables![this.id] = "Start";
+              break;
+          }
+        },
+        hold() {
+          console.log("Clickkkkk...");
+        },
+        style() {
+          switch (getClickableState(this.layer, this.id)) {
+            case "Start":
+              return { "background-color": "green" };
+            case "A new state!":
+              return { "background-color": "yellow" };
+            case "Keep going!":
+              return { "background-color": "orange" };
+            case "Maybe that's a bit too far...":
+              return { "background-color": "red" };
+            default:
+              return {};
+          }
+        }
+      }
+    }
+  }
+} as RawLayer;
diff --git a/src/data/layers/demo-infinity.js b/src/data/layers/demo-infinity.js
deleted file mode 100644
index 1a9765d..0000000
--- a/src/data/layers/demo-infinity.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import Decimal, { format } from '../../util/bignum';
-import player from '../../game/player';
-import { layers } from '../../game/layers';
-import { hasUpgrade, hasMilestone, getBuyableAmount, setBuyableAmount, hasChallenge } from '../../util/features';
-import { resetLayer } from '../../util/layers';
-
-export default {
-    id: "i",
-    position: 2, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order
-    startData() { return {
-        unlocked: false,
-		points: new Decimal(0),
-
-    }},
-  branches: ["p"],
-    color: "#964B00",
-    requires(){
-      let require = new Decimal(8).plus(player.i.points.div(10).floor().times(2))
-      return require
-    } , // Can be a function that takes requirement increases into account
-  effectDisplay(){return "Multiplying points and prestige points by "+format(player[this.layer].points.plus(1).pow(hasUpgrade("p",235)?6.9420:1))},
-    resource: "Infinity", // Name of prestige currency
-    baseResource: "pointy points", // Name of resource prestige is based on
-    baseAmount() {return player.p.buyables[21]}, // Get the current amount of baseResource
-    type: "custom", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
-    resetGain(){
-
-      if (hasMilestone("p",12)){return getBuyableAmount("p",21).div(2).floor().times(2).times(5).sub(30).sub(player.i.points)}
-      return (player.p.buyables[21].gte(layers.i.requires)?1:0)}, // Prestige currency exponent
-    getNextAt(){return new Decimal(100)},
-  canReset(){return player.p.buyables[21].gte(layers.i.requires)},
-  prestigeButtonDisplay(){return "Reset everything for +"+format(layers.i.resetGain)+" Infinity.<br>You need "+format(layers.i.requires)+" pointy points to reset."},
-    row: 1, // Row the layer is in on the tree (0 is the first row)
-    hotkeys: [
-        {key: "i", description: "I: Infinity", press(){if (layers.i.canReset) resetLayer(this.layer)}},
-    ],
-    layerShown(){return player[this.layer].unlocked||new Decimal(player.p.buyables[21]).gte(8)},
-    milestones: {
-    0: {
-        requirementDisplay: "2 Infinity points",
-        effectDisplay: "Keep ALL milestones on reset",
-        done() { return player[this.layer].points.gte(2) },
-
-    },
-      1: {
-        requirementDisplay: "3 Infinity points",
-        effectDisplay: "Pointy points don't reset generators",
-        done() { return player[this.layer].points.gte(3) },
-      unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-      2: {
-        requirementDisplay: "4 Infinity points",
-        effectDisplay: "Start with 6 <b>Time Dilation</b>, 3 <b>Point</b>, and 1 of the other 2 challenges",
-        done() { return player[this.layer].points.gte(4) },
-unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-      3: {
-        requirementDisplay: "5 Infinity points",
-        effectDisplay: "Start with 40 upgrades and 6 boosts",
-        done() { return player[this.layer].points.gte(5) },
-unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-      4: {
-        requirementDisplay: "6 Infinity points",
-        effectDisplay: "You can choose all of the 14th row upgrades, and remove the respec button",
-        done() { return player[this.layer].points.gte(6) },
-unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-      5: {
-        requirementDisplay: "8 Infinity points",
-        effectDisplay: "Keep all upgrades and 7 Time dilation",
-        done() { return player[this.layer].points.gte(8) },
-unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-      6: {
-        requirementDisplay: "10 Infinity points",
-        effectDisplay: "Infinity reset nothing and auto prestige",
-        done() { return player[this.layer].points.gte(10) },
-unlocked(){return hasMilestone(this.layer,this.id-1)}
-    },
-    },
-  resetsNothing(){return hasMilestone(this.layer,6)},
-update(){
-  if (hasMilestone(this.layer,0)){
-    if (!hasMilestone("p",0)){
-      player.p.milestones.push(0)
-      player.p.milestones.push(1)
-      player.p.milestones.push(2)
-      player.p.milestones.push(3)
-      player.p.milestones.push(4)
-      player.p.milestones.push(5)
-      player.p.milestones.push(6)
-      player.p.milestones.push(7)
-      player.p.milestones.push(8)
-    }
-  }
-  if (hasMilestone(this.layer,2)){
-    if (!hasChallenge("p",11)){
-      player.p.challenges[11]=new Decimal(hasMilestone(this.layer,5)?7:6)
-      player.p.challenges[12]=new Decimal(3)
-      player.p.challenges[21]=new Decimal(1)
-      player.p.challenges[22]=new Decimal(1)
-    }
-  }
-  if (hasMilestone(this.layer,3)){
-    if (!hasUpgrade("p",71)){
-      player.p.upgrades=[11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44,51,52,53,54,61,62,63,64,71,72,73,74,81,82,83,84,91,92,93,94,101,102,103,104]
-    }
-    if (getBuyableAmount("p",11).lt(6)){setBuyableAmount("p",11,new Decimal(6))}
-  }
-  if (hasUpgrade(this.layer,13)){
-
-    for (let i=0;i<(hasUpgrade("p",222)?100:hasUpgrade("p",215)?10:1);i++){
-      if (layers.p.buyables[12].canAfford)layers.p.buyables[12].buy()
-    if (layers.p.buyables[13].canAfford)layers.p.buyables[13].buy()
-    if (layers.p.buyables[14].canAfford&&layers.p.buyables[14].unlocked())layers.p.buyables[14].buy()
-    if (layers.p.buyables[21].canAfford)layers.p.buyables[21].buy()}
-  }
-  if (hasUpgrade("p",223)){
-    if (hasMilestone("p",14))player.p.buyables[22]=player.p.buyables[22].max(player.p.buyables[21].sub(7))
-    else if (layers.p.buyables[22].canAfford)layers.p.buyables[22].buy()
-  }
-  if (hasMilestone(this.layer,5)&&!hasUpgrade("p",111)){player.p.upgrades=[11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44,51,52,53,54,61,62,63,64,71,72,73,74,81,82,83,84,91,92,93,94,101,102,103,104,111,121,122,131,132,141,142,143]}
-    if (hasMilestone(this.layer,6)) {
-      this.reset();
-    }
-},
-  upgrades:{
-    rows: 999,
-    cols: 5,
-    11:{
-      title: "Prestige",
-      description: "Gain 100% of prestige points per second",
-      cost(){
-        return new Decimal(1)},
-      unlocked(){return hasMilestone(this.layer,4)}
-    },
-    12:{
-      title: "Automation",
-      description: "Remove the nerf of upgrade <b>Active</b>",
-      cost(){
-        return new Decimal(2)},
-      unlocked(){return hasUpgrade(this.layer,11)}
-    },
-    13:{
-      title: "Pointy",
-      description: "Automatically buy generators and pointy points",
-      cost(){
-        return new Decimal(5)},
-      unlocked(){return hasUpgrade(this.layer,11)}
-    },
-  }
-  }
diff --git a/src/data/layers/demo-infinity.ts b/src/data/layers/demo-infinity.ts
new file mode 100644
index 0000000..6e62446
--- /dev/null
+++ b/src/data/layers/demo-infinity.ts
@@ -0,0 +1,354 @@
+/* eslint-disable */
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Layer, RawLayer } from "@/typings/layer";
+import Decimal, { format } from "@/util/bignum";
+import {
+  getBuyableAmount, hasChallenge, hasMilestone, hasUpgrade, setBuyableAmount
+} from "@/util/features";
+import { resetLayer } from "@/util/layers";
+
+export default {
+  id: "i",
+  position: 2, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order
+  startData() {
+    return {
+      unlocked: false,
+      points: new Decimal(0)
+    };
+  },
+  branches: ["p"],
+  color: "#964B00",
+  requires() {
+    const require = new Decimal(8).plus(
+      player.layers.i.points
+        .div(10)
+        .floor()
+        .times(2)
+    );
+    return require;
+  }, // Can be a function that takes requirement increases into account
+  effectDisplay() {
+    return (
+      "Multiplying points and prestige points by " +
+      format(
+        player.layers[this.layer].points
+          .plus(1)
+          .pow(hasUpgrade("p", 235) ? 6.942 : 1)
+      )
+    );
+  },
+  resource: "Infinity", // Name of prestige currency
+  baseResource: "pointy points", // Name of resource prestige is based on
+  baseAmount() {
+    return player.layers.p.buyables![21];
+  }, // Get the current amount of baseResource
+  type: "custom", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
+  resetGain() {
+    if (hasMilestone("p", 12)) {
+      return getBuyableAmount("p", 21)!
+        .div(2)
+        .floor()
+        .times(2)
+        .times(5)
+        .sub(30)
+        .sub(player.layers.i.points);
+    }
+    return player.layers.p.buyables![21].gte(layers.i.requires!) ? 1 : 0;
+  }, // Prestige currency exponent
+  getNextAt() {
+    return new Decimal(100);
+  },
+  canReset() {
+    return player.layers.p.buyables![21].gte(layers.i.requires!);
+  },
+  prestigeButtonDisplay() {
+    return (
+      "Reset everything for +" +
+      format(layers.i.resetGain) +
+      " Infinity.<br>You need " +
+      format(layers.i.requires!) +
+      " pointy points to reset."
+    );
+  },
+  row: 1, // Row the layer is in on the tree (0 is the first row)
+  hotkeys: [
+    {
+      key: "i",
+      description: "I: Infinity",
+      press() {
+        if (layers.i.canReset) resetLayer(this.layer);
+      }
+    }
+  ],
+  layerShown() {
+    return (
+      player.layers[this.layer].unlocked ||
+      new Decimal(player.layers.p.buyables[21]).gte(8)
+    );
+  },
+  milestones: {
+    data: {
+      0: {
+        requirementDisplay: "2 Infinity points",
+        effectDisplay: "Keep ALL milestones on reset",
+        done() {
+          return player.layers[this.layer].points.gte(2);
+        }
+      },
+      1: {
+        requirementDisplay: "3 Infinity points",
+        effectDisplay: "Pointy points don't reset generators",
+        done() {
+          return player.layers[this.layer].points.gte(3);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      },
+      2: {
+        requirementDisplay: "4 Infinity points",
+        effectDisplay:
+          "Start with 6 <b>Time Dilation</b>, 3 <b>Point</b>, and 1 of the other 2 challenges",
+        done() {
+          return player.layers[this.layer].points.gte(4);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      },
+      3: {
+        requirementDisplay: "5 Infinity points",
+        effectDisplay: "Start with 40 upgrades and 6 boosts",
+        done() {
+          return player.layers[this.layer].points.gte(5);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      },
+      4: {
+        requirementDisplay: "6 Infinity points",
+        effectDisplay:
+          "You can choose all of the 14th row upgrades, and remove the respec button",
+        done() {
+          return player.layers[this.layer].points.gte(6);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      },
+      5: {
+        requirementDisplay: "8 Infinity points",
+        effectDisplay: "Keep all upgrades and 7 Time dilation",
+        done() {
+          return player.layers[this.layer].points.gte(8);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      },
+      6: {
+        requirementDisplay: "10 Infinity points",
+        effectDisplay: "Infinity reset nothing and auto prestige",
+        done() {
+          return player.layers[this.layer].points.gte(10);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, Number(this.id) - 1);
+        }
+      }
+    }
+  },
+  resetsNothing() {
+    return hasMilestone(this.layer, 6);
+  },
+  update(this: Layer) {
+    if (hasMilestone(this.layer, 0)) {
+      if (!hasMilestone("p", 0)) {
+        player.layers.p.milestones!.push(0);
+        player.layers.p.milestones!.push(1);
+        player.layers.p.milestones!.push(2);
+        player.layers.p.milestones!.push(3);
+        player.layers.p.milestones!.push(4);
+        player.layers.p.milestones!.push(5);
+        player.layers.p.milestones!.push(6);
+        player.layers.p.milestones!.push(7);
+        player.layers.p.milestones!.push(8);
+      }
+    }
+    if (hasMilestone(this.layer, 2)) {
+      if (!hasChallenge("p", 11)) {
+        player.layers.p.challenges![11] = new Decimal(
+          hasMilestone(this.layer, 5) ? 7 : 6
+        );
+        player.layers.p.challenges![12] = new Decimal(3);
+        player.layers.p.challenges![21] = new Decimal(1);
+        player.layers.p.challenges![22] = new Decimal(1);
+      }
+    }
+    if (hasMilestone(this.layer, 3)) {
+      if (!hasUpgrade("p", 71)) {
+        player.layers.p.upgrades = [
+          11,
+          12,
+          13,
+          14,
+          21,
+          22,
+          23,
+          24,
+          31,
+          32,
+          33,
+          34,
+          41,
+          42,
+          43,
+          44,
+          51,
+          52,
+          53,
+          54,
+          61,
+          62,
+          63,
+          64,
+          71,
+          72,
+          73,
+          74,
+          81,
+          82,
+          83,
+          84,
+          91,
+          92,
+          93,
+          94,
+          101,
+          102,
+          103,
+          104
+        ];
+      }
+      if (getBuyableAmount("p", 11)!.lt(6)) {
+        setBuyableAmount("p", 11, new Decimal(6));
+      }
+    }
+    if (hasUpgrade(this.layer, 13)) {
+      for (
+        let i = 0;
+        i < (hasUpgrade("p", 222) ? 100 : hasUpgrade("p", 215) ? 10 : 1);
+        i++
+      ) {
+        if (layers.p.buyables!.data[12].canAfford) layers.p.buyables!.data[12].buy();
+        if (layers.p.buyables!.data[13].canAfford) layers.p.buyables!.data[13].buy();
+        if (
+          layers.p.buyables!.data[14].canAfford &&
+          layers.p.buyables!.data[14].unlocked
+        )
+          layers.p.buyables!.data[14].buy();
+        if (layers.p.buyables!.data[21].canAfford) layers.p.buyables!.data[21].buy();
+      }
+    }
+    if (hasUpgrade("p", 223)) {
+      if (hasMilestone("p", 14))
+        player.layers.p.buyables![22] = player.layers.p.buyables![22].max(
+          player.layers.p.buyables![21].sub(7)
+        );
+      else if (layers.p.buyables!.data[22].canAfford) layers.p.buyables!.data[22].buy();
+    }
+    if (hasMilestone(this.layer, 5) && !hasUpgrade("p", 111)) {
+      player.layers.p.upgrades = [
+        11,
+        12,
+        13,
+        14,
+        21,
+        22,
+        23,
+        24,
+        31,
+        32,
+        33,
+        34,
+        41,
+        42,
+        43,
+        44,
+        51,
+        52,
+        53,
+        54,
+        61,
+        62,
+        63,
+        64,
+        71,
+        72,
+        73,
+        74,
+        81,
+        82,
+        83,
+        84,
+        91,
+        92,
+        93,
+        94,
+        101,
+        102,
+        103,
+        104,
+        111,
+        121,
+        122,
+        131,
+        132,
+        141,
+        142,
+        143
+      ];
+    }
+    if (hasMilestone(this.layer, 6)) {
+      this.reset();
+    }
+  },
+  upgrades: {
+    rows: 999,
+    cols: 5,
+    data: {
+      11: {
+        title: "Prestige",
+        description: "Gain 100% of prestige points per second",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, 4);
+        }
+      },
+      12: {
+        title: "Automation",
+        description: "Remove the nerf of upgrade <b>Active</b>",
+        cost() {
+          return new Decimal(2);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 11);
+        }
+      },
+      13: {
+        title: "Pointy",
+        description: "Automatically buy generators and pointy points",
+        cost() {
+          return new Decimal(5);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 11);
+        }
+      }
+    }
+  }
+} as RawLayer;
diff --git a/src/data/layers/demo.js b/src/data/layers/demo.js
deleted file mode 100644
index 10a9f70..0000000
--- a/src/data/layers/demo.js
+++ /dev/null
@@ -1,940 +0,0 @@
-import Decimal, { format } from '../../util/bignum';
-import player from '../../game/player';
-import { layers } from '../../game/layers';
-import { hasUpgrade, hasMilestone, getBuyableAmount, setBuyableAmount, hasChallenge } from '../../util/features';
-import { resetLayer } from '../../util/layers';
-
-export default {
-	id: "p",
-	position: 2,
-	startData() { return {
-		unlocked: true,
-		points: new Decimal(0),
-		gp: new Decimal(0),
-		g: new Decimal(0),
-		geff: new Decimal(1),
-		cmult: new Decimal(1),
-	}},
-	color: "#4BDC13",
-	requires(){
-		let require = new Decimal(68.99)
-		if (hasMilestone(this.layer,0))require=require.plus(0.01)
-		if (hasUpgrade(this.layer,21))require=require.tetrate(hasUpgrade("p",34)?(new Decimal(1).div(new Decimal(1).plus(layers.p.upgrades[34].effect))):1)
-		if (hasUpgrade(this.layer,22))require=require.pow(hasUpgrade("p",34)?(new Decimal(1).div(new Decimal(1).plus(layers.p.upgrades[34].effect))):1)
-		if (hasUpgrade(this.layer,23))require=require.div(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-		if (hasUpgrade(this.layer,24))require=require.sub(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-		return require.max(1)
-	},
-	resource: "prestige points",
-	baseResource: "points",
-	baseAmount() {return player.points}, // Get the current amount of baseResource
-	type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
-	exponent: 0.5, // Prestige currency exponent
-	gainMult() { // Calculate the multiplier for main currency from bonuses
-		let mult = new Decimal(1)
-		if (hasUpgrade(this.layer,131))mult=mult.times(10)
-			if (player.i.unlocked)mult=mult.times(player.i.points.plus(1).pow(hasUpgrade("p",235)?6.9420:1))
-				if (hasUpgrade(this.layer,222))mult=mult.times(getBuyableAmount(this.layer,22).plus(1))
-					if (hasUpgrade("p",231)){
-						let asdf = (hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10())
-						mult=mult.mul(asdf.plus(1))
-					}
-					if (hasMilestone(this.layer,13))mult=mult.mul(new Decimal(2).plus(layers.p.buyables[33].effect).pow(getBuyableAmount(this.layer,32)))
-						return mult
-				},
-	gainExp() { // Calculate the exponent on main currency from bonuses
-		return new Decimal(1)
-	},
-	row: 0, // Row the layer is in on the tree (0 is the first row)
-	hotkeys: [
-	{key: "p", description: "P: Reset for prestige points", press(){if (layers.p.canReset) resetLayer(this.layer)}},
-	],
-	layerShown(){return true},
-	upgrades:{
-		rows: 999,
-		cols: 5,
-		11:{
-			title: "Gain points",
-			description: "Point generation is increased by 1",
-			cost(){
-				if (hasMilestone(this.layer,2))return new Decimal(1)
-					return new Decimal(1.00001)},
-				unlocked(){return true}
-			},
-			12:{
-				title: "Gain more points",
-				description: "Point generation is singled",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,11)}
-			},
-			13:{
-				title: "Gain more points",
-				description: "Point generation is lined",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,12)}
-			},
-			14:{
-				title: "Gain more points",
-				description: "Point generation is tetrated by 1",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,13)}
-			},
-			21:{
-				title: "Lower prestige requirement",
-				description: "Prestige point requirement is superrooted by 1",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,14)}
-			},
-			22:{
-				title: "Lower prestige requirement more",
-				description: "Prestige point requirement is line rooted",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,21)}
-			},
-			23:{
-				title: "Lower prestige requirement more",
-				description: "Prestige point requirement is wholed",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,22)}
-			},
-			24:{
-				title: "Lower prestige requirement more",
-				description: "Prestige point requirement is decreased by 1",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,23)}
-			},
-			31:{
-				title: "Unlock",
-				description: "Unlock an upgrade",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,24)}
-			},
-			32:{
-				title: "An",
-				description: "Unlock an upgrade",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,31)}
-			},
-			33:{
-				title: "Upgrade",
-				description: "Unlock an upgrade",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,32)}
-			},
-			34:{
-				title: "Increase",
-				description(){return "Add 0.01 to all above upgrades. Currently: +"+format(this.effect)},
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,33)},
-				effect(){
-					let r = (hasUpgrade("p",41)?new Decimal(0.01).times(layers.p.upgrades[41].effect):new Decimal(0.01))
-					r=r.times(new Decimal(1).plus(new Decimal(player[this.layer].challenges[11]).add(1).pow(hasUpgrade(this.layer,121)?1.2:1)))
-					if (hasUpgrade(this.layer,92)) r=r.plus(new Decimal(0.001).times(player[this.layer].g.plus(1)).min(0.05))
-						return r
-				}
-			},
-			41:{
-				title: "Increase again",
-				description(){return "Multiply the previous upgrade by 1.01. Currently: x"+format(this.effect)},
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,34)},
-				effect(){return new Decimal(1.01).pow(hasUpgrade("p",42)?layers.p.upgrades[42].effect:1).times(hasUpgrade("p",63)?2:1)}
-			},
-			42:{
-				title: "Increase again",
-				description(){return "Exponentiate the previous upgrade by 1.01. Currently: ^"+format(this.effect)},
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,41)},
-				effect(){return new Decimal(1.01).tetrate(hasUpgrade("p",43)?layers.p.upgrades[43].effect:1).times(hasUpgrade("p",63)?2:1).times(hasUpgrade("p",64)?2:1)}
-			},
-			43:{
-				title: "Increase again",
-				description(){return "Tetrate the previous upgrade by 1.01. Currently: ^^"+format(this.effect)},
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,42)},
-				effect(){return new Decimal(1.01).pentate(hasUpgrade("p",44)?layers.p.upgrades[44].effect:1).times(hasUpgrade("p",63)?2:1).times(hasUpgrade("p",64)?2:1)}
-			},
-			44:{
-				title: "Increase again",
-				description(){return "Pentate the previous upgrade by 1.01. Currently: ^^^"+format(this.effect)},
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,43)},
-				effect(){return new Decimal(1.01).times(hasUpgrade("p",63)?2:1).times(hasUpgrade("p",64)?2:1)}
-			},
-			51:{
-				title: "Challenging",
-				description: "This upgrade doesn't unlock a challenge",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,44)},
-			},
-			52:{
-				title: "Not challenging",
-				description: "This upgrade doesn't add 1 to the completion limit",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,51)},
-			},
-			53:{
-				title: "Not not challenging",
-				description: "This upgrade doesn't add 1 to the completion limit",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,52)},
-			},
-			54:{
-				title: "(not^3) challenging",
-				description: "Fix the bug where you can't buy upgrades when you have 1 prestige point",
-				cost(){return new Decimal(0.99999)},
-				unlocked(){return hasUpgrade(this.layer,53)},
-				onPurchase(){player.p.points=player.p.points.round()},
-			},
-			61:{
-				title: "(not^4) challenging",
-				description: "Doesn't unlock a second challenge",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,54)&&hasUpgrade(this.layer,53)},
-			},
-			62:{
-				title: "Infinity points",
-				description: "You can now complete Time Dilation 4 more times",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,61)},
-			},
-			63:{
-				title: "Eternity points",
-				description: "Double all fourth row upgrade effects",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,62)},
-			},
-			64:{
-				title: "Reality points",
-				description: "Previous upgrade, but only to the last 3 upgrades",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,63)},
-			},
-			71:{
-				title: "1",
-				description: "Add 1.1 to point gain, but reset all above upgrades",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,64)},
-				onPurchase(){if (!hasMilestone(this.layer,0)) player[this.layer].upgrades=[71]}
-			},
-			72:{
-				title: "2",
-				description: "Multiply point gain by 1.1, but reset all above upgrades",
-				cost(){return new Decimal(2)},
-				unlocked(){return hasUpgrade(this.layer,64)&&hasUpgrade(this.layer, this.id-1)},
-				onPurchase(){if (!hasMilestone(this.layer,1))player[this.layer].upgrades=[71,72]}
-			},
-			73:{
-				title: "3",
-				description: "Raise point gain by ^1.1, but reset all above upgrades",
-				cost(){return new Decimal(4)},
-				unlocked(){return hasUpgrade(this.layer,64)&&hasUpgrade(this.layer, this.id-1)},
-				onPurchase(){if (!hasMilestone(this.layer,1))player[this.layer].upgrades=[71,72,73]}
-			},
-			74:{
-				title: "4",
-				description: "Tetrate point gain by 1.1, but reset all above upgrades",
-				cost(){return new Decimal(8)},
-				unlocked(){return hasUpgrade(this.layer,64)&&hasUpgrade(this.layer, this.id-1)},
-				onPurchase(){if (!hasMilestone(this.layer,2))player[this.layer].upgrades=[71,72,73,74]
-					if (hasMilestone(this.layer,1)&&!hasMilestone(this.layer,2)) {player[this.layer].upgrades=[11,12,13,14,21,22,23,24,71,72,73,74]}}
-				},
-			81:{
-				title: "5",
-				description: "Generator efficiency is increased by 2",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,74)&&(player[this.layer].buyables[12].gt(0)||player[this.layer].buyables[21].gt(0))},
-			},
-			82:{
-				title: "6",
-				description: "Unlock another way to buy generators",
-				cost(){return new Decimal(1)},
-				unlocked(){return hasUpgrade(this.layer,81)&&(player[this.layer].buyables[12].gt(0)||player[this.layer].buyables[21].gt(0))},
-			},
-			83:{
-				title: "7",
-				description: "Generator efficiency is boosted by prestige points",
-				cost(){return new Decimal(3)},
-				unlocked(){return hasUpgrade(this.layer,82)},
-			},
-			84:{
-				title: "8",
-				description: "You can complete <b>Point</b> one more time",
-				cost(){return new Decimal(3)},
-				unlocked(){return hasUpgrade(this.layer,83)},
-			},
-			91:{
-				title: "9",
-				description: "New Challenge Time",
-				cost(){return new Decimal(20)},
-				unlocked(){return hasUpgrade(this.layer,84)&&new Decimal(player[this.layer].challenges[12]).gte(3)},
-			},
-			92:{
-				title: "10",
-				description: "Each of the first 50 generators adds 0.001 to <b>Increase</b>",
-				cost(){return new Decimal(5)},
-				unlocked(){return hasUpgrade(this.layer,91)&&hasChallenge(this.layer,21)},
-			},
-			93:{
-				title: "11",
-				description: "Change the tree trunk in generator effect to a hypertessaract root",
-				cost(){return new Decimal(7)},
-				unlocked(){return hasUpgrade(this.layer,92)},
-			},
-			94:{
-				title: "12",
-				description: "Unlock a clickable in generators",
-				cost(){return new Decimal(50)},
-				unlocked(){return hasUpgrade(this.layer,93)},
-			},
-			101:{
-				title: "10th row????",
-				description: "Decrease the dimensions of <b>11</b> by 2",
-				cost(){return new Decimal(10)},
-				unlocked(){return hasUpgrade(this.layer,94)},
-			},
-			102:{
-				title: "2 Tree Trunks",
-				description: "Double log of generator points adds to generator efficiency",
-				cost(){return new Decimal(25)},
-				unlocked(){return hasUpgrade(this.layer,101)},
-			},
-			103:{
-				title: "(not^5) challenging",
-				description: "Unlock the last challenge",
-				cost(){return new Decimal(103)},
-				unlocked(){return hasUpgrade(this.layer,102)},
-			},
-			104:{
-				title: "2 layers tree",
-				description: "Prestige points boost points, and unlock another tab",
-				cost(){return new Decimal(100)},
-				unlocked(){return hasUpgrade(this.layer,103)&&hasChallenge(this.layer,22)},
-			},
-			111:{
-				title: "not (hardcapped)",
-				description: "Remove the generator clickable hardcap, and you can only pick one upgrade on each row below this",
-				cost(){return new Decimal(110)},
-				unlocked(){return hasUpgrade(this.layer,104)&&hasMilestone(this.layer,6)},
-			},
-			112:{
-				title: "Respec button",
-				description: "Respec all lower upgrades, but you don't get points back",
-				cost(){return new Decimal(100)},
-				unlocked(){return hasUpgrade(this.layer,111)&&(hasUpgrade(this.layer,121)||hasUpgrade(this.layer,122))&&!hasMilestone("i",4)},
-				onPurchase(){
-					player.p.upgrades=player.p.upgrades.filter(function x(i){return i<112})
-				}
-			},
-			121:{
-				title: "Timers",
-				description: "Raise the <b>Time Dilation</b> reward effect to the 1.2",
-				cost(){return new Decimal(500)},
-				unlocked(){return hasUpgrade(this.layer,111)&&(!hasUpgrade(this.layer,122)||hasMilestone(this.layer,7))},
-			},
-			122:{
-				title: "Generators",
-				description: "Decrease the first generator buyable cost scaling base by 2",
-				cost(){return new Decimal(500)},
-				unlocked(){return hasUpgrade(this.layer,111)&&(!hasUpgrade(this.layer,121)||hasMilestone(this.layer,7))},
-			},
-			131:{
-				title: "Prestige",
-				description: "Gain 10x more prestige points",
-				cost(){return new Decimal(5000)},
-				unlocked(){return (hasUpgrade(this.layer,121)||hasUpgrade(this.layer,122))&&(!hasUpgrade(this.layer,132)||hasMilestone(this.layer,7))},
-			},
-			132:{
-				title: "One and a half",
-				description: "Raise generator effect to the 1.5",
-				cost(){return new Decimal(5000)},
-				unlocked(){return (hasUpgrade(this.layer,121)||hasUpgrade(this.layer,122))&&(!hasUpgrade(this.layer,131)||hasMilestone(this.layer,7))},
-			},
-
-			141:{
-				title: "Active",
-				description: "Multiply generator efficiency now increases by 1, but it doesn't automatically click.",
-				cost(){return new Decimal(50000)},
-				unlocked(){return (hasUpgrade(this.layer,131)||hasUpgrade(this.layer,132))&&((!hasUpgrade(this.layer,142)&&!hasUpgrade(this.layer,143))||hasMilestone("i",4))},
-			},
-			142:{
-				title: "Passive",
-				description: "Gain 5x more points",
-				cost(){return new Decimal(50000)},
-				unlocked(){return (hasUpgrade(this.layer,131)||hasUpgrade(this.layer,132))&&((!hasUpgrade(this.layer,141)&&!hasUpgrade(this.layer,143))||hasMilestone("i",4))},
-			},
-			143:{
-				title: "Idle",
-				description: "Hours played multiply generator power",
-				cost(){return new Decimal(50000)},
-				unlocked(){return (hasUpgrade(this.layer,131)||hasUpgrade(this.layer,132))&&((!hasUpgrade(this.layer,142)&&!hasUpgrade(this.layer,141))||hasMilestone("i",4))},
-			},
-			211:{
-				title: "Prestige",
-				description: "Pointy points multiply points",
-				cost(){return new Decimal(1)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades")},
-			},
-			212:{
-				title: "Pointy",
-				description: "Pointy prestige points reduce the cost scaling of pointy points",
-				cost(){return new Decimal(2)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,211))},
-			},
-			213:{
-				title: "Time",
-				description: "Generator power also multiplies point gain",
-				cost(){return new Decimal(6)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,212))},
-			},
-			214:{
-				title: "^0",
-				description: "Further reduce the pointy point scaling",
-				cost(){return new Decimal(11)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,213))},
-			},
-			215:{
-				title: "bulk",
-				description: "Auto-pointy points now buys 10 per tick",
-				cost(){return new Decimal(27)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,214))},
-			},
-			221:{
-				title: "^-1",
-				description: "^0 is even more powerful",
-				cost(){return new Decimal(28)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,215))},
-			},
-			222:{
-				title: "???",
-				description: "square <b>bulk</b> and pointy prestige points multiply prestige points",
-				cost(){return new Decimal(90)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,221))},
-			},
-			223:{
-				title: "more automation",
-				description: "Automatically gain pointy prestige points",
-				cost(){return new Decimal(96)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,222))},
-			},
-			224:{
-				title: "Generation",
-				description: "Generator costs are divided by generator effect",
-				cost(){return new Decimal(100)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,223))},
-			},
-			225:{
-				title: "Boosters",
-				description: "Unlock boosters (next update)",
-				cost(){return new Decimal(135)},
-				canAfford(){return getBuyableAmount(this.layer,22).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,22,getBuyableAmount(this.layer,22).sub(this.cost))},
-				unlocked(){return (hasMilestone("i",5)&&layers.p.activeSubtab!="Upgrades"&&hasUpgrade(this.layer,224))},
-			},
-			231:{
-				title: "Blue",
-				description: "The generator effect also affects prestige points",
-				cost(){return new Decimal(4)},
-				canAfford(){return getBuyableAmount(this.layer,23).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,23,getBuyableAmount(this.layer,23).sub(this.cost))},
-				unlocked(){return (layers.p.activeSubtab!="Upgrades"&&hasMilestone(this.layer,11))},
-				currencyDisplayName: "pointy boosters"
-			},
-			232:{
-				title: "Red",
-				description: "Unlock a third way to buy generators",
-				cost(){return new Decimal(5)},
-				canAfford(){return getBuyableAmount(this.layer,23).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,23,getBuyableAmount(this.layer,23).sub(this.cost))},
-				unlocked(){return (layers.p.activeSubtab!="Upgrades"&&hasMilestone(this.layer,12))},
-				currencyDisplayName: "pointy boosters"
-			},
-			233:{
-				title: "Green",
-				description: "Prestige points do not reset your pointy points and boosters don't reset generators",
-				cost(){return new Decimal(5)},
-				canAfford(){return getBuyableAmount(this.layer,23).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,23,getBuyableAmount(this.layer,23).sub(this.cost))},
-				unlocked(){return (layers.p.activeSubtab!="Upgrades"&&hasMilestone(this.layer,12))},
-				currencyDisplayName: "pointy boosters"
-			},
-			234:{
-				title: "Yellow",
-				description: "Divide the cost of the third generator buyable based on boosters",
-				cost(){return new Decimal(6)},
-				canAfford(){return getBuyableAmount(this.layer,23).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,23,getBuyableAmount(this.layer,23).sub(this.cost))},
-				unlocked(){return (layers.p.activeSubtab!="Upgrades"&&hasMilestone(this.layer,12))},
-				currencyDisplayName: "pointy boosters"
-			},
-			235:{
-				title: "Orange",
-				description: "Raise the Infinity effect to the 6.9420th power",
-				cost(){return new Decimal(8)},
-				canAfford(){return getBuyableAmount(this.layer,23).gte(this.cost)},
-				pay(){setBuyableAmount(this.layer,23,getBuyableAmount(this.layer,23).sub(this.cost))},
-				unlocked(){return (layers.p.activeSubtab!="Upgrades"&&hasMilestone(this.layer,12))},
-				currencyDisplayName: "pointy boosters"
-			},
-		},
-
-		clickables: {
-			rows: 1,
-			cols: 1,
-			11: {
-
-				display() {return "Multiply generator efficiency by "+format(player.p.cmult)+((player.p.cmult.min(100).eq(100)&&!hasUpgrade(this.layer,111))?" (hardcapped)":"")},
-				unlocked(){return hasUpgrade("p",94)},
-				click(){player.p.cmult=player.p.cmult.plus(hasUpgrade("p",141)?1:0.01)
-				if (!hasUpgrade(this.layer,111))player.p.cmult=player.p.cmult.min(100)
-			},
-		canClick(){return player.p.cmult.lt(100)||hasUpgrade(this.layer,111)},
-	},
-
-},
-
-challenges:{
-	rows: 99,
-	cols: 2,
-	11:{
-		name: "Time dilation",
-		challengeDescription(){return "Point gain exponent is raised to the ^0.75"},
-		goal(){return new Decimal(100).times(new Decimal(10).pow(new Decimal(player[this.layer].challenges[this.id]).times(new Decimal(1).sub(new Decimal(layers[this.layer].challenges[12].effect).div(100))).pow(2)))},
-		rewardDescription(){return "You have completed this challenge "+player[this.layer].challenges[this.id]+"/"+this.completionLimit+" times. Multiply <b>Increase</b>'s effect by challenge completions+1. Currently: x"+format(new Decimal(player[this.layer].challenges[this.id]).add(1).pow(hasUpgrade(this.layer,121)?1.2:1))},
-		unlocked(){return hasUpgrade("p",51)||hasChallenge(this.layer,this.id)},
-		completionLimit(){
-			if (hasUpgrade("p",62))return 7
-				if (hasUpgrade("p",53))return 3
-					if (hasUpgrade("p",52))return 2
-						return 1
-
-				}
-			},
-			12:{
-				name: "Point",
-				challengeDescription: "Points are pointed",
-				goal(){return new Decimal(100)},
-				rewardDescription(){return "You have completed this challenge "+player[this.layer].challenges[this.id]+"/"+this.completionLimit+" times, making previous challenge goal scale "+(layers[this.layer].challenges[this.id].effect)+"% slower."},
-				unlocked(){return hasUpgrade("p",61)||hasChallenge(this.layer,this.id)},
-				effect(){
-					if (!hasChallenge(this.layer,this.id)) return 0
-						if (player[this.layer].challenges[this.id]==1) return 50
-							if (player[this.layer].challenges[this.id]==2) return 60
-								if (player[this.layer].challenges[this.id]==3) return 70
-							},
-						completionLimit(){
-							let l=new Decimal(1)
-							if (hasUpgrade("p",84)) l=l.plus(1)
-								if (hasMilestone("p",3))l=l.plus(1)
-									return l
-
-							}
-						},
-						21:{
-							name: "Time Points",
-							challengeDescription: "You are stuck in all above challenges",
-							goal(){return new Decimal(308.25)},
-							rewardDescription(){return "Lower the first generator buyable cost base by 6"},
-							unlocked(){return hasUpgrade("p",91)||hasChallenge(this.layer,this.id)},
-						},
-						22:{
-							name: "Last Challenge",
-							challengeDescription: "Generator points do nothing",
-							goal(){return new Decimal(9999)},
-							rewardDescription(){return "Autoclick the clickable and reduce <b>2 Tree Trunks</b> by 1"},
-							unlocked(){return hasUpgrade("p",103)||hasChallenge(this.layer,this.id)},
-						}
-					},
-					buyables: {
-						rows: 99,
-						cols: 4,
-						11: {
-							cost() { return new Decimal(0)},
-							display() { return "Reset all upgrades and challenges, but get a boost. You have reset "+getBuyableAmount(this.layer,this.id)+" times.<br>"+(getBuyableAmount(this.layer,this.id).eq(6)?"You can't buy more than 6 boosts!":"You need all upgrades to reset.") },
-							canAfford() { return (player[this.layer].points.gte(this.cost)&&hasUpgrade(this.layer,74)&&hasUpgrade(this.layer,64))&&getBuyableAmount(this.layer,this.id).lt(6) },
-							buy() {
-								player[this.layer].points = player[this.layer].points.sub(this.cost)
-								setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-								player[this.layer].points=new Decimal(0)
-								player[this.layer].upgrades=[]
-								if (hasMilestone(this.layer,1))player[this.layer].upgrades=[11,12,13,14,21,22,23,24]
-									if (hasMilestone(this.layer,3))player[this.layer].upgrades=[11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44,51,52,53,54,61,62,63,64]
-										if (!hasMilestone(this.layer,2)){
-											for (let c in layers[this.layer].challenges){
-												player[this.layer].challenges[c]=new Decimal(0)
-											}
-										}
-									},
-									unlocked(){return (hasUpgrade(this.layer,74)&&hasUpgrade(this.layer,64))||hasMilestone(this.layer,0)}
-								},
-								12: {
-									cost() { return new Decimal(1).times(new Decimal(hasChallenge(this.layer,21)?4:10).sub(hasUpgrade(this.layer,122)?2:0).pow(player.p.buyables[this.id])).div(hasUpgrade(this.layer,224)?(hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10()):1)},
-									display() { return "Buy a generator for "+format(this.cost)+" points" },
-									canAfford() { return (player.points.gte(this.cost)&&hasMilestone(this.layer,5)) },
-									buy() {
-										if (!hasMilestone("p",13))player.points = player.points.sub(this.cost)
-											setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-										player[this.layer].g=player[this.layer].g.plus(1)
-									},
-									unlocked(){return (hasMilestone(this.layer,5))}
-								},
-								13: {
-									cost() { return new Decimal(1).times(new Decimal(2).pow(player.p.buyables[this.id])).div(hasUpgrade(this.layer,224)?(hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10()):1)},
-									display() { return "Buy a generator for "+format(this.cost)+" prestige points" },
-									canAfford() { return (player.p.points.gte(this.cost)&&hasUpgrade("p",82)) },
-									buy() {
-										if (!hasMilestone("p",13))player.p.points = player.p.points.sub(this.cost)
-											setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-										player[this.layer].g=player[this.layer].g.plus(1)
-									},
-									unlocked(){return (hasUpgrade(this.layer,82))}
-								},
-								14: {
-									cost() { return new Decimal(900).mul(new Decimal(1.01).pow(getBuyableAmount(this.layer,this.id))).round().div(hasUpgrade(this.layer,234)?getBuyableAmount(this.layer,23).pow(0.3).plus(1):1)},
-									display() { return "Buy a generator for "+format(this.cost)+" Infinity points" },
-									canAfford() { return (player.i.points.gte(this.cost)&&hasUpgrade("p",232)) },
-									buy() {
-										if (!hasMilestone("p",13))player.i.points = player.i.points.sub(this.cost).round()
-											setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-										player[this.layer].g=player[this.layer].g.plus(1)
-									},
-									unlocked(){return (hasUpgrade(this.layer,232))}
-								},
-								21: {
-									cost() { return new Decimal(20).plus(getBuyableAmount(this.layer, this.id).pow(new Decimal(2).sub(new Decimal(hasUpgrade(this.layer,221)?0.9:hasUpgrade(this.layer,214)?0.6:0.3).times(hasUpgrade(this.layer,212)?(new Decimal(1).sub(new Decimal(0.75).pow(getBuyableAmount(this.layer,22)))):0))))},
-									display() { return "Reset your generators for +1 pointy point! Cost: "+format(this.cost)+" Generators" },
-									canAfford() { return (player.p.g.gte(this.cost)&&hasUpgrade("p",104)) },
-									buy() {
-										if (!hasMilestone("i",1))player.p.g = new Decimal(0)
-											setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-										if (!hasMilestone("i",1))setBuyableAmount(this.layer,12, new Decimal(0))
-											if (!hasMilestone("i",1))setBuyableAmount(this.layer,13, new Decimal(0))
-												if (!hasMilestone("i",1))player.p.gp=new Decimal(0)
-											},
-										unlocked(){return (hasUpgrade(this.layer,104))}
-									},
-									22: {
-										cost() { return new Decimal(8).plus(getBuyableAmount(this.layer,this.id))},
-										display() { return "Gain a pointy prestige point. Cost: "+format(this.cost)+" Pointy Points" },
-										canAfford() { return (getBuyableAmount(this.layer,21).gte(this.cost)&&(hasMilestone("i",5))) },
-										buy() {
-											if (!hasUpgrade(this.layer,233))setBuyableAmount(this.layer,21, getBuyableAmount(this.layer,21).sub(this.cost))
-												setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-										},
-										unlocked(){return (hasMilestone("i",5))}
-									},
-									23: {
-										cost() { return new Decimal(124).plus(getBuyableAmount(this.layer,this.id).times(2).pow(2))},
-										display() { return "Gain a booster. Cost: "+format(this.cost)+" Pointy Points" },
-										canAfford() { return (getBuyableAmount(this.layer,21).gte(this.cost)&&(hasMilestone("i",5))) },
-										buy() {
-											if (!hasMilestone(this.layer,15))setBuyableAmount(this.layer,21, getBuyableAmount(this.layer,21).sub(this.cost))
-												setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-											if (!hasMilestone(this.layer,15)){
-												if (!hasMilestone(this.layer,12)){player.p.upgrades=player.p.upgrades.filter((x)=>{return (x<200||x>230)})
-													if (hasMilestone(this.layer,11)){player.p.upgrades.push(215);player.p.upgrades.push(225);player.p.upgrades.push(223);player.p.upgrades.push(222)}}
-												setBuyableAmount("p",21,new Decimal(0))
-												setBuyableAmount("p",22,new Decimal(0))
-												if (!hasUpgrade("p",233)){
-													setBuyableAmount("p",12,new Decimal(0))
-													setBuyableAmount("p",13,new Decimal(0))
-													setBuyableAmount("p",14,new Decimal(0))
-
-													player.p.g = new Decimal(0)}
-													player.p.gp=new Decimal(0)}
-												},
-												unlocked(){return (hasUpgrade("p",225)||getBuyableAmount("p",23).gt(0))}
-											},
-											31: {
-												cost() { return new Decimal(1e93).times(new Decimal(1.5).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(1.1).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-												effect(){return new Decimal(2).plus(layers.p.buyables[33].effect).pow(getBuyableAmount(this.layer,this.id).plus(layers.p.buyables[51].effect))},
-												display() { return "Double point gain. \nCurrently: x"+format(this.effect)+"\nCost: "+format(this.cost)+" Prestige points" },
-												canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-												buy() {
-													player.p.points=player.p.points.sub(this.cost)
-													setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-												},
-												unlocked(){return (hasMilestone("p",13))}
-											},
-											32: {
-												cost() { return new Decimal(1e95).times(new Decimal(2).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(1.01).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-												display() { return "Double prestige point gain. \nCurrently: x"+format(new Decimal(2).plus(layers.p.buyables[33].effect).pow(getBuyableAmount(this.layer,this.id)))+"\nCost: "+format(this.cost)+" Prestige points" },
-												canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-												buy() {
-													player.p.points=player.p.points.sub(this.cost)
-													setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-												},
-												unlocked(){return (hasMilestone("p",13)&&getBuyableAmount(this.layer,31).gte(5))}
-											},
-											33: {
-												cost() { return new Decimal(1e100).times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(1.01).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-												effect(){return new Decimal(0.01).mul(getBuyableAmount(this.layer,this.id)).times(layers.p.buyables[43].effect)},
-												display() { return "Add 0.01 to the previous 2 buyable bases. \nCurrently: +"+format(this.effect)+"\nCost: "+format(this.cost)+" Prestige points" },
-												canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-												buy() {
-													player.p.points=player.p.points.sub(this.cost)
-													setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-												},
-												unlocked(){return (hasMilestone("p",13)&&(getBuyableAmount(this.layer,this.id).gt(0)||player.p.points.gte(1e100)))}
-											},
-											41: {
-												cost() { return new Decimal(1e110).times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-												effect(){return new Decimal(0.01).mul(getBuyableAmount(this.layer,this.id).plus(layers.p.buyables[51].effect))},
-												display() { return "Add 0.01 to the booster effect base. \nCurrently: +"+format(this.effect)+"\nCost: "+format(this.cost)+" Prestige points" },
-												canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-												buy() {
-													player.p.points=player.p.points.sub(this.cost)
-													setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-												},
-												unlocked(){return (hasMilestone("p",13)&&(getBuyableAmount(this.layer,this.id).gt(0)||player.p.points.gte(1e110)))}
-											},
-											42: {
-												cost() { let c =  new Decimal(1e270).times(new Decimal(2).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(1.01).pow(getBuyableAmount(this.layer,this.id).pow(2)))
-
-													return c
-												},
-												effect(){
-													let f= new Decimal(1.001).pow(getBuyableAmount(this.layer,this.id))
-													if (f.gte(1.1))f=f.pow(0.8).times(new Decimal(1.1).pow(0.2))
-														if (f.gte(1.35))f=f.pow(0.5).times(new Decimal(1.35).pow(0.5))
-															if (f.gte(3))f=new Decimal(3)
-																return f
-														},
-														display() { return "Raise point gain to the 1.001 \nCurrently: ^"+format(this.effect)+(this.effect.eq(3)?"(hardcapped)":"")+"\nCost: "+format(this.cost)+" Prestige points" },
-														canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13)))&&this.effect.lt(3) },
-														buy() {
-															player.p.points=player.p.points.sub(this.cost)
-															setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-														},
-														unlocked(){return (hasMilestone("p",13)&&(getBuyableAmount(this.layer,this.id).gt(0)||player.p.points.gte(1e270)))}
-													},
-													43: {
-														cost() { return new Decimal("1e375").times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-														effect(){return new Decimal(0.01).mul(getBuyableAmount(this.layer,this.id)).plus(1)},
-														display() { return "Multiply the above buyable effect. \nCurrently: *"+format(this.effect)+"\nCost: "+format(this.cost)+" Prestige points" },
-														canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-														buy() {
-															player.p.points=player.p.points.sub(this.cost)
-															setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-														},
-														unlocked(){return (hasMilestone("p",13)&&(getBuyableAmount(this.layer,this.id).gt(0)||player.p.points.gte("1e375")))}
-													},
-													51: {
-														cost() { return new Decimal("1e1740").times(new Decimal(10).pow(getBuyableAmount(this.layer,this.id))).times(new Decimal(1e10).pow(getBuyableAmount(this.layer,this.id).pow(2)))},
-														effect(){return getBuyableAmount(this.layer,this.id).pow(0.55)},
-														display() { return "Add free levels to the above 2 buyables \nCurrently: "+format(this.effect)+"\nCost: "+format(this.cost)+" Prestige points" },
-														canAfford() { return (player.p.points.gte(this.cost)&&(hasMilestone("p",13))) },
-														buy() {
-															player.p.points=player.p.points.sub(this.cost)
-															setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
-														},
-														unlocked(){return (hasMilestone("p",15)&&(getBuyableAmount(this.layer,this.id).gt(0)||player.p.points.gte("1e1700")))}
-													},
-												},
-												milestones: {
-													0: {
-														requirementDisplay: "1 reset",
-														effectDisplay: "Add 0.01 to base point gain and prestige requirement, and <b>1</b> doesn't reset upgrades",
-														done() { return getBuyableAmount("p",11).gte(1) },
-														unlocked(){return layers.p.activeSubtab!="Pointy points"}
-													},
-													1: {
-														requirementDisplay: "2 resets",
-														effectDisplay: "<div><b>2</b> and <b>3</b> don't reset upgrades, and start with the first 8 upgrades on reset</div>",
-														done() { return getBuyableAmount("p",11).gte(2) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													2: {
-														requirementDisplay: "3 resets",
-														effectDisplay: "<div><b>4</b> doesn't reset upgrades, and permanently fix the bug where you can't buy upgrades when you have 1 prestige point</div>",
-														done() { return getBuyableAmount("p",11).gte(3) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													3: {
-														requirementDisplay: "4 resets",
-														effectDisplay: "Don't reset challenges, add 1 to <b>Point</b> maximum completions, and start with 24 upgrades",
-														done() { return getBuyableAmount("p",11).gte(4) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													4: {
-														requirementDisplay: "5 resets",
-														effectDisplay: "Each useless upgrade adds 0.1 to base point gain",
-														done() { return getBuyableAmount("p",11).gte(5) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													5: {
-														requirementDisplay: "6 resets",
-														effectDisplay: "Unlock something",
-														done() { return getBuyableAmount("p",11).gte(6) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													6: {
-														requirementDisplay: "1 pointy point",
-														effectDisplay: "Unlock the upgrade tree",
-														done() { return getBuyableAmount("p",21).gte(1) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&(hasUpgrade(this.layer,104)||player.i.unlocked)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													7: {
-														requirementDisplay: "7 pointy points",
-														effectDisplay: "You can now buy both first and second row upgrade tree upgrades",
-														done() { return getBuyableAmount("p",21).gte(7) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&(hasUpgrade(this.layer,111)||player.i.unlocked)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													8: {
-														requirementDisplay: "8 pointy points",
-														effectDisplay: "Unlock another layer",
-														done() { return getBuyableAmount("p",21).gte(8) },
-														unlocked(){return hasMilestone(this.layer,this.id-1)&&(hasUpgrade(this.layer,141)||hasUpgrade(this.layer,143)||hasUpgrade(this.layer,142)||player.i.unlocked)&&layers.p.activeSubtab!="Pointy points"}
-													},
-													11: {
-														requirementDisplay: "3 boosters",
-														effectDisplay: "Keep automation on booster reset",
-														done() { return getBuyableAmount("p",23).gte(3) },
-														unlocked(){return (getBuyableAmount("p",23).gt(0)||hasMilestone(this.layer,this.id))}
-													},
-													12: {
-														requirementDisplay: "5 boosters",
-														effectDisplay: "Keep all prestige upgrades on booster reset and buy max infinity points",
-														done() { return getBuyableAmount("p",23).gte(5) },
-														unlocked(){return (getBuyableAmount("p",23).gt(0)||hasMilestone(this.layer,this.id))}
-													},
-													13: {
-														requirementDisplay: "10 boosters",
-														effectDisplay: "Generators cost nothing",
-														done() { return getBuyableAmount("p",23).gte(10) },
-														unlocked(){return (getBuyableAmount("p",23).gt(0)||hasMilestone(this.layer,this.id))}
-													},
-													14: {
-														requirementDisplay: "15 boosters",
-														effectDisplay: "Auto buy the first 3 buyables and buy max pointy prestige points",
-														done() { return getBuyableAmount("p",23).gte(15) },
-														unlocked(){return (getBuyableAmount("p",41).gt(0)||hasMilestone(this.layer,this.id))}
-													},
-													15: {
-														requirementDisplay: "20 boosters",
-														effectDisplay: "Boosters reset nothing and auto booster",
-														done() { return getBuyableAmount("p",23).gte(16) },
-														unlocked(){return (getBuyableAmount("p",41).gt(0)||hasMilestone(this.layer,this.id))}
-													},
-												},
-												passiveGeneration(){return (hasUpgrade("i",11)?1:0)},
-												update(diff){
-													if (hasMilestone(this.layer,2)&&!hasUpgrade(this.layer,54)){
-														player[this.layer].upgrades.push(54)
-													}
-													if (hasMilestone(this.layer,1)&&!hasUpgrade(this.layer,11)&&!hasMilestone(this.layer,3)){
-														player[this.layer].upgrades=[11,12,13,14,21,22,23,24]
-													}
-													if (hasMilestone(this.layer,3)&&!hasUpgrade(this.layer,31)){
-														player[this.layer].upgrades=[11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44,51,52,53,54,61,62,63,64]
-													}
-													if (hasMilestone(this.layer,5)){
-														player[this.layer].gp=player[this.layer].gp.plus(player.p.g.times(diff).times(player.p.geff))
-													}
-													let geff=new Decimal(1)
-													if (hasUpgrade("p",81)) geff=geff.plus(2)
-														if (hasUpgrade("p",102)) geff=geff.plus(hasChallenge("p",22)?player.p.gp.plus(1).log(10):player.p.gp.plus(1).log(10).plus(1).log(10))
-															if (hasUpgrade("p",83)) geff=geff.times(player.p.points.plus(1).log(10).plus(1))
-																if (hasUpgrade("p",94)) geff=geff.times(player.p.cmult)
-																	if (hasUpgrade("p",104)) geff=geff.times(new Decimal(player.p.buyables[21]).plus(1))
-																		if (hasUpgrade("p",143)) geff=geff.times(new Decimal(player.timePlayed).div(3600).max(1))
-																			if (hasUpgrade("p",225)) geff=geff.pow(new Decimal(player.p.buyables[23]).div(10).mul(new Decimal(0.1).plus(layers.p.buyables[41].effect).times(10)).plus(1))
-																				player.p.geff=geff
-																			if (hasChallenge("p",22)&&(!hasUpgrade("p",141)||hasUpgrade("i",12)))player.p.cmult=player.p.cmult.plus(hasUpgrade("p",141)?1:0.01)
-																				if (!hasUpgrade("p",111)) player.p.cmult=player.p.cmult.min(100)
-																					if (hasMilestone(this.layer,14)){
-																						if (layers.p.buyables[31].canAfford())layers.p.buyables[31].buy()
-																							if (layers.p.buyables[32].canAfford())layers.p.buyables[32].buy()
-																								if (layers.p.buyables[33].canAfford())layers.p.buyables[33].buy()
-																							}
-																						if (hasMilestone(this.layer,15)){
-																							if (layers.p.buyables[23].canAfford())layers.p.buyables[23].buy()
-																						}
-																				},
-																				subtabs: {
-																					"Upgrades": {
-																						display: `
-																							<main-display />
-																							<spacer />
-																							<prestige-button display="" />
-																							<spacer />
-																							<spacer />
-																							<upgrades />`
-																					},
-																					"Challenges": {
-																						unlocked() { return hasUpgrade("p", 51) || hasMilestone("p", 0); },
-																						display: `
-																							<spacer />
-																							<spacer />
-																							<challenges />`
-																					},
-																					"Buyables and Milestones": {
-																						unlocked(){return hasUpgrade("p",74)||hasMilestone("p",0)},
-																						display: `
-																							<spacer />
-																							<spacer />
-																							<row><buyable id="11" /></row>
-																							<spacer />
-																							<div v-if="hasMilestone('p', 0)">Your boosts are making the point challenge {{ getBuyableAmount('p', 11).plus(1) }}x less pointy</div>
-																							<spacer />
-																							<milestones />`
-																					},
-																					"Generators": {
-																						unlocked(){return hasMilestone("p",5)||player.i.points.gte(1)},
-																						display: `
-																							<spacer />
-																							<div>You have {{ format(player.p.gp) }} generator points, adding {{ format(hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10()) }} to point gain</div>
-																							<div>You have {{ format(player.p.g) }} generators, generating {{ format(player.p.g.times(player.p.geff)) }} generator points per second</div>
-																							<div>Generator efficiency is {{ format(player.p.geff) }}</div>
-																							<spacer />
-																							<spacer />
-																							<buyables :buyables="[12, 13, 14]" />
-																							<row><clickable id="11" /></row>`
-																					},
-																					"Pointy Points": {
-																						unlocked(){return hasUpgrade("p",104)||player.i.points.gte(1)},
-																						display: `
-																							<div style="color: red; font-size: 32px; font-family: Comic Sans MS">{{ format(player.p.buyables[21]) }} pointy points</div>
-																							<div style="color: red; font-size: 32px; font-family: Comic Sans MS">My pointy points are multiplying generator efficiency by {{ format(new Decimal(player.p.buyables[21]).plus(1)) }}</div>
-																							<spacer />
-																							<spacer />
-																							<row><buyable id="21" /></row>
-																							<div v-if="hasMilestone('i', 5)" style="color: red; font-size: 32px; font-family: Comic Sans MS">I have {{ format(player.p.buyables[22]) }} pointy prestige points</div>
-																							<row><buyable id="22" /></row>
-																							<spacer />
-																							<upgrades :upgrades="[211, 212, 213, 214, 215]" />
-																							<upgrades :upgrades="[221, 222, 223, 224, 225]" />
-																							<div v-if="hasMilestone('p', 225)" style="color: red; font-size: 32px; font-family: Comic Sans MS">I have {{ format(player.p.buyables[23]) }} pointy boosters!</div>
-																							<row><buyable id="23" /></row>
-																							<div v-if="hasMilestone('p', 225) || getBuyableAmount('p', 23).gt(0)" style="color: red; font-size: 32px; font-family: Comic Sans MS">My pointy boosters are raising generator efficiency to the ^{{ format(new Decimal(player.p.buyables[23]).div(10).mul(new Decimal(0.1).plus(layers.p.buyables[41].effect).times(10)).plus(1)) }}</div>
-																							<spacer />
-																							<spacer />
-																							<div v-if="hasMilestone('p', 11)" style="font-size: 24px">Booster upgrades</div>
-																							<upgrades :upgrades="[231, 232, 233, 234, 235]" />`
-																					},
-																					"Buyables": {
-																						unlocked(){return hasMilestone("p",13)},
-																						display: `
-																							<buyables :buyables="[31, 32, 33]" />
-																							<buyables :buyables="[41, 42, 43]" />`
-																					}
-																				}
-}
\ No newline at end of file
diff --git a/src/data/layers/demo.ts b/src/data/layers/demo.ts
new file mode 100644
index 0000000..329ac6f
--- /dev/null
+++ b/src/data/layers/demo.ts
@@ -0,0 +1,2301 @@
+/* eslint-disable */
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { DecimalSource } from "@/lib/break_eternity";
+import { RawLayer } from "@/typings/layer";
+import Decimal, { format } from "@/util/bignum";
+import {
+  getBuyableAmount, hasChallenge, hasMilestone, hasUpgrade, setBuyableAmount
+} from "@/util/features";
+import { resetLayer } from "@/util/layers";
+
+export default {
+  id: "p",
+  position: 2,
+  startData() {
+    return {
+      unlocked: true,
+      points: new Decimal(0),
+      gp: new Decimal(0),
+      g: new Decimal(0),
+      geff: new Decimal(1),
+      cmult: new Decimal(1)
+    };
+  },
+  color: "#4BDC13",
+  requires() {
+    let require = new Decimal(68.99);
+    if (hasMilestone(this.layer, 0)) require = require.plus(0.01);
+    if (hasUpgrade(this.layer, 21))
+      require = require.tetrate(
+        hasUpgrade("p", 34)
+          ? new Decimal(1)
+              .div(
+                new Decimal(1).plus(
+                  layers.p.upgrades!.data[34].effect as Decimal
+                )
+              )
+              .toNumber()
+          : 1
+      );
+    if (hasUpgrade(this.layer, 22))
+      require = require.pow(
+        hasUpgrade("p", 34)
+          ? new Decimal(1).div(
+              new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+            )
+          : 1
+      );
+    if (hasUpgrade(this.layer, 23))
+      require = require.div(
+        hasUpgrade("p", 34)
+          ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+          : 1
+      );
+    if (hasUpgrade(this.layer, 24))
+      require = require.sub(
+        hasUpgrade("p", 34)
+          ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+          : 1
+      );
+    return require.max(1);
+  },
+  resource: "prestige points",
+  baseResource: "points",
+  baseAmount() {
+    return player.points;
+  }, // Get the current amount of baseResource
+  type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have
+  exponent: 0.5, // Prestige currency exponent
+  gainMult() {
+    // Calculate the multiplier for main currency from bonuses
+    let mult = new Decimal(1);
+    if (hasUpgrade(this.layer, 131)) mult = mult.times(10);
+    if (player.layers.i.unlocked)
+      mult = mult.times(
+        player.layers.i.points.plus(1).pow(hasUpgrade("p", 235) ? 6.942 : 1)
+      );
+    if (hasUpgrade(this.layer, 222))
+      mult = mult.times(getBuyableAmount(this.layer, 22)!.plus(1));
+    if (hasUpgrade("p", 231)) {
+      const asdf = hasUpgrade("p", 132)
+        ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(2))
+        : hasUpgrade("p", 101)
+        ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(3))
+        : hasUpgrade("p", 93)
+        ? (player.layers.p.gp as Decimal).plus(1).pow(0.2)
+        : (player.layers.p.gp as Decimal).plus(1).log10();
+      mult = mult.mul(asdf.plus(1));
+    }
+    if (hasMilestone(this.layer, 13))
+      mult = mult.mul(
+        new Decimal(2)
+          .plus(layers.p.buyables!.data[33].effect as Decimal)
+          .pow(getBuyableAmount(this.layer, 32)!)
+      );
+    return mult;
+  },
+  gainExp() {
+    // Calculate the exponent on main currency from bonuses
+    return new Decimal(1);
+  },
+  row: 0, // Row the layer is in on the tree (0 is the first row)
+  hotkeys: [
+    {
+      key: "p",
+      description: "P: Reset for prestige points",
+      press() {
+        if (layers.p.canReset) resetLayer(this.layer);
+      }
+    }
+  ],
+  layerShown() {
+    return true;
+  },
+  upgrades: {
+    rows: 999,
+    cols: 5,
+    data: {
+      11: {
+        title: "Gain points",
+        description: "Point generation is increased by 1",
+        cost() {
+          if (hasMilestone(this.layer, 2)) return new Decimal(1);
+          return new Decimal(1.00001);
+        },
+        unlocked() {
+          return true;
+        }
+      },
+      12: {
+        title: "Gain more points",
+        description: "Point generation is singled",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 11);
+        }
+      },
+      13: {
+        title: "Gain more points",
+        description: "Point generation is lined",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 12);
+        }
+      },
+      14: {
+        title: "Gain more points",
+        description: "Point generation is tetrated by 1",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 13);
+        }
+      },
+      21: {
+        title: "Lower prestige requirement",
+        description: "Prestige point requirement is superrooted by 1",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 14);
+        }
+      },
+      22: {
+        title: "Lower prestige requirement more",
+        description: "Prestige point requirement is line rooted",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 21);
+        }
+      },
+      23: {
+        title: "Lower prestige requirement more",
+        description: "Prestige point requirement is wholed",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 22);
+        }
+      },
+      24: {
+        title: "Lower prestige requirement more",
+        description: "Prestige point requirement is decreased by 1",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 23);
+        }
+      },
+      31: {
+        title: "Unlock",
+        description: "Unlock an upgrade",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 24);
+        }
+      },
+      32: {
+        title: "An",
+        description: "Unlock an upgrade",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 31);
+        }
+      },
+      33: {
+        title: "Upgrade",
+        description: "Unlock an upgrade",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 32);
+        }
+      },
+      34: {
+        title: "Increase",
+        description() {
+          return (
+            "Add 0.01 to all above upgrades. Currently: +" +
+            format(this.effect as Decimal)
+          );
+        },
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 33);
+        },
+        effect() {
+          let r = hasUpgrade("p", 41)
+            ? new Decimal(0.01).times(layers.p.upgrades!.data[41].effect as Decimal)
+            : new Decimal(0.01);
+          r = r.times(
+            new Decimal(1).plus(
+              new Decimal(player.layers[this.layer].challenges![11])
+                .add(1)
+                .pow(hasUpgrade(this.layer, 121) ? 1.2 : 1)
+            )
+          );
+          if (hasUpgrade(this.layer, 92))
+            r = r.plus(
+              new Decimal(0.001)
+                .times((player.layers[this.layer].g as Decimal).plus(1))
+                .min(0.05)
+            );
+          return r;
+        }
+      },
+      41: {
+        title: "Increase again",
+        description() {
+          return (
+            "Multiply the previous upgrade by 1.01. Currently: x" +
+            format(this.effect as Decimal)
+          );
+        },
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 34);
+        },
+        effect() {
+          return new Decimal(1.01)
+            .pow(hasUpgrade("p", 42) ? layers.p.upgrades!.data[42].effect as Decimal : 1)
+            .times(hasUpgrade("p", 63) ? 2 : 1);
+        }
+      },
+      42: {
+        title: "Increase again",
+        description() {
+          return (
+            "Exponentiate the previous upgrade by 1.01. Currently: ^" +
+            format(this.effect as Decimal)
+          );
+        },
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 41);
+        },
+        effect() {
+          return new Decimal(1.01)
+            .tetrate(hasUpgrade("p", 43) ? (layers.p.upgrades!.data[43].effect as Decimal).toNumber() : 1)
+            .times(hasUpgrade("p", 63) ? 2 : 1)
+            .times(hasUpgrade("p", 64) ? 2 : 1);
+        }
+      },
+      43: {
+        title: "Increase again",
+        description() {
+          return (
+            "Tetrate the previous upgrade by 1.01. Currently: ^^" +
+            format(this.effect as Decimal)
+          );
+        },
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 42);
+        },
+        effect() {
+          return new Decimal(1.01)
+            .pentate(hasUpgrade("p", 44) ? (layers.p.upgrades!.data[44].effect as Decimal).toNumber() : 1)
+            .times(hasUpgrade("p", 63) ? 2 : 1)
+            .times(hasUpgrade("p", 64) ? 2 : 1);
+        }
+      },
+      44: {
+        title: "Increase again",
+        description() {
+          return (
+            "Pentate the previous upgrade by 1.01. Currently: ^^^" +
+            format(this.effect as Decimal)
+          );
+        },
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 43);
+        },
+        effect() {
+          return new Decimal(1.01)
+            .times(hasUpgrade("p", 63) ? 2 : 1)
+            .times(hasUpgrade("p", 64) ? 2 : 1);
+        }
+      },
+      51: {
+        title: "Challenging",
+        description: "This upgrade doesn't unlock a challenge",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 44);
+        }
+      },
+      52: {
+        title: "Not challenging",
+        description: "This upgrade doesn't add 1 to the completion limit",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 51);
+        }
+      },
+      53: {
+        title: "Not not challenging",
+        description: "This upgrade doesn't add 1 to the completion limit",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 52);
+        }
+      },
+      54: {
+        title: "(not^3) challenging",
+        description:
+          "Fix the bug where you can't buy upgrades when you have 1 prestige point",
+        cost() {
+          return new Decimal(0.99999);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 53);
+        },
+        onPurchase() {
+          player.layers.p.points = player.layers.p.points.round();
+        }
+      },
+      61: {
+        title: "(not^4) challenging",
+        description: "Doesn't unlock a second challenge",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 54) && hasUpgrade(this.layer, 53);
+        }
+      },
+      62: {
+        title: "Infinity points",
+        description: "You can now complete Time Dilation 4 more times",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 61);
+        }
+      },
+      63: {
+        title: "Eternity points",
+        description: "Double all fourth row upgrade effects",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 62);
+        }
+      },
+      64: {
+        title: "Reality points",
+        description: "Previous upgrade, but only to the last 3 upgrades",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 63);
+        }
+      },
+      71: {
+        title: "1",
+        description: "Add 1.1 to point gain, but reset all above upgrades",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 64);
+        },
+        onPurchase() {
+          if (!hasMilestone(this.layer, 0))
+            player.layers[this.layer].upgrades = [71];
+        }
+      },
+      72: {
+        title: "2",
+        description: "Multiply point gain by 1.1, but reset all above upgrades",
+        cost() {
+          return new Decimal(2);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 64) &&
+            hasUpgrade(this.layer, Number(this.id) - 1)
+          );
+        },
+        onPurchase() {
+          if (!hasMilestone(this.layer, 1))
+            player.layers[this.layer].upgrades = [71, 72];
+        }
+      },
+      73: {
+        title: "3",
+        description: "Raise point gain by ^1.1, but reset all above upgrades",
+        cost() {
+          return new Decimal(4);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 64) &&
+            hasUpgrade(this.layer, Number(this.id) - 1)
+          );
+        },
+        onPurchase() {
+          if (!hasMilestone(this.layer, 1))
+            player.layers[this.layer].upgrades = [71, 72, 73];
+        }
+      },
+      74: {
+        title: "4",
+        description: "Tetrate point gain by 1.1, but reset all above upgrades",
+        cost() {
+          return new Decimal(8);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 64) &&
+            hasUpgrade(this.layer, Number(this.id) - 1)
+          );
+        },
+        onPurchase() {
+          if (!hasMilestone(this.layer, 2))
+            player.layers[this.layer].upgrades = [71, 72, 73, 74];
+          if (hasMilestone(this.layer, 1) && !hasMilestone(this.layer, 2)) {
+            player.layers[this.layer].upgrades = [
+              11,
+              12,
+              13,
+              14,
+              21,
+              22,
+              23,
+              24,
+              71,
+              72,
+              73,
+              74
+            ];
+          }
+        }
+      },
+      81: {
+        title: "5",
+        description: "Generator efficiency is increased by 2",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 74) &&
+            (player.layers[this.layer].buyables![12].gt(0) ||
+              player.layers[this.layer].buyables![21].gt(0))
+          );
+        }
+      },
+      82: {
+        title: "6",
+        description: "Unlock another way to buy generators",
+        cost() {
+          return new Decimal(1);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 81) &&
+            (player.layers[this.layer].buyables![12].gt(0) ||
+              player.layers[this.layer].buyables![21].gt(0))
+          );
+        }
+      },
+      83: {
+        title: "7",
+        description: "Generator efficiency is boosted by prestige points",
+        cost() {
+          return new Decimal(3);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 82);
+        }
+      },
+      84: {
+        title: "8",
+        description: "You can complete <b>Point</b> one more time",
+        cost() {
+          return new Decimal(3);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 83);
+        }
+      },
+      91: {
+        title: "9",
+        description: "New Challenge Time",
+        cost() {
+          return new Decimal(20);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 84) &&
+            new Decimal(player.layers[this.layer].challenges![12]).gte(3)
+          );
+        }
+      },
+      92: {
+        title: "10",
+        description:
+          "Each of the first 50 generators adds 0.001 to <b>Increase</b>",
+        cost() {
+          return new Decimal(5);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 91) && hasChallenge(this.layer, 21);
+        }
+      },
+      93: {
+        title: "11",
+        description:
+          "Change the tree trunk in generator effect to a hypertessaract root",
+        cost() {
+          return new Decimal(7);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 92);
+        }
+      },
+      94: {
+        title: "12",
+        description: "Unlock a clickable in generators",
+        cost() {
+          return new Decimal(50);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 93);
+        }
+      },
+      101: {
+        title: "10th row????",
+        description: "Decrease the dimensions of <b>11</b> by 2",
+        cost() {
+          return new Decimal(10);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 94);
+        }
+      },
+      102: {
+        title: "2 Tree Trunks",
+        description:
+          "Double log of generator points adds to generator efficiency",
+        cost() {
+          return new Decimal(25);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 101);
+        }
+      },
+      103: {
+        title: "(not^5) challenging",
+        description: "Unlock the last challenge",
+        cost() {
+          return new Decimal(103);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 102);
+        }
+      },
+      104: {
+        title: "2 layers tree",
+        description: "Prestige points boost points, and unlock another tab",
+        cost() {
+          return new Decimal(100);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 103) && hasChallenge(this.layer, 22);
+        }
+      },
+      111: {
+        title: "not (hardcapped)",
+        description:
+          "Remove the generator clickable hardcap, and you can only pick one upgrade on each row below this",
+        cost() {
+          return new Decimal(110);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 104) && hasMilestone(this.layer, 6);
+        }
+      },
+      112: {
+        title: "Respec button",
+        description: "Respec all lower upgrades, but you don't get points back",
+        cost() {
+          return new Decimal(100);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 111) &&
+            (hasUpgrade(this.layer, 121) || hasUpgrade(this.layer, 122)) &&
+            !hasMilestone("i", 4)
+          );
+        },
+        onPurchase() {
+          player.layers.p.upgrades = player.layers.p.upgrades!.filter((i: string | number) => {
+            return Number(i) < 112;
+          });
+        }
+      },
+      121: {
+        title: "Timers",
+        description: "Raise the <b>Time Dilation</b> reward effect to the 1.2",
+        cost() {
+          return new Decimal(500);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 111) &&
+            (!hasUpgrade(this.layer, 122) || hasMilestone(this.layer, 7))
+          );
+        }
+      },
+      122: {
+        title: "Generators",
+        description:
+          "Decrease the first generator buyable cost scaling base by 2",
+        cost() {
+          return new Decimal(500);
+        },
+        unlocked() {
+          return (
+            hasUpgrade(this.layer, 111) &&
+            (!hasUpgrade(this.layer, 121) || hasMilestone(this.layer, 7))
+          );
+        }
+      },
+      131: {
+        title: "Prestige",
+        description: "Gain 10x more prestige points",
+        cost() {
+          return new Decimal(5000);
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 121) || hasUpgrade(this.layer, 122)) &&
+            (!hasUpgrade(this.layer, 132) || hasMilestone(this.layer, 7))
+          );
+        }
+      },
+      132: {
+        title: "One and a half",
+        description: "Raise generator effect to the 1.5",
+        cost() {
+          return new Decimal(5000);
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 121) || hasUpgrade(this.layer, 122)) &&
+            (!hasUpgrade(this.layer, 131) || hasMilestone(this.layer, 7))
+          );
+        }
+      },
+
+      141: {
+        title: "Active",
+        description:
+          "Multiply generator efficiency now increases by 1, but it doesn't automatically click.",
+        cost() {
+          return new Decimal(50000);
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 131) || hasUpgrade(this.layer, 132)) &&
+            ((!hasUpgrade(this.layer, 142) && !hasUpgrade(this.layer, 143)) ||
+              hasMilestone("i", 4))
+          );
+        }
+      },
+      142: {
+        title: "Passive",
+        description: "Gain 5x more points",
+        cost() {
+          return new Decimal(50000);
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 131) || hasUpgrade(this.layer, 132)) &&
+            ((!hasUpgrade(this.layer, 141) && !hasUpgrade(this.layer, 143)) ||
+              hasMilestone("i", 4))
+          );
+        }
+      },
+      143: {
+        title: "Idle",
+        description: "Hours played multiply generator power",
+        cost() {
+          return new Decimal(50000);
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 131) || hasUpgrade(this.layer, 132)) &&
+            ((!hasUpgrade(this.layer, 142) && !hasUpgrade(this.layer, 141)) ||
+              hasMilestone("i", 4))
+          );
+        }
+      },
+      211: {
+        title: "Prestige",
+        description: "Pointy points multiply points",
+        cost() {
+          return new Decimal(1);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) && player.subtabs.p.mainTabs != "Upgrades"
+          );
+        }
+      },
+      212: {
+        title: "Pointy",
+        description:
+          "Pointy prestige points reduce the cost scaling of pointy points",
+        cost() {
+          return new Decimal(2);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 211)
+          );
+        }
+      },
+      213: {
+        title: "Time",
+        description: "Generator power also multiplies point gain",
+        cost() {
+          return new Decimal(6);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 212)
+          );
+        }
+      },
+      214: {
+        title: "^0",
+        description: "Further reduce the pointy point scaling",
+        cost() {
+          return new Decimal(11);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 213)
+          );
+        }
+      },
+      215: {
+        title: "bulk",
+        description: "Auto-pointy points now buys 10 per tick",
+        cost() {
+          return new Decimal(27);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 214)
+          );
+        }
+      },
+      221: {
+        title: "^-1",
+        description: "^0 is even more powerful",
+        cost() {
+          return new Decimal(28);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 215)
+          );
+        }
+      },
+      222: {
+        title: "???",
+        description:
+          "square <b>bulk</b> and pointy prestige points multiply prestige points",
+        cost() {
+          return new Decimal(90);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 221)
+          );
+        }
+      },
+      223: {
+        title: "more automation",
+        description: "Automatically gain pointy prestige points",
+        cost() {
+          return new Decimal(96);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 222)
+          );
+        }
+      },
+      224: {
+        title: "Generation",
+        description: "Generator costs are divided by generator effect",
+        cost() {
+          return new Decimal(100);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 223)
+          );
+        }
+      },
+      225: {
+        title: "Boosters",
+        description: "Unlock boosters (next update)",
+        cost() {
+          return new Decimal(135);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 22)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            22,
+            getBuyableAmount(this.layer, 22)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("i", 5) &&
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasUpgrade(this.layer, 224)
+          );
+        }
+      },
+      231: {
+        title: "Blue",
+        description: "The generator effect also affects prestige points",
+        cost() {
+          return new Decimal(4);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 23)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            23,
+            getBuyableAmount(this.layer, 23)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasMilestone(this.layer, 11)
+          );
+        },
+        currencyDisplayName: "pointy boosters"
+      },
+      232: {
+        title: "Red",
+        description: "Unlock a third way to buy generators",
+        cost() {
+          return new Decimal(5);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 23)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            23,
+            getBuyableAmount(this.layer, 23)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasMilestone(this.layer, 12)
+          );
+        },
+        currencyDisplayName: "pointy boosters"
+      },
+      233: {
+        title: "Green",
+        description:
+          "Prestige points do not reset your pointy points and boosters don't reset generators",
+        cost() {
+          return new Decimal(5);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 23)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            23,
+            getBuyableAmount(this.layer, 23)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasMilestone(this.layer, 12)
+          );
+        },
+        currencyDisplayName: "pointy boosters"
+      },
+      234: {
+        title: "Yellow",
+        description:
+          "Divide the cost of the third generator buyable based on boosters",
+        cost() {
+          return new Decimal(6);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 23)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            23,
+            getBuyableAmount(this.layer, 23)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasMilestone(this.layer, 12)
+          );
+        },
+        currencyDisplayName: "pointy boosters"
+      },
+      235: {
+        title: "Orange",
+        description: "Raise the Infinity effect to the 6.9420th power",
+        cost() {
+          return new Decimal(8);
+        },
+        canAfford() {
+          return getBuyableAmount(this.layer, 23)!.gte(this.cost);
+        },
+        pay() {
+          setBuyableAmount(
+            this.layer,
+            23,
+            getBuyableAmount(this.layer, 23)!.sub(this.cost)
+          );
+        },
+        unlocked() {
+          return (
+            player.subtabs.p.mainTabs != "Upgrades" &&
+            hasMilestone(this.layer, 12)
+          );
+        },
+        currencyDisplayName: "pointy boosters"
+      }
+    }
+  },
+
+  clickables: {
+    rows: 1,
+    cols: 1,
+    data: {
+      11: {
+        display() {
+          return (
+            "Multiply generator efficiency by " +
+            format(player.layers.p.cmult as Decimal) +
+            ((player.layers.p.cmult as Decimal).min(100).eq(100) && !hasUpgrade(this.layer, 111)
+              ? " (hardcapped)"
+              : "")
+          );
+        },
+        unlocked() {
+          return hasUpgrade("p", 94);
+        },
+        click() {
+          player.layers.p.cmult = (player.layers.p.cmult as Decimal).plus(hasUpgrade("p", 141) ? 1 : 0.01);
+          if (!hasUpgrade(this.layer, 111))
+            player.layers.p.cmult = (player.layers.p.cmult as Decimal).min(100);
+        },
+        canClick() {
+          return (player.layers.p.cmult as Decimal).lt(100) || hasUpgrade(this.layer, 111);
+        }
+      }
+    }
+  },
+
+  challenges: {
+    rows: 99,
+    cols: 2,
+    data: {
+      11: {
+        name: "Time dilation",
+        challengeDescription() {
+          return "Point gain exponent is raised to the ^0.75";
+        },
+        goal() {
+          return new Decimal(100).times(
+            new Decimal(10).pow(
+              new Decimal(player.layers[this.layer].challenges![this.id])
+                .times(
+                  new Decimal(1).sub(
+                    new Decimal(
+                      layers[this.layer].challenges!.data[12].effect as number
+                    ).div(100)
+                  )
+                )
+                .pow(2)
+            )
+          );
+        },
+        rewardDescription() {
+          return (
+            "You have completed this challenge " +
+            player.layers[this.layer].challenges![this.id] +
+            "/" +
+            this.completionLimit +
+            " times. Multiply <b>Increase</b>'s effect by challenge completions+1. Currently: x" +
+            format(
+              new Decimal(player.layers[this.layer].challenges![this.id])
+                .add(1)
+                .pow(hasUpgrade(this.layer, 121) ? 1.2 : 1)
+            )
+          );
+        },
+        unlocked() {
+          return hasUpgrade("p", 51) || hasChallenge(this.layer, this.id);
+        },
+        completionLimit() {
+          if (hasUpgrade("p", 62)) return 7;
+          if (hasUpgrade("p", 53)) return 3;
+          if (hasUpgrade("p", 52)) return 2;
+          return 1;
+        }
+      },
+      12: {
+        name: "Point",
+        challengeDescription: "Points are pointed",
+        goal() {
+          return new Decimal(100);
+        },
+        rewardDescription() {
+          return (
+            "You have completed this challenge " +
+            player.layers[this.layer].challenges![this.id] +
+            "/" +
+            this.completionLimit +
+            " times, making previous challenge goal scale " +
+            layers[this.layer].challenges!.data[this.id].effect +
+            "% slower."
+          );
+        },
+        unlocked() {
+          return hasUpgrade("p", 61) || hasChallenge(this.layer, this.id);
+        },
+        effect() {
+          if (!hasChallenge(this.layer, this.id)) return 0;
+          if (player.layers[this.layer].challenges![this.id] == new Decimal(1))
+            return 50;
+          if (player.layers[this.layer].challenges![this.id] == new Decimal(2))
+            return 60;
+          if (player.layers[this.layer].challenges![this.id] == new Decimal(3))
+            return 70;
+        },
+        completionLimit() {
+          let l = new Decimal(1);
+          if (hasUpgrade("p", 84)) l = l.plus(1);
+          if (hasMilestone("p", 3)) l = l.plus(1);
+          return l;
+        }
+      },
+      21: {
+        name: "Time Points",
+        challengeDescription: "You are stuck in all above challenges",
+        goal() {
+          return new Decimal(308.25);
+        },
+        rewardDescription() {
+          return "Lower the first generator buyable cost base by 6";
+        },
+        unlocked() {
+          return hasUpgrade("p", 91) || hasChallenge(this.layer, this.id);
+        }
+      },
+      22: {
+        name: "Last Challenge",
+        challengeDescription: "Generator points do nothing",
+        goal() {
+          return new Decimal(9999);
+        },
+        rewardDescription() {
+          return "Autoclick the clickable and reduce <b>2 Tree Trunks</b> by 1";
+        },
+        unlocked() {
+          return hasUpgrade("p", 103) || hasChallenge(this.layer, this.id);
+        }
+      }
+    }
+  },
+  buyables: {
+    rows: 99,
+    cols: 4,
+    data: {
+      11: {
+        cost() {
+          return new Decimal(0);
+        },
+        display() {
+          return (
+            "Reset all upgrades and challenges, but get a boost. You have reset " +
+            getBuyableAmount(this.layer, this.id) +
+            " times.<br>" +
+            (getBuyableAmount(this.layer, this.id)!.eq(6)
+              ? "You can't buy more than 6 boosts!"
+              : "You need all upgrades to reset.")
+          );
+        },
+        canAfford() {
+          return (
+            player.layers[this.layer].points.gte(this.cost!) &&
+            hasUpgrade(this.layer, 74) &&
+            hasUpgrade(this.layer, 64) &&
+            getBuyableAmount(this.layer, this.id)!.lt(6)
+          );
+        },
+        buy() {
+          player.layers[this.layer].points = player.layers[
+            this.layer
+          ].points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          player.layers[this.layer].points = new Decimal(0);
+          player.layers[this.layer].upgrades = [];
+          if (hasMilestone(this.layer, 1))
+            player.layers[this.layer].upgrades = [
+              11,
+              12,
+              13,
+              14,
+              21,
+              22,
+              23,
+              24
+            ];
+          if (hasMilestone(this.layer, 3))
+            player.layers[this.layer].upgrades = [
+              11,
+              12,
+              13,
+              14,
+              21,
+              22,
+              23,
+              24,
+              31,
+              32,
+              33,
+              34,
+              41,
+              42,
+              43,
+              44,
+              51,
+              52,
+              53,
+              54,
+              61,
+              62,
+              63,
+              64
+            ];
+          if (!hasMilestone(this.layer, 2)) {
+            for (const c in layers[this.layer].challenges) {
+              player.layers[this.layer].challenges![c] = new Decimal(0);
+            }
+          }
+        },
+        unlocked() {
+          return (
+            (hasUpgrade(this.layer, 74) && hasUpgrade(this.layer, 64)) ||
+            hasMilestone(this.layer, 0)
+          );
+        }
+      },
+      12: {
+        cost() {
+          return new Decimal(1)
+            .times(
+              new Decimal(hasChallenge(this.layer, 21) ? 4 : 10)
+                .sub(hasUpgrade(this.layer, 122) ? 2 : 0)
+                .pow(player.layers.p.buyables![this.id])
+            )
+            .div(
+              hasUpgrade(this.layer, 224)
+                ? hasUpgrade("p", 132)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(2))
+                  : hasUpgrade("p", 101)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(3))
+                  : hasUpgrade("p", 93)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(0.2)
+                  : (player.layers.p.gp as Decimal).plus(1).log10()
+                : 1
+            );
+        },
+        display() {
+          return "Buy a generator for " + format(this.cost!) + " points";
+        },
+        canAfford() {
+          return player.points.gte(this.cost!) && hasMilestone(this.layer, 5);
+        },
+        buy() {
+          if (!hasMilestone("p", 13))
+            player.points = player.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          player.layers[this.layer].g = (player.layers[this.layer].g as Decimal).plus(1);
+        },
+        unlocked() {
+          return hasMilestone(this.layer, 5);
+        }
+      },
+      13: {
+        cost() {
+          return new Decimal(1)
+            .times(new Decimal(2).pow(player.layers.p.buyables![this.id]))
+            .div(
+              hasUpgrade(this.layer, 224)
+                ? hasUpgrade("p", 132)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(2))
+                  : hasUpgrade("p", 101)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(3))
+                  : hasUpgrade("p", 93)
+                  ? (player.layers.p.gp as Decimal).plus(1).pow(0.2)
+                  : (player.layers.p.gp as Decimal).plus(1).log10()
+                : 1
+            );
+        },
+        display() {
+          return (
+            "Buy a generator for " + format(this.cost!) + " prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasUpgrade("p", 82);
+        },
+        buy() {
+          if (!hasMilestone("p", 13))
+            player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          player.layers[this.layer].g = (player.layers[this.layer].g as Decimal).plus(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 82);
+        }
+      },
+      14: {
+        cost() {
+          return new Decimal(900)
+            .mul(new Decimal(1.01).pow(getBuyableAmount(this.layer, this.id)!))
+            .round()
+            .div(
+              hasUpgrade(this.layer, 234)
+                ? getBuyableAmount(this.layer, 23)!
+                    .pow(0.3)
+                    .plus(1)
+                : 1
+            );
+        },
+        display() {
+          return (
+            "Buy a generator for " + format(this.cost!) + " Infinity points"
+          );
+        },
+        canAfford() {
+          return player.layers.i.points.gte(this.cost!) && hasUpgrade("p", 232);
+        },
+        buy() {
+          if (!hasMilestone("p", 13))
+            player.layers.i.points = player.layers.i.points.sub(this.cost!).round();
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          player.layers[this.layer].g = (player.layers[this.layer].g as Decimal).plus(1);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 232);
+        }
+      },
+      21: {
+        cost() {
+          return new Decimal(20).plus(
+            getBuyableAmount(this.layer, this.id)!.pow(
+              new Decimal(2).sub(
+                new Decimal(
+                  hasUpgrade(this.layer, 221)
+                    ? 0.9
+                    : hasUpgrade(this.layer, 214)
+                    ? 0.6
+                    : 0.3
+                ).times(
+                  hasUpgrade(this.layer, 212)
+                    ? new Decimal(1).sub(
+                        new Decimal(0.75).pow(getBuyableAmount(this.layer, 22)!)
+                      )
+                    : 0
+                )
+              )
+            )
+          );
+        },
+        display() {
+          return (
+            "Reset your generators for +1 pointy point! Cost: " +
+            format(this.cost!) +
+            " Generators"
+          );
+        },
+        canAfford() {
+          return (player.layers.p.g as Decimal).gte(this.cost!) && hasUpgrade("p", 104);
+        },
+        buy() {
+          if (!hasMilestone("i", 1)) player.layers.p.g = new Decimal(0);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          if (!hasMilestone("i", 1))
+            setBuyableAmount(this.layer, 12, new Decimal(0));
+          if (!hasMilestone("i", 1))
+            setBuyableAmount(this.layer, 13, new Decimal(0));
+          if (!hasMilestone("i", 1)) player.layers.p.gp = new Decimal(0);
+        },
+        unlocked() {
+          return hasUpgrade(this.layer, 104);
+        }
+      },
+      22: {
+        cost() {
+          return new Decimal(8).plus(getBuyableAmount(this.layer, this.id)!);
+        },
+        display() {
+          return (
+            "Gain a pointy prestige point. Cost: " +
+            format(this.cost!) +
+            " Pointy Points"
+          );
+        },
+        canAfford() {
+          return (
+            getBuyableAmount(this.layer, 21)!.gte(this.cost!) &&
+            hasMilestone("i", 5)
+          );
+        },
+        buy() {
+          if (!hasUpgrade(this.layer, 233))
+            setBuyableAmount(
+              this.layer,
+              21,
+              getBuyableAmount(this.layer, 21)!.sub(this.cost!)
+            );
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return hasMilestone("i", 5);
+        }
+      },
+      23: {
+        cost() {
+          return new Decimal(124).plus(
+            getBuyableAmount(this.layer, this.id)!
+              .times(2)
+              .pow(2)
+          );
+        },
+        display() {
+          return (
+            "Gain a booster. Cost: " + format(this.cost!) + " Pointy Points"
+          );
+        },
+        canAfford() {
+          return (
+            getBuyableAmount(this.layer, 21)!.gte(this.cost!) &&
+            hasMilestone("i", 5)
+          );
+        },
+        buy() {
+          if (!hasMilestone(this.layer, 15))
+            setBuyableAmount(
+              this.layer,
+              21,
+              getBuyableAmount(this.layer, 21)!.sub(this.cost!)
+            );
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+          if (!hasMilestone(this.layer, 15)) {
+            if (!hasMilestone(this.layer, 12)) {
+              player.layers.p.upgrades = player.layers.p.upgrades!.filter((x: string | Number) => {
+                return Number(x) < 200 || Number(x) > 230;
+              });
+              if (hasMilestone(this.layer, 11)) {
+                player.layers.p.upgrades.push(215);
+                player.layers.p.upgrades.push(225);
+                player.layers.p.upgrades.push(223);
+                player.layers.p.upgrades.push(222);
+              }
+            }
+            setBuyableAmount("p", 21, new Decimal(0));
+            setBuyableAmount("p", 22, new Decimal(0));
+            if (!hasUpgrade("p", 233)) {
+              setBuyableAmount("p", 12, new Decimal(0));
+              setBuyableAmount("p", 13, new Decimal(0));
+              setBuyableAmount("p", 14, new Decimal(0));
+
+              player.layers.p.g = new Decimal(0);
+            }
+            player.layers.p.gp = new Decimal(0);
+          }
+        },
+        unlocked() {
+          return hasUpgrade("p", 225) || getBuyableAmount("p", 23)!.gt(0);
+        }
+      },
+      31: {
+        cost() {
+          return new Decimal(1e93)
+            .times(new Decimal(1.5).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(1.1).pow(
+                getBuyableAmount(this.layer, this.id)!.pow(2)
+              )
+            );
+        },
+        effect() {
+          return new Decimal(2)
+            .plus(layers.p.buyables!.data[33].effect as Decimal)
+            .pow(
+              getBuyableAmount(this.layer, this.id)!.plus(
+                layers.p.buyables!.data[51].effect as Decimal
+              )
+            );
+        },
+        display() {
+          return (
+            "Double point gain. \nCurrently: x" +
+            format(this.effect as Decimal) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return hasMilestone("p", 13);
+        }
+      },
+      32: {
+        cost() {
+          return new Decimal(1e95)
+            .times(new Decimal(2).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(1.01).pow(
+                getBuyableAmount(this.layer, this.id)!.pow(2)
+              )
+            );
+        },
+        display() {
+          return (
+            "Double prestige point gain. \nCurrently: x" +
+            format(
+              new Decimal(2)
+                .plus(layers.p.buyables!.data[33].effect as Decimal)
+                .pow(getBuyableAmount(this.layer, this.id)!)
+            ) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 13) && getBuyableAmount(this.layer, 31)!.gte(5)
+          );
+        }
+      },
+      33: {
+        cost() {
+          return new Decimal(1e100)
+            .times(new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(1.01).pow(
+                getBuyableAmount(this.layer, this.id)!.pow(2)
+              )
+            );
+        },
+        effect() {
+          return new Decimal(0.01)
+            .mul(getBuyableAmount(this.layer, this.id)!)
+            .times(layers.p.buyables!.data[43].effect as Decimal);
+        },
+        display() {
+          return (
+            "Add 0.01 to the previous 2 buyable bases. \nCurrently: +" +
+            format(this.effect as Decimal) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 13) &&
+            (getBuyableAmount(this.layer, this.id)!.gt(0) ||
+              player.layers.p.points.gte(1e100))
+          );
+        }
+      },
+      41: {
+        cost() {
+          return new Decimal(1e110)
+            .times(new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!.pow(2))
+            );
+        },
+        effect() {
+          return new Decimal(0.01).mul(
+            getBuyableAmount(this.layer, this.id)!.plus(
+              layers.p.buyables!.data[51].effect as Decimal
+            )
+          );
+        },
+        display() {
+          return (
+            "Add 0.01 to the booster effect base. \nCurrently: +" +
+            format(this.effect as Decimal) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 13) &&
+            (getBuyableAmount(this.layer, this.id)!.gt(0) ||
+              player.layers.p.points.gte(1e110))
+          );
+        }
+      },
+      42: {
+        cost() {
+          const c = new Decimal(1e270)
+            .times(new Decimal(2).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(1.01).pow(
+                getBuyableAmount(this.layer, this.id)!.pow(2)
+              )
+            );
+
+          return c;
+        },
+        effect() {
+          let f = new Decimal(1.001).pow(
+            getBuyableAmount(this.layer, this.id)!
+          );
+          if (f.gte(1.1)) f = f.pow(0.8).times(new Decimal(1.1).pow(0.2));
+          if (f.gte(1.35)) f = f.pow(0.5).times(new Decimal(1.35).pow(0.5));
+          if (f.gte(3)) f = new Decimal(3);
+          return f;
+        },
+        display() {
+          return (
+            "Raise point gain to the 1.001 \nCurrently: ^" +
+            format(this.effect as Decimal) +
+            ((this.effect as Decimal).eq(3) ? "(hardcapped)" : "") +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return (
+            player.layers.p.points.gte(this.cost!) &&
+            hasMilestone("p", 13) &&
+            (this.effect as Decimal).lt(3)
+          );
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 13) &&
+            (getBuyableAmount(this.layer, this.id)!.gt(0) ||
+              player.layers.p.points.gte(1e270))
+          );
+        }
+      },
+      43: {
+        cost() {
+          return new Decimal("1e375")
+            .times(new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!.pow(2))
+            );
+        },
+        effect() {
+          return new Decimal(0.01)
+            .mul(getBuyableAmount(this.layer, this.id)!)
+            .plus(1);
+        },
+        display() {
+          return (
+            "Multiply the above buyable effect. \nCurrently: *" +
+            format(this.effect as Decimal) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 13) &&
+            (getBuyableAmount(this.layer, this.id)!.gt(0) ||
+              player.layers.p.points.gte("1e375"))
+          );
+        }
+      },
+      51: {
+        cost() {
+          return new Decimal("1e1740")
+            .times(new Decimal(10).pow(getBuyableAmount(this.layer, this.id)!))
+            .times(
+              new Decimal(1e10).pow(
+                getBuyableAmount(this.layer, this.id)!.pow(2)
+              )
+            );
+        },
+        effect() {
+          return getBuyableAmount(this.layer, this.id)!.pow(0.55);
+        },
+        display() {
+          return (
+            "Add free levels to the above 2 buyables \nCurrently: " +
+            format(this.effect as Decimal) +
+            "\nCost: " +
+            format(this.cost!) +
+            " Prestige points"
+          );
+        },
+        canAfford() {
+          return player.layers.p.points.gte(this.cost!) && hasMilestone("p", 13);
+        },
+        buy() {
+          player.layers.p.points = player.layers.p.points.sub(this.cost!);
+          setBuyableAmount(
+            this.layer,
+            this.id,
+            getBuyableAmount(this.layer, this.id)!.add(1)
+          );
+        },
+        unlocked() {
+          return (
+            hasMilestone("p", 15) &&
+            (getBuyableAmount(this.layer, this.id)!.gt(0) ||
+              player.layers.p.points.gte("1e1700"))
+          );
+        }
+      }
+    }
+  },
+  milestones: {
+    data: {
+      0: {
+        requirementDisplay: "1 reset",
+        effectDisplay:
+          "Add 0.01 to base point gain and prestige requirement, and <b>1</b> doesn't reset upgrades",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(1);
+        },
+        unlocked() {
+          return layers.p.activeSubtab?.id == "Pointy points";
+        }
+      },
+      1: {
+        requirementDisplay: "2 resets",
+        effectDisplay:
+          "<div><b>2</b> and <b>3</b> don't reset upgrades, and start with the first 8 upgrades on reset</div>",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(2);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      2: {
+        requirementDisplay: "3 resets",
+        effectDisplay:
+          "<div><b>4</b> doesn't reset upgrades, and permanently fix the bug where you can't buy upgrades when you have 1 prestige point</div>",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(3);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      3: {
+        requirementDisplay: "4 resets",
+        effectDisplay:
+          "Don't reset challenges, add 1 to <b>Point</b> maximum completions, and start with 24 upgrades",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(4);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      4: {
+        requirementDisplay: "5 resets",
+        effectDisplay: "Each useless upgrade adds 0.1 to base point gain",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(5);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      5: {
+        requirementDisplay: "6 resets",
+        effectDisplay: "Unlock something",
+        done() {
+          return getBuyableAmount("p", 11)!.gte(6);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      6: {
+        requirementDisplay: "1 pointy point",
+        effectDisplay: "Unlock the upgrade tree",
+        done() {
+          return getBuyableAmount("p", 21)!.gte(1);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            (hasUpgrade(this.layer, 104) || player.layers.i.unlocked) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      7: {
+        requirementDisplay: "7 pointy points",
+        effectDisplay:
+          "You can now buy both first and second row upgrade tree upgrades",
+        done() {
+          return getBuyableAmount("p", 21)!.gte(7);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            (hasUpgrade(this.layer, 111) || player.layers.i.unlocked) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      8: {
+        requirementDisplay: "8 pointy points",
+        effectDisplay: "Unlock another layer",
+        done() {
+          return getBuyableAmount("p", 21)!.gte(8);
+        },
+        unlocked() {
+          return (
+            hasMilestone(this.layer, Number(this.id) - 1) &&
+            (hasUpgrade(this.layer, 141) ||
+              hasUpgrade(this.layer, 143) ||
+              hasUpgrade(this.layer, 142) ||
+              player.layers.i.unlocked) &&
+            layers.p.activeSubtab?.id == "Pointy points"
+          );
+        }
+      },
+      11: {
+        requirementDisplay: "3 boosters",
+        effectDisplay: "Keep automation on booster reset",
+        done() {
+          return getBuyableAmount("p", 23)!.gte(3);
+        },
+        unlocked() {
+          return (
+            getBuyableAmount("p", 23)!.gt(0) ||
+            hasMilestone(this.layer, this.id)
+          );
+        }
+      },
+      12: {
+        requirementDisplay: "5 boosters",
+        effectDisplay:
+          "Keep all prestige upgrades on booster reset and buy max infinity points",
+        done() {
+          return getBuyableAmount("p", 23)!.gte(5);
+        },
+        unlocked() {
+          return (
+            getBuyableAmount("p", 23)!.gt(0) ||
+            hasMilestone(this.layer, this.id)
+          );
+        }
+      },
+      13: {
+        requirementDisplay: "10 boosters",
+        effectDisplay: "Generators cost nothing",
+        done() {
+          return getBuyableAmount("p", 23)!.gte(10);
+        },
+        unlocked() {
+          return (
+            getBuyableAmount("p", 23)!.gt(0) ||
+            hasMilestone(this.layer, this.id)
+          );
+        }
+      },
+      14: {
+        requirementDisplay: "15 boosters",
+        effectDisplay:
+          "Auto buy the first 3 buyables and buy max pointy prestige points",
+        done() {
+          return getBuyableAmount("p", 23)!.gte(15);
+        },
+        unlocked() {
+          return (
+            getBuyableAmount("p", 41)!.gt(0) ||
+            hasMilestone(this.layer, this.id)
+          );
+        }
+      },
+      15: {
+        requirementDisplay: "20 boosters",
+        effectDisplay: "Boosters reset nothing and auto booster",
+        done() {
+          return getBuyableAmount("p", 23)!.gte(16);
+        },
+        unlocked() {
+          return (
+            getBuyableAmount("p", 41)!.gt(0) ||
+            hasMilestone(this.layer, this.id)
+          );
+        }
+      }
+    }
+  },
+  passiveGeneration() {
+    return hasUpgrade("i", 11) ? 1 : 0;
+  },
+  update(diff) {
+    if (hasMilestone(this.layer, 2) && !hasUpgrade(this.layer, 54)) {
+      player.layers[this.layer].upgrades!.push(54);
+    }
+    if (
+      hasMilestone(this.layer, 1) &&
+      !hasUpgrade(this.layer, 11) &&
+      !hasMilestone(this.layer, 3)
+    ) {
+      player.layers[this.layer].upgrades = [11, 12, 13, 14, 21, 22, 23, 24];
+    }
+    if (hasMilestone(this.layer, 3) && !hasUpgrade(this.layer, 31)) {
+      player.layers[this.layer].upgrades = [
+        11,
+        12,
+        13,
+        14,
+        21,
+        22,
+        23,
+        24,
+        31,
+        32,
+        33,
+        34,
+        41,
+        42,
+        43,
+        44,
+        51,
+        52,
+        53,
+        54,
+        61,
+        62,
+        63,
+        64
+      ];
+    }
+    if (hasMilestone(this.layer, 5)) {
+      player.layers[this.layer].gp = (player.layers[this.layer].gp as Decimal).plus(
+        (player.layers.p.g as Decimal).times(diff).times(player.layers.p.geff as Decimal)
+      );
+    }
+    let geff = new Decimal(1);
+    if (hasUpgrade("p", 81)) geff = geff.plus(2);
+    if (hasUpgrade("p", 102))
+      geff = geff.plus(
+        hasChallenge("p", 22)
+          ? (player.layers.p.gp as Decimal).plus(1).log(10)
+          : (player.layers.p.gp as Decimal)
+              .plus(1)
+              .log(10)
+              .plus(1)
+              .log(10)
+      );
+    if (hasUpgrade("p", 83))
+      geff = geff.times(
+        player.layers.p.points
+          .plus(1)
+          .log(10)
+          .plus(1)
+      );
+    if (hasUpgrade("p", 94)) geff = geff.times(player.layers.p.cmult as Decimal);
+    if (hasUpgrade("p", 104))
+      geff = geff.times(new Decimal(player.layers.p.buyables![21]).plus(1));
+    if (hasUpgrade("p", 143))
+      geff = geff.times(new Decimal(player.timePlayed).div(3600).max(1));
+    if (hasUpgrade("p", 225))
+      geff = geff.pow(
+        new Decimal(player.layers.p.buyables![23])
+          .div(10)
+          .mul(
+            new Decimal(0.1)
+              .plus(layers.p.buyables!.data[41].effect as Decimal)
+              .times(10)
+          )
+          .plus(1)
+      );
+    player.layers.p.geff = geff;
+    if (hasChallenge("p", 22) && (!hasUpgrade("p", 141) || hasUpgrade("i", 12)))
+      player.layers.p.cmult = (player.layers.p.cmult as Decimal).plus(hasUpgrade("p", 141) ? 1 : 0.01);
+    if (!hasUpgrade("p", 111)) player.layers.p.cmult = (player.layers.p.cmult as Decimal).min(100);
+    if (hasMilestone(this.layer, 14)) {
+      if (layers.p.buyables!.data[31].canAfford)
+        layers.p.buyables!.data[31].buy();
+      if (layers.p.buyables!.data[32].canAfford)
+        layers.p.buyables!.data[32].buy();
+      if (layers.p.buyables!.data[33].canAfford)
+        layers.p.buyables!.data[33].buy();
+    }
+    if (hasMilestone(this.layer, 15)) {
+      if (layers.p.buyables!.data[23].canAfford)
+        layers.p.buyables!.data[23].buy();
+    }
+  },
+  subtabs: {
+    Upgrades: {
+      display: `
+				<main-display />
+				<spacer />
+				<prestige-button display="" />
+				<spacer />
+				<spacer />
+				<upgrades />`
+    },
+    Challenges: {
+      unlocked() {
+        return hasUpgrade("p", 51) || hasMilestone("p", 0);
+      },
+      display: `
+				<spacer />
+				<spacer />
+				<challenges />`
+    },
+    "Buyables and Milestones": {
+      unlocked() {
+        return hasUpgrade("p", 74) || hasMilestone("p", 0);
+      },
+      display: `
+				<spacer />
+				<spacer />
+				<row><buyable id="11" /></row>
+				<spacer />
+				<div v-if="hasMilestone('p', 0)">Your boosts are making the point challenge {{ getBuyableAmount('p', 11).plus(1) }}x less pointy</div>
+				<spacer />
+				<milestones />`
+    },
+    Generators: {
+      unlocked() {
+        return hasMilestone("p", 5) || player.layers.i.points.gte(1);
+      },
+      display: `
+				<spacer />
+				<div>You have {{ format(player.layers.p.gp) }} generator points, adding {{ format(hasUpgrade("p",132)?player.layers.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.layers.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.layers.p.gp.plus(1).pow(0.2):player.layers.p.gp.plus(1).log10()) }} to point gain</div>
+				<div>You have {{ format(player.layers.p.g) }} generators, generating {{ format(player.layers.p.g.times(player.layers.p.geff)) }} generator points per second</div>
+				<div>Generator efficiency is {{ format(player.layers.p.geff) }}</div>
+				<spacer />
+				<spacer />
+				<buyables :buyables="[12, 13, 14]" />
+				<row><clickable id="11" /></row>`
+    },
+    "Pointy Points": {
+      unlocked() {
+        return hasUpgrade("p", 104) || player.layers.i.points.gte(1);
+      },
+      display: `
+				<div style="color: red; font-size: 32px; font-family: Comic Sans MS">{{ format(player.layers.p.buyables![21]) }} pointy points</div>
+				<div style="color: red; font-size: 32px; font-family: Comic Sans MS">My pointy points are multiplying generator efficiency by {{ format(new Decimal(player.layers.p.buyables![21]).plus(1)) }}</div>
+				<spacer />
+				<spacer />
+				<row><buyable id="21" /></row>
+				<div v-if="hasMilestone('i', 5)" style="color: red; font-size: 32px; font-family: Comic Sans MS">I have {{ format(player.layers.p.buyables![22]) }} pointy prestige points</div>
+				<row><buyable id="22" /></row>
+				<spacer />
+				<upgrades :upgrades="[211, 212, 213, 214, 215]" />
+				<upgrades :upgrades="[221, 222, 223, 224, 225]" />
+				<div v-if="hasMilestone('p', 225)" style="color: red; font-size: 32px; font-family: Comic Sans MS">I have {{ format(player.layers.p.buyables![23]) }} pointy boosters!</div>
+				<row><buyable id="23" /></row>
+				<div v-if="hasMilestone('p', 225) || getBuyableAmount('p', 23).gt(0)" style="color: red; font-size: 32px; font-family: Comic Sans MS">My pointy boosters are raising generator efficiency to the ^{{ format(new Decimal(player.layers.p.buyables![23]).div(10).mul(new Decimal(0.1).plus(layers.p.buyables[41].effect).times(10)).plus(1)) }}</div>
+				<spacer />
+				<spacer />
+				<div v-if="hasMilestone('p', 11)" style="font-size: 24px">Booster upgrades</div>
+				<upgrades :upgrades="[231, 232, 233, 234, 235]" />`
+    },
+    Buyables: {
+      unlocked() {
+        return hasMilestone("p", 13);
+      },
+      display: `
+				<buyables :buyables="[31, 32, 33]" />
+				<buyables :buyables="[41, 42, 43]" />`
+    }
+  }
+} as RawLayer;
diff --git a/src/data/mod.js b/src/data/mod.js
deleted file mode 100644
index eb16b35..0000000
--- a/src/data/mod.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import { computed } from 'vue';
-import { hasUpgrade, upgradeEffect, hasMilestone, inChallenge, getBuyableAmount } from '../util/features';
-import { layers } from '../game/layers';
-import player from '../game/player';
-import Decimal from '../util/bignum';
-
-// Import initial layers
-import f from './layers/aca/f.js';
-import c from './layers/aca/c.js';
-import a from './layers/aca/a.js';
-import demoLayer from './layers/demo.js';
-import demoInfinityLayer from './layers/demo-infinity.js';
-const g = {
-	id: "g",
-	symbol: "TH",
-	branches: ["c"],
-	color: '#6d3678',
-	shown: true,
-	canClick() {return player.points.gte(10)},
-	tooltip: "Thanos your points",
-	click() {
-		player.points = player.points.div(2);
-		console.log(this.layer);
-	}
-};
-const h = {
-	id: "h",
-	branches: ["g", () => ({ target: 'flatBoi', featureType: 'bar', endOffset: { x: -50 + 100 * layers.c.bars.flatBoi.progress.toNumber() } })],
-	tooltip() {return "Restore your points to {{ player.c.otherThingy }}"},
-	row: "side",
-	position: 3,
-	canClick() {return player.points.lt(player.c.otherThingy)},
-	click() {player.points = new Decimal(player.c.otherThingy)}
-};
-const spook = {
-	id: "spook",
-	row: 1,
-	layerShown: "ghost",
-};
-
-const main = {
-	id: 'main',
-	display: `
-		<div v-if="player.devSpeed === 0">Game Paused</div>
-		<div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
-		<div v-if="player.offTime != undefined">Offline Time: {{ formatTime(player.offTime.remain) }}</div>
-		<div>
-			<span v-if="player.points.lt('1e1000')">You have </span>
-			<h2>{{ format(player.points) }}</h2>
-			<span v-if="player.points.lt('1e1e6')"> points</span>
-		</div>
-		<div v-if="Decimal.gt(pointGain, 0)">
-			({{ player.oompsMag != 0 ? format(player.oomps) + " OOM" + (player.oompsMag < 0 ? "^OOM" : player.oompsMag > 1 ? "^" + player.oompsMag : "") + "s" : formatSmall(pointGain) }}/sec)
-		</div>
-		<spacer />
-		<tree :append="true" />`,
-	name: "Tree"
-};
-
-export const getInitialLayers = () => [ main, f, c, a, g, h, spook, demoLayer, demoInfinityLayer ];
-
-export function getStartingData() {
-	return {
-		points: new Decimal(10),
-	}
-}
-
-export const hasWon = computed(() => {
-	return false;
-});
-
-export const pointGain = computed(() => {
-	if(!hasUpgrade("c", 11))
-		return new Decimal(0);
-	let gain = new Decimal(3.19)
-	if (hasUpgrade("c", 12)) gain = gain.times(upgradeEffect("c", 12))
-	if (hasMilestone("p",0))gain=gain.plus(0.01)
-	if (hasMilestone("p",4)){
-		if (hasUpgrade("p",12))gain=gain.plus(0.1)
-		if (hasUpgrade("p",13))gain=gain.plus(0.1)
-		if (hasUpgrade("p",14))gain=gain.plus(0.1)
-		if (hasUpgrade("p",21))gain=gain.plus(0.1)
-		if (hasUpgrade("p",22))gain=gain.plus(0.1)
-		if (hasUpgrade("p",23))gain=gain.plus(0.1)
-		if (hasUpgrade("p",31))gain=gain.plus(0.1)
-		if (hasUpgrade("p",32))gain=gain.plus(0.1)
-		if (hasUpgrade("p",33))gain=gain.plus(0.1)
-	}
-	if (hasUpgrade("p",11))gain=gain.plus(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",12))gain=gain.times(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",13))gain=gain.pow(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",14))gain=gain.tetrate(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-
-	if (hasUpgrade("p",71)) gain=gain.plus(1.1)
-	if (hasUpgrade("p",72)) gain=gain.times(1.1)
-	if (hasUpgrade("p",73)) gain=gain.pow(1.1)
-	if (hasUpgrade("p",74)) gain=gain.tetrate(1.1)
-	if (hasMilestone("p",5)&&!inChallenge("p",22)){
-		let asdf = (hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10())
-		gain=gain.plus(asdf)
-		if (hasUpgrade("p",213))gain=gain.mul(asdf.plus(1))
-    }
-	if (hasUpgrade("p",104)) gain=gain.times(player.p.points.plus(1).pow(0.5))
-	if (hasUpgrade("p",142))gain=gain.times(5)
-	if (player.i.unlocked)gain=gain.times(player.i.points.plus(1).pow(hasUpgrade("p",235)?6.9420:1))
-	if (inChallenge("p",11)||inChallenge("p",21))gain=new Decimal(10).pow(gain.log10().pow(0.75))
-	if (inChallenge("p",12)||inChallenge("p",21))gain=gain.pow(new Decimal(1).sub(new Decimal(1).div(getBuyableAmount("p",11).plus(1))))
-	if (hasUpgrade("p",211))gain=gain.times(getBuyableAmount("p",21).plus(1))
-	if (hasMilestone("p",13))gain=gain.times(layers.p.buyables[31].effect)
-	if (hasMilestone("p",13))gain=gain.pow(layers.p.buyables[42].effect)
-	return gain;
-});
-
-/* eslint-disable-next-line no-unused-vars */
-export function update(delta) {
-}
-
-/* eslint-disable-next-line no-unused-vars */
-export function fixOldSave(oldVersion, playerData) {
-}
diff --git a/src/data/mod.ts b/src/data/mod.ts
new file mode 100644
index 0000000..6dfbc4e
--- /dev/null
+++ b/src/data/mod.ts
@@ -0,0 +1,187 @@
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { RawLayer } from "@/typings/layer";
+import { PlayerData } from "@/typings/player";
+import Decimal from "@/util/bignum";
+import {
+    getBuyableAmount,
+    hasMilestone,
+    hasUpgrade,
+    inChallenge,
+    upgradeEffect
+} from "@/util/features";
+import { computed } from "vue";
+import a from "./layers/aca/a";
+import c from "./layers/aca/c";
+import f from "./layers/aca/f";
+import demoLayer from "./layers/demo";
+import demoInfinityLayer from "./layers/demo-infinity";
+
+// Import initial layers
+
+const g = {
+    id: "g",
+    symbol: "TH",
+    branches: ["c"],
+    color: "#6d3678",
+    shown: true,
+    canClick() {
+        return player.points.gte(10);
+    },
+    tooltip: "Thanos your points",
+    click() {
+        player.points = player.points.div(2);
+        console.log(this.layer);
+    }
+} as RawLayer;
+const h = {
+    id: "h",
+    branches: [
+        "g",
+        () => ({
+            target: "flatBoi",
+            featureType: "bar",
+            endOffset: {
+                x:
+                    -50 +
+                    100 *
+                        (layers.c.bars!.data.flatBoi.progress instanceof Number
+                            ? (layers.c.bars!.data.flatBoi.progress as number)
+                            : (layers.c.bars!.data.flatBoi.progress as Decimal).toNumber())
+            }
+        })
+    ],
+    tooltip() {
+        return "Restore your points to {{ player.c.otherThingy }}";
+    },
+    row: "side",
+    position: 3,
+    canClick() {
+        return player.points.lt(player.layers.c.otherThingy as Decimal);
+    },
+    click() {
+        player.points = new Decimal(player.layers.c.otherThingy as Decimal);
+    }
+} as RawLayer;
+const spook = {
+    id: "spook",
+    row: 1,
+    layerShown: "ghost"
+} as RawLayer;
+
+const main = {
+    id: "main",
+    display: `
+		<div v-if="player.devSpeed === 0">Game Paused</div>
+		<div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
+		<div v-if="player.offTime != undefined">Offline Time: {{ formatTime(player.offTime.remain) }}</div>
+		<div>
+			<span v-if="player.points.lt('1e1000')">You have </span>
+			<h2>{{ format(player.points) }}</h2>
+			<span v-if="player.points.lt('1e1e6')"> points</span>
+		</div>
+		<div v-if="Decimal.gt(pointGain, 0)">
+			({{ player.oompsMag != 0 ? format(player.oomps) + " OOM" + (player.oompsMag < 0 ? "^OOM" : player.oompsMag > 1 ? "^" + player.oompsMag : "") + "s" : formatSmall(pointGain) }}/sec)
+		</div>
+		<spacer />
+		<tree :append="true" />`,
+    name: "Tree"
+} as RawLayer;
+
+export const getInitialLayers = (
+    /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+    playerData: Partial<PlayerData>
+): Array<RawLayer> => [main, f, c, a, g, h, spook, demoLayer, demoInfinityLayer];
+
+export function getStartingData(): Record<string, unknown> {
+    return {
+        points: new Decimal(10)
+    };
+}
+
+export const hasWon = computed(() => {
+    return false;
+});
+
+export const pointGain = computed(() => {
+    if (!hasUpgrade("c", 11)) return new Decimal(0);
+    let gain = new Decimal(3.19);
+    if (hasUpgrade("c", 12)) gain = gain.times(upgradeEffect("c", 12) as Decimal);
+    if (hasMilestone("p", 0)) gain = gain.plus(0.01);
+    if (hasMilestone("p", 4)) {
+        if (hasUpgrade("p", 12)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 13)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 14)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 21)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 22)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 23)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 31)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 32)) gain = gain.plus(0.1);
+        if (hasUpgrade("p", 33)) gain = gain.plus(0.1);
+    }
+    if (hasUpgrade("p", 11))
+        gain = gain.plus(
+            hasUpgrade("p", 34)
+                ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+                : 1
+        );
+    if (hasUpgrade("p", 12))
+        gain = gain.times(
+            hasUpgrade("p", 34)
+                ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+                : 1
+        );
+    if (hasUpgrade("p", 13))
+        gain = gain.pow(
+            hasUpgrade("p", 34)
+                ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal)
+                : 1
+        );
+    if (hasUpgrade("p", 14))
+        gain = gain.tetrate(
+            hasUpgrade("p", 34)
+                ? new Decimal(1).plus(layers.p.upgrades!.data[34].effect as Decimal).toNumber()
+                : 1
+        );
+
+    if (hasUpgrade("p", 71)) gain = gain.plus(1.1);
+    if (hasUpgrade("p", 72)) gain = gain.times(1.1);
+    if (hasUpgrade("p", 73)) gain = gain.pow(1.1);
+    if (hasUpgrade("p", 74)) gain = gain.tetrate(1.1);
+    if (hasMilestone("p", 5) && !inChallenge("p", 22)) {
+        const asdf = hasUpgrade("p", 132)
+            ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(2))
+            : hasUpgrade("p", 101)
+            ? (player.layers.p.gp as Decimal).plus(1).pow(new Decimal(1).div(3))
+            : hasUpgrade("p", 93)
+            ? (player.layers.p.gp as Decimal).plus(1).pow(0.2)
+            : (player.layers.p.gp as Decimal).plus(1).log10();
+        gain = gain.plus(asdf);
+        if (hasUpgrade("p", 213)) gain = gain.mul(asdf.plus(1));
+    }
+    if (hasUpgrade("p", 104)) gain = gain.times(player.layers.p.points.plus(1).pow(0.5));
+    if (hasUpgrade("p", 142)) gain = gain.times(5);
+    if (player.layers.i.unlocked)
+        gain = gain.times(player.layers.i.points.plus(1).pow(hasUpgrade("p", 235) ? 6.942 : 1));
+    if (inChallenge("p", 11) || inChallenge("p", 21))
+        gain = new Decimal(10).pow(gain.log10().pow(0.75));
+    if (inChallenge("p", 12) || inChallenge("p", 21))
+        gain = gain.pow(new Decimal(1).sub(new Decimal(1).div(getBuyableAmount("p", 11)!.plus(1))));
+    if (hasUpgrade("p", 211)) gain = gain.times(getBuyableAmount("p", 21)!.plus(1));
+    if (hasMilestone("p", 13)) gain = gain.times(layers.p.buyables!.data[31].effect as Decimal);
+    if (hasMilestone("p", 13)) gain = gain.pow(layers.p.buyables!.data[42].effect as Decimal);
+    return gain;
+});
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+export function update(delta: Decimal): void {}
+/* eslint-enable @typescript-eslint/no-unused-vars */
+
+/* eslint-disable @typescript-eslint/no-unused-vars */
+export function fixOldSave(
+    oldVersion: string | undefined,
+    playerData: Partial<PlayerData>
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
+): void {}
+/* eslint-enable @typescript-eslint/no-unused-vars */
diff --git a/src/data/themes.js b/src/data/themes.js
deleted file mode 100644
index d8fbffd..0000000
--- a/src/data/themes.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const defaultTheme = {
-	variables: {
-		"--background": "#0f0f0f",
-		"--background-tooltip": "rgba(0, 0, 0, 0.75)",
-		"--secondary-background": "#0f0f0f",
-		"--color": "#dfdfdf",
-		"--points": "#ffffff",
-		"--locked": "#bf8f8f",
-		"--bought": "#77bf5f",
-		"--link": "#02f2f2",
-		"--separator": "#dfdfdf",
-		"--border-radius": "25%",
-		"--danger": "rgb(220, 53, 69)",
-		"--modal-border": "solid 2px var(--color)",
-		"--feature-margin": "0px",
-	},
-	stackedInfoboxes: false,
-	floatingTabs: true
-};
-
-export default {
-	classic: defaultTheme,
-	paper: {
-		...defaultTheme,
-		variables: {
-			...defaultTheme.variables,
-			"--background": "#2a323d",
-			"--secondary-background": "#333c4a",
-			"--locked": "#3a3e45",
-			"--bought": "#5C8A58",
-			"--separator": "#333c4a",
-			"--border-radius": "4px",
-			"--modal-border": "",
-			"--feature-margin": "5px",
-		},
-		stackedInfoboxes: true,
-		floatingTabs: false
-	},
-	aqua: {
-		...defaultTheme,
-		variables: {
-			...defaultTheme.variables,
-			"--background": "#001f3f",
-			"--background-tooltip": "rgba(0, 15, 31, 0.75)",
-			"--secondary-background": "#001f3f",
-			"--color": "#bfdfff",
-			"--points": "#dfefff",
-			"--locked": "#c4a7b3",
-			"--separator": "#bfdfff"
-		}
-	}
-};
diff --git a/src/data/themes.ts b/src/data/themes.ts
new file mode 100644
index 0000000..35b8eaa
--- /dev/null
+++ b/src/data/themes.ts
@@ -0,0 +1,60 @@
+import { Theme } from "@/typings/theme";
+
+const defaultTheme: Theme = {
+    variables: {
+        "--background": "#0f0f0f",
+        "--background-tooltip": "rgba(0, 0, 0, 0.75)",
+        "--secondary-background": "#0f0f0f",
+        "--color": "#dfdfdf",
+        "--points": "#ffffff",
+        "--locked": "#bf8f8f",
+        "--bought": "#77bf5f",
+        "--link": "#02f2f2",
+        "--separator": "#dfdfdf",
+        "--border-radius": "25%",
+        "--danger": "rgb(220, 53, 69)",
+        "--modal-border": "solid 2px var(--color)",
+        "--feature-margin": "0px"
+    },
+    stackedInfoboxes: false,
+    floatingTabs: true
+};
+
+export enum Themes {
+    Classic = "classic",
+    Paper = "paper",
+    Aqua = "aqua"
+}
+
+export default {
+    classic: defaultTheme,
+    paper: {
+        ...defaultTheme,
+        variables: {
+            ...defaultTheme.variables,
+            "--background": "#2a323d",
+            "--secondary-background": "#333c4a",
+            "--locked": "#3a3e45",
+            "--bought": "#5C8A58",
+            "--separator": "#333c4a",
+            "--border-radius": "4px",
+            "--modal-border": "",
+            "--feature-margin": "5px"
+        },
+        stackedInfoboxes: true,
+        floatingTabs: false
+    } as Theme,
+    aqua: {
+        ...defaultTheme,
+        variables: {
+            ...defaultTheme.variables,
+            "--background": "#001f3f",
+            "--background-tooltip": "rgba(0, 15, 31, 0.75)",
+            "--secondary-background": "#001f3f",
+            "--color": "#bfdfff",
+            "--points": "#dfefff",
+            "--locked": "#c4a7b3",
+            "--separator": "#bfdfff"
+        }
+    } as Theme
+} as Record<Themes, Theme>;
diff --git a/src/game/enums.ts b/src/game/enums.ts
new file mode 100644
index 0000000..f175079
--- /dev/null
+++ b/src/game/enums.ts
@@ -0,0 +1,30 @@
+export enum LayerType {
+    Static = "static",
+    Normal = "normal",
+    Custom = "custom",
+    None = "none"
+}
+
+export enum Direction {
+    Up = "Up",
+    Down = "Down",
+    Left = "Left",
+    Right = "Right",
+    Default = "Up"
+}
+
+export enum MilestoneDisplay {
+    All = "all",
+    Last = "last",
+    Configurable = "configurable",
+    Incomplete = "incomplete",
+    None = "none"
+}
+
+export enum ImportingStatus {
+    NotImporting = "NOT_IMPORTING",
+    Importing = "IMPORTING",
+    Failed = "FAILED",
+    WrongID = "WRONG_ID",
+    Force = "FORCE"
+}
diff --git a/src/game/gameLoop.js b/src/game/gameLoop.js
deleted file mode 100644
index 7849245..0000000
--- a/src/game/gameLoop.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { update as modUpdate, hasWon, pointGain } from '../data/mod';
-import Decimal from '../util/bignum';
-import modInfo from '../data/modInfo.json';
-import { layers } from './layers';
-import player from './player';
-
-function updatePopups(/* diff */) {
-	// TODO
-}
-
-function updateParticles(/* diff */) {
-	// TODO
-}
-
-function updateOOMPS(diff) {
-	if (player.points != undefined) {
-		player.oompsMag = 0;
-		if (player.points.lte(new Decimal(1e100))) {
-			player.lastPoints = player.points;
-			return;
-		}
-
-		let curr = player.points;
-		let prev = player.lastPoints || new Decimal(0);
-		player.lastPoints = curr;
-		if (curr.gt(prev)) {
-			if (curr.gte("10^^8")) {
-				curr = curr.slog(1e10);
-				prev = prev.slog(1e10);
-				player.oomps = curr.sub(prev).div(diff);
-				player.oompsMag = -1;
-			} else {
-				while (curr.div(prev).log(10).div(diff).gte("100") && player.oompsMag <= 5 && prev.gt(0)) {
-					curr = curr.log(10);
-					prev = prev.log(10);
-					player.oomps = curr.sub(prev).div(diff);
-					player.oompsMag++;
-				}
-			}
-		}
-	}
-}
-
-function updateLayers(diff) {
-	// Update each active layer
-	const activeLayers = Object.keys(layers).filter(layer => !layers[layer].deactivated);
-	activeLayers.forEach(layer => {
-		if (player[layer].resetTime != undefined) {
-			player[layer].resetTime = player[layer].resetTime.add(diff);
-		}
-		if (layers[layer].passiveGeneration) {
-			player[layer].points =
-				player[layer].points.add(Decimal.times(layers[layer].resetGain, layers[layer].passiveGeneration).times(diff));
-		}
-		layers[layer].update?.(diff);
-	});
-	// Automate each active layer
-	activeLayers.forEach(layer => {
-		if (layers[layer].autoReset && layers[layer].canReset) {
-			layers[layer].reset();
-		}
-		layers[layer].automate?.();
-		if (layers[layer].upgrades && layers[layer].autoUpgrade) {
-			Object.values(layers[layer].upgrades).forEach(upgrade => upgrade.buy());
-		}
-	});
-	// Check each active layer for newly unlocked achievements or milestones
-	activeLayers.forEach(layer => {
-		if (layers[layer].milestones) {
-			Object.values(layers[layer].milestones).forEach(milestone => {
-				if (milestone.unlocked !== false && !milestone.earned && milestone.done) {
-					player[layer].milestones.push(milestone.id);
-					milestone.onComplete?.();
-					// TODO popup notification
-					player[layer].lastMilestone = milestone.id;
-				}
-			});
-		}
-		if (layers[layer].achievements) {
-			Object.values(layers[layer].achievements).forEach(achievement => {
-				if (achievement.unlocked !== false && !achievement.earned && achievement.done) {
-					player[layer].achievements.push(achievement.id);
-					achievement.onComplete?.();
-					// TODO popup notification
-				}
-			});
-		}
-	});
-}
-
-function update() {
-	let now = Date.now();
-	let diff = (now - player.time) / 1e3;
-	player.time = now;
-	let trueDiff = diff;
-
-	// Always update UI
-	updatePopups(trueDiff);
-	updateParticles(trueDiff);
-	player.lastTenTicks.push(trueDiff);
-	if (player.lastTenTicks.length > 10) {
-		player.lastTenTicks = player.lastTenTicks.slice(1);
-	}
-
-	// Stop here if the game is paused on the win screen
-	if (hasWon.value && !player.keepGoing) {
-		return;
-	}
-	// Stop here if the player had a NaN value
-	if (player.hasNaN) {
-		return;
-	}
-
-	diff = new Decimal(diff).max(0);
-
-	// Add offline time if any
-	if (player.offTime != undefined) {
-		if (player.offTime.remain > modInfo.offlineLimit * 3600) {
-			player.offTime.remain = modInfo.offlineLimit * 3600;
-		}
-		if (player.offTime.remain > 0 && player.devSpeed !== 0) {
-			let offlineDiff = Math.max(player.offTime.remain / 10, diff);
-			player.offTime.remain -= offlineDiff;
-			diff = diff.add(offlineDiff);
-		} else if (player.devSpeed === 0) {
-			player.offTime.remain += diff.toNumber();
-		}
-		if (!player.offlineProd || player.offTime.remain <= 0) {
-			player.offTime = undefined;
-		}
-	}
-
-	// Cap at max tick length
-	diff = Decimal.min(diff, modInfo.maxTickLength);
-
-	// Apply dev speed
-	if (player.devSpeed != undefined) {
-		diff = diff.times(player.devSpeed);
-	}
-
-	// Update
-	if (diff.eq(0)) {
-		return;
-	}
-	player.timePlayed = player.timePlayed.add(diff);
-	if (player.points != undefined) {
-		player.points = player.points.add(Decimal.times(pointGain.value, diff));
-	}
-	modUpdate(diff);
-	updateOOMPS(trueDiff);
-	updateLayers(diff);
-}
-
-export default function startGameLoop() {
-	setInterval(update, 50);
-}
diff --git a/src/game/gameLoop.ts b/src/game/gameLoop.ts
new file mode 100644
index 0000000..f197526
--- /dev/null
+++ b/src/game/gameLoop.ts
@@ -0,0 +1,171 @@
+import { hasWon, pointGain, update as modUpdate } from "@/data/mod";
+import modInfo from "@/data/modInfo.json";
+import Decimal, { DecimalSource } from "@/util/bignum";
+import { layers } from "./layers";
+import player from "./player";
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function updatePopups(diff: number) {
+    // TODO
+}
+
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+function updateParticles(diff: number) {
+    // TODO
+}
+
+function updateOOMPS(diff: DecimalSource) {
+    if (player.points != undefined) {
+        player.oompsMag = 0;
+        if (player.points.lte(new Decimal(1e100))) {
+            player.lastPoints = player.points;
+            return;
+        }
+
+        let curr = player.points;
+        let prev = (player.lastPoints as Decimal) || new Decimal(0);
+        player.lastPoints = curr;
+        if (curr.gt(prev)) {
+            if (curr.gte("10^^8")) {
+                curr = curr.slog(1e10);
+                prev = prev.slog(1e10);
+                player.oomps = curr.sub(prev).div(diff);
+                player.oompsMag = -1;
+            } else {
+                while (
+                    curr
+                        .div(prev)
+                        .log(10)
+                        .div(diff)
+                        .gte("100") &&
+                    player.oompsMag <= 5 &&
+                    prev.gt(0)
+                ) {
+                    curr = curr.log(10);
+                    prev = prev.log(10);
+                    player.oomps = curr.sub(prev).div(diff);
+                    player.oompsMag++;
+                }
+            }
+        }
+    }
+}
+
+function updateLayers(diff: DecimalSource) {
+    // Update each active layer
+    const activeLayers = Object.keys(layers).filter(layer => !layers[layer].deactivated);
+    activeLayers.forEach(layer => {
+        if (player.layers[layer].resetTime != undefined) {
+            player.layers[layer].resetTime = player.layers[layer].resetTime.add(diff);
+        }
+        if (layers[layer].passiveGeneration) {
+            const passiveGeneration =
+                typeof layers[layer].passiveGeneration == "boolean"
+                    ? 1
+                    : (layers[layer].passiveGeneration as DecimalSource);
+            player.layers[layer].points = player.layers[layer].points.add(
+                Decimal.times(layers[layer].resetGain, passiveGeneration).times(diff)
+            );
+        }
+        layers[layer].update?.(diff);
+    });
+    // Automate each active layer
+    activeLayers.forEach(layer => {
+        if (layers[layer].autoReset && layers[layer].canReset) {
+            layers[layer].reset();
+        }
+        layers[layer].automate?.();
+        if (layers[layer].upgrades && layers[layer].autoUpgrade) {
+            Object.values(layers[layer].upgrades!.data).forEach(upgrade => upgrade.buy());
+        }
+    });
+    // Check each active layer for newly unlocked achievements or milestones
+    activeLayers.forEach(layer => {
+        if (layers[layer].milestones) {
+            Object.values(layers[layer].milestones!.data).forEach(milestone => {
+                if (milestone.unlocked !== false && !milestone.earned && milestone.done) {
+                    player.layers[layer].milestones.push(milestone.id);
+                    milestone.onComplete?.();
+                    // TODO popup notification
+                    player.layers[layer].lastMilestone = milestone.id;
+                }
+            });
+        }
+        if (layers[layer].achievements) {
+            Object.values(layers[layer].achievements!.data).forEach(achievement => {
+                if (achievement.unlocked !== false && !achievement.earned && achievement.done) {
+                    player.layers[layer].achievements.push(achievement.id);
+                    achievement.onComplete?.();
+                    // TODO popup notification
+                }
+            });
+        }
+    });
+}
+
+function update() {
+    const now = Date.now();
+    let diff: DecimalSource = (now - player.time) / 1e3;
+    player.time = now;
+    const trueDiff = diff;
+
+    // Always update UI
+    updatePopups(trueDiff);
+    updateParticles(trueDiff);
+    player.lastTenTicks.push(trueDiff);
+    if (player.lastTenTicks.length > 10) {
+        player.lastTenTicks = player.lastTenTicks.slice(1);
+    }
+
+    // Stop here if the game is paused on the win screen
+    if (hasWon.value && !player.keepGoing) {
+        return;
+    }
+    // Stop here if the player had a NaN value
+    if (player.hasNaN) {
+        return;
+    }
+
+    diff = new Decimal(diff).max(0);
+
+    // Add offline time if any
+    if (player.offlineTime != undefined) {
+        if (player.offlineTime.gt(modInfo.offlineLimit * 3600)) {
+            player.offlineTime = new Decimal(modInfo.offlineLimit * 3600);
+        }
+        if (player.offlineTime.gt(0) && player.devSpeed !== 0) {
+            const offlineDiff = Decimal.max(player.offlineTime.div(10), diff);
+            player.offlineTime = player.offlineTime.sub(offlineDiff);
+            diff = diff.add(offlineDiff);
+        } else if (player.devSpeed === 0) {
+            player.offlineTime = player.offlineTime.add(diff);
+        }
+        if (!player.offlineProd || player.offlineTime.lt(0)) {
+            player.offlineTime = null;
+        }
+    }
+
+    // Cap at max tick length
+    diff = Decimal.min(diff, modInfo.maxTickLength);
+
+    // Apply dev speed
+    if (player.devSpeed != undefined) {
+        diff = diff.times(player.devSpeed);
+    }
+
+    // Update
+    if (diff.eq(0)) {
+        return;
+    }
+    player.timePlayed = player.timePlayed.add(diff);
+    if (player.points != undefined) {
+        player.points = player.points.add(Decimal.times(pointGain.value, diff));
+    }
+    modUpdate(diff);
+    updateOOMPS(trueDiff);
+    updateLayers(diff);
+}
+
+export default function startGameLoop(): void {
+    setInterval(update, 50);
+}
diff --git a/src/game/layers.js b/src/game/layers.js
deleted file mode 100644
index af6d6c7..0000000
--- a/src/game/layers.js
+++ /dev/null
@@ -1,452 +0,0 @@
-import clone from 'lodash.clonedeep';
-import { isFunction, isPlainObject } from '../util/common';
-import { createProxy, createGridProxy } from '../util/proxies';
-import playerProxy from './player';
-import Decimal from '../util/bignum';
-import { noCache, getStartingBuyables, getStartingClickables, getStartingChallenges, defaultLayerProperties } from '../util/layers';
-import { applyPlayerData } from '../util/save';
-import { isRef } from 'vue';
-
-export const layers = {};
-export const hotkeys = [];
-window.layers = layers;
-
-export function addLayer(layer, player = null) {
-	player = player || playerProxy;
-
-	// Check for required properties
-	if (!('id' in layer)) {
-		console.error(`Cannot add layer without a "id" property!`, layer);
-		return;
-	}
-	if (layer.type === "static" || layer.type === "normal") {
-		const missingProperty = [ 'baseAmount', 'requires' ].find(prop => !(prop in layer));
-		if (missingProperty) {
-			console.error(`Cannot add layer without a "${missingProperty}" property!`, layer);
-			return;
-		}
-	}
-
-	// Clone object to prevent modifying the original
-	layer = clone(layer);
-
-	player[layer.id] = applyPlayerData({
-		upgrades: [],
-		achievements: [],
-		milestones: [],
-		infoboxes: {},
-		buyables: getStartingBuyables(layer),
-		clickables: getStartingClickables(layer),
-		challenges: getStartingChallenges(layer),
-		grids: {},
-		...layer.startData?.()
-	}, player[layer.id]);
-
-	// Set default property values
-	layer = Object.assign({}, defaultLayerProperties, layer);
-	layer.layer = layer.id;
-	if (layer.type === "static" && (layer.base == undefined || Decimal.lte(layer.base, 1))) {
-		layer.base = 2;
-	}
-
-	// Process each feature
-	for (let property of uncachedProperties) {
-		if (layer[property] && !isRef(layer.property)) {
-			layer[property].forceCached = false;
-		}
-	}
-	for (let property of gridProperties) {
-		if (layer[property]) {
-			setRowCol(layer[property]);
-		}
-	}
-	for (let property of featureProperties) {
-		if (layer[property]) {
-			setupFeature(layer.id, layer[property]);
-		}
-	}
-	if (layer.upgrades) {
-		for (let id in layer.upgrades) {
-			if (isPlainObject(layer.upgrades[id])) {
-				layer.upgrades[id].bought = function() {
-					return !layer.deactivated && playerProxy[layer.id].upgrades.some(upgrade => upgrade == id);
-				}
-				setDefault(layer.upgrades[id], 'canAfford', function() {
-					if (this.currencyInternalName) {
-						let name = this.currencyInternalName;
-						if (this.currencyLocation) {
-							return !(this.currencyLocation[name].lt(this.cost));
-						} else if (this.currencyLayer) {
-							let lr = this.currencyLayer;
-							return !(playerProxy[lr][name].lt(this.cost));
-						} else {
-							return !(playerProxy[name].lt(this.cost));
-						}
-					} else {
-						return !(playerProxy[this.layer].points.lt(this.cost))
-					}
-				});
-				setDefault(layer.upgrades[id], 'pay', function() {
-					if (this.bought || !this.canAfford) {
-						return;
-					}
-					if (this.currencyInternalName) {
-						let name = this.currencyInternalName
-						if (this.currencyLocation) {
-							if (this.currencyLocation[name].lt(this.cost)) {
-								return;
-							}
-							this.currencyLocation[name] = this.currencyLocation[name].sub(this.cost);
-						} else if (this.currencyLayer) {
-							let lr = this.currencyLayer;
-							if (playerProxy[lr][name].lt(this.cost)) {
-								return;
-							}
-							playerProxy[lr][name] = playerProxy[lr][name].sub(this.cost);
-						} else {
-							if (playerProxy[name].lt(this.cost)) {
-								return;
-							}
-							playerProxy[name] = playerProxy[name].sub(this.cost);
-						}
-					} else {
-						if (playerProxy[this.layer].points.lt(this.cost)) {
-							return;
-						}
-						playerProxy[this.layer].points = playerProxy[this.layer].points.sub(this.cost);
-					}
-				}, false);
-				setDefault(layer.upgrades[id], 'buy', function() {
-					if (this.bought || !this.canAfford) {
-						return;
-					}
-					this.pay();
-					playerProxy[this.layer].upgrades.push(this.id);
-					this.onPurchase?.();
-				}, false);
-			}
-		}
-	}
-	if (layer.achievements) {
-		for (let id in layer.achievements) {
-			if (isPlainObject(layer.achievements[id])) {
-				layer.achievements[id].earned = function() {
-					return !layer.deactivated && playerProxy[layer.id].achievements.some(achievement => achievement == id);
-				}
-				setDefault(layer.achievements[id], 'onComplete', null, false);
-			}
-		}
-	}
-	if (layer.challenges) {
-		layer.activeChallenge = function() {
-			return Object.values(this.challenges).find(challenge => challenge.active);
-		}
-		for (let id in layer.challenges) {
-			if (isPlainObject(layer.challenges[id])) {
-				layer.challenges[id].shown = function() {
-					return this.unlocked !== false && (playerProxy.hideChallenges === false || !this.maxed);
-				}
-				layer.challenges[id].completed = function() {
-					return !layer.deactivated && playerProxy[layer.id].challenges[id]?.gt(0);
-				}
-				layer.challenges[id].completions = function() {
-					return playerProxy[layer.id].challenges[id];
-				}
-				layer.challenges[id].maxed = function() {
-					return !layer.deactivated && Decimal.gte(playerProxy[layer.id].challenges[id], this.completionLimit);
-				}
-				layer.challenges[id].active = function() {
-					return !layer.deactivated && playerProxy[layer.id].activeChallenge === id;
-				}
-				layer.challenges[id].toggle = noCache(function() {
-					let exiting = playerProxy[layer.id].activeChallenge === id;
-					if (exiting) {
-						if (this.canComplete && !this.maxed) {
-							let completions = this.canComplete;
-							if (completions === true) {
-								completions = 1;
-							}
-							playerProxy[layer.id].challenges[id] =
-								Decimal.min(playerProxy[layer.id].challenges[id].add(completions), this.completionLimit);
-							this.onComplete?.();
-						}
-						playerProxy[layer.id].activeChallenge = null;
-						this.onExit?.();
-						layer.reset(true);
-					} else if (!exiting && this.canStart) {
-						layer.reset(true);
-						playerProxy[layer.id].activeChallenge = id;
-						this.onEnter?.();
-					}
-				});
-				setDefault(layer.challenges[id], 'onComplete', null, false);
-				setDefault(layer.challenges[id], 'onEnter', null, false);
-				setDefault(layer.challenges[id], 'onExit', null, false);
-				setDefault(layer.challenges[id], 'canStart', true);
-				setDefault(layer.challenges[id], 'completionLimit', new Decimal(1));
-				setDefault(layer.challenges[id], 'mark', function() {
-					return Decimal.gt(this.completionLimit, 1) && this.maxed;
-				});
-				setDefault(layer.challenges[id], 'canComplete', function() {
-					if (!this.active) {
-						return false;
-					}
-					if (this.currencyInternalName) {
-						let name = this.currencyInternalName;
-						if (this.currencyLocation) {
-							return !(this.currencyLocation[name].lt(this.goal));
-						} else if (this.currencyLayer) {
-							let lr = this.currencyLayer;
-							return !(playerProxy[lr][name].lt(this.goal));
-						} else {
-							return !(playerProxy[name].lt(this.goal));
-						}
-					} else {
-						return !(playerProxy.points.lt(this.goal));
-					}
-				});
-			}
-		}
-	}
-	if (layer.buyables) {
-		setDefault(layer.buyables, 'respec', null, false);
-		setDefault(layer.buyables, 'reset', function() {
-			playerProxy[this.layer].buyables = getStartingBuyables(layer);
-		}, false);
-		for (let id in layer.buyables) {
-			if (isPlainObject(layer.buyables[id])) {
-				layer.buyables[id].amount = function() {
-					return playerProxy[layer.id].buyables[id];
-				}
-				layer.buyables[id].amountSet = function(amount) {
-					playerProxy[layer.id].buyables[id] = amount;
-				}
-				layer.buyables[id].canBuy = function() {
-					return !layer.deactivated && this.unlocked !== false && this.canAfford !== false &&
-						Decimal.lt(playerProxy[layer.id].buyables[id], this.purchaseLimit);
-				}
-				setDefault(layer.buyables[id], 'purchaseLimit', new Decimal(Infinity));
-				setDefault(layer.buyables[id], 'sellOne', null, false);
-				setDefault(layer.buyables[id], 'sellAll', null, false);
-				if (layer.buyables[id].cost != undefined) {
-					setDefault(layer.buyables[id], 'buy', function() {
-						if (this.canBuy) {
-							playerProxy[this.layer].points = playerProxy[this.layer].points.sub(this.cost());
-							this.amount = this.amount.add(1);
-						}
-					}, false);
-				}
-			}
-		}
-	}
-	if (layer.clickables) {
-		layer.clickables.layer = layer.id;
-		setDefault(layer.clickables, 'masterButtonClick', null, false);
-		if (layer.clickables.masterButtonDisplay != undefined) {
-			setDefault(layer.clickables, 'showMaster', true);
-		}
-		for (let id in layer.clickables) {
-			if (isPlainObject(layer.clickables[id])) {
-				layer.clickables[id].state = function() {
-					return playerProxy[layer.id].clickables[id];
-				}
-				layer.clickables[id].stateSet = function(state) {
-					playerProxy[layer.id].clickables[id] = state;
-				}
-				setDefault(layer.clickables[id], 'click', null, false);
-				setDefault(layer.clickables[id], 'hold', null, false);
-			}
-		}
-	}
-	if (layer.milestones) {
-		for (let id in layer.milestones) {
-			if (isPlainObject(layer.milestones[id])) {
-				layer.milestones[id].earned = function() {
-					return !layer.deactivated && playerProxy[layer.id].milestones.some(milestone => milestone == id);
-				}
-				layer.milestones[id].shown = function() {
-					if (!this.unlocked) {
-						return false;
-					}
-					switch (playerProxy.msDisplay) {
-						default:
-						case "all":
-							return true;
-						case "last":
-							return this.optionsDisplay || !this.earned ||
-								playerProxy[this.layer].milestones[playerProxy[this.layer].milestones.length - 1] === this.id;
-						case "configurable":
-							return this.optionsDisplay || !this.earned;
-						case "incomplete":
-							return !this.earned;
-						case "none":
-							return false;
-					}
-				}
-			}
-		}
-	}
-	if (layer.grids) {
-		for (let id in layer.grids) {
-			if (isPlainObject(layer.grids[id])) {
-				setDefault(player[layer.id].grids, id, {});
-				layer.grids[id].getData = function(cell) {
-					if (playerProxy[layer.id].grids[id][cell] != undefined) {
-						return playerProxy[layer.id].grids[id][cell];
-					}
-					if (isFunction(this.getStartData)) {
-						return this.getStartData(cell);
-					}
-					return this.getStartData;
-				}
-				layer.grids[id].dataSet = function(cell, data) {
-					playerProxy[layer.id].grids[id][cell] = data;
-				}
-				setDefault(layer.grids[id], 'getUnlocked', true, false);
-				setDefault(layer.grids[id], 'getCanClick', true, false);
-				setDefault(layer.grids[id], 'getStartData', "", false);
-				setDefault(layer.grids[id], 'getStyle', null, false);
-				setDefault(layer.grids[id], 'click', null, false);
-				setDefault(layer.grids[id], 'hold', null, false);
-				setDefault(layer.grids[id], 'getTitle', null, false);
-				setDefault(layer.grids[id], 'getDisplay', null, false);
-				layer.grids[id] = createGridProxy(layer.grids[id]);
-			}
-		}
-	}
-	if (layer.subtabs) {
-		layer.activeSubtab = function() {
-			if (layer.subtabs[playerProxy.subtabs[layer.id].mainTabs] &&
-				layer.subtabs[playerProxy.subtabs[layer.id].mainTabs].unlocked !== false) {
-				return layer.subtabs[playerProxy.subtabs[layer.id].mainTabs];
-			}
-			// Default to first unlocked tab
-			return Object.values(layer.subtabs).find(subtab => subtab.unlocked !== false);
-		}
-		setDefault(player, 'subtabs', {});
-		setDefault(player.subtabs, layer.id, {});
-		setDefault(player.subtabs[layer.id], 'mainTabs', Object.keys(layer.subtabs)[0]);
-		for (let id in layer.subtabs) {
-			if (isPlainObject(layer.subtabs[id])) {
-				layer.subtabs[id].active = function() {
-					return playerProxy.subtabs[this.layer].mainTabs === this.id;
-				}
-			}
-		}
-	}
-	if (layer.microtabs) {
-		setDefault(player, 'subtabs', {});
-		setDefault(player.subtabs, layer.id, {});
-		for (let family in layer.microtabs) {
-			layer.microtabs[family].activeMicrotab = function() {
-				if (this[playerProxy.subtabs[this.layer][family]] && this[playerProxy.subtabs[this.layer][family]].unlocked !== false) {
-					return this[playerProxy.subtabs[this.layer][family]];
-				}
-				// Default to first unlocked tab
-				return this[Object.keys(this).find(microtab => microtab !== 'activeMicrotab' && this[microtab].unlocked !== false)];
-			}
-			setDefault(player.subtabs[layer.id], family, Object.keys(layer.microtabs[family]).find(tab => tab !== 'activeMicrotab'));
-			layer.microtabs[family].layer = layer.id;
-			layer.microtabs[family].family = family;
-			for (let id in layer.microtabs[family]) {
-				if (isPlainObject(layer.microtabs[family][id])) {
-					layer.microtabs[family][id].layer = layer.id;
-					layer.microtabs[family][id].family = family;
-					layer.microtabs[family][id].id = id;
-					layer.microtabs[family][id].active = function() {
-						return playerProxy.subtabs[this.layer][this.family] === this.id;
-					}
-				}
-			}
-		}
-	}
-	if (layer.hotkeys) {
-		for (let id in layer.hotkeys) {
-			if (isPlainObject(layer.hotkeys[id])) {
-				setDefault(layer.hotkeys[id], 'press', null, false);
-				setDefault(layer.hotkeys[id], 'unlocked', function() {
-					return layer.unlocked;
-				});
-			}
-		}
-	}
-
-	// Create layer proxy
-	layer = createProxy(layer);
-
-	// Register layer
-	layers[layer.id] = layer;
-
-	// Register hotkeys
-	if (layer.hotkeys) {
-		for (let id in layer.hotkeys) {
-			hotkeys[layer.hotkeys[id].key] = layer.hotkeys[id];
-		}
-	}
-}
-
-export function removeLayer(layer) {
-	// Un-set hotkeys
-	if (layers[layer].hotkeys) {
-		for (let id in layers[layer].hotkeys) {
-			delete hotkeys[id];
-		}
-	}
-
-	delete layers[layer];
-}
-
-export function reloadLayer(layer) {
-	removeLayer(layer.id);
-
-	// Re-create layer
-	addLayer(layer);
-}
-
-const uncachedProperties = [ 'startData', 'click', 'update', 'reset', 'hardReset' ];
-const gridProperties = [ 'upgrades', 'achievements', 'challenges', 'buyables', 'clickables' ];
-const featureProperties = [ 'upgrades', 'achievements', 'challenges', 'buyables', 'clickables', 'milestones', 'bars',
-	'infoboxes', 'grids', 'hotkeys', 'subtabs' ];
-
-function setRowCol(features) {
-	if (features.rows && features.cols) {
-		return
-	}
-	let maxRow = 0;
-	let maxCol = 0;
-	for (let id in features) {
-		if (!isNaN(id)) {
-			if (Math.floor(id / 10) > maxRow) {
-				maxRow = Math.floor(id / 10);
-			}
-			if (id % 10 > maxCol) {
-				maxCol = id % 10;
-			}
-		}
-	}
-	features.rows = maxRow;
-	features.cols = maxCol;
-}
-
-function setupFeature(layer, features) {
-	features.layer = layer;
-	for (let id in features) {
-		const feature = features[id];
-		if (isPlainObject(feature)) {
-			feature.id = id;
-			feature.layer = layer;
-			if (feature.unlocked == undefined) {
-				feature.unlocked = true;
-			}
-		}
-	}
-}
-
-function setDefault(object, key, value, forceCached) {
-	if (object[key] == undefined && value != undefined) {
-		object[key] = value;
-	}
-	if (object[key] != undefined && isFunction(object[key]) && forceCached != undefined) {
-		object[key].forceCached = forceCached;
-	}
-}
diff --git a/src/game/layers.ts b/src/game/layers.ts
new file mode 100644
index 0000000..2ce627a
--- /dev/null
+++ b/src/game/layers.ts
@@ -0,0 +1,604 @@
+import { CacheableFunction } from "@/typings/cacheableFunction";
+import { Achievement } from "@/typings/features/achievement";
+import { Buyable } from "@/typings/features/buyable";
+import { Challenge } from "@/typings/features/challenge";
+import { Clickable } from "@/typings/features/clickable";
+import {
+    Feature,
+    Features,
+    GridFeatures,
+    RawFeature,
+    RawFeatures,
+    RawGridFeatures
+} from "@/typings/features/feature";
+import { Grid } from "@/typings/features/grid";
+import { Hotkey } from "@/typings/features/hotkey";
+import { Milestone } from "@/typings/features/milestone";
+import { Microtab, Subtab } from "@/typings/features/subtab";
+import { Upgrade } from "@/typings/features/upgrade";
+import { Layer, RawLayer } from "@/typings/layer";
+import { PlayerData } from "@/typings/player";
+import { State } from "@/typings/state";
+import Decimal, { DecimalSource } from "@/util/bignum";
+import { isFunction } from "@/util/common";
+import {
+    defaultLayerProperties,
+    getStartingBuyables,
+    getStartingChallenges,
+    getStartingClickables,
+    noCache
+} from "@/util/layers";
+import { createGridProxy, createLayerProxy } from "@/util/proxies";
+import { applyPlayerData } from "@/util/save";
+import clone from "lodash.clonedeep";
+import { isRef } from "vue";
+import { default as playerProxy } from "./player";
+
+export const layers: Record<string, Readonly<Layer>> = {};
+export const hotkeys: Hotkey[] = [];
+window.layers = layers;
+
+export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
+    player = player || playerProxy;
+
+    // Check for required properties
+    if (!("id" in layer)) {
+        console.error(`Cannot add layer without a "id" property!`, layer);
+        return;
+    }
+    if (layer.type === "static" || layer.type === "normal") {
+        const missingProperty = ["baseAmount", "requires"].find(prop => !(prop in layer));
+        if (missingProperty) {
+            console.error(`Cannot add layer without a "${missingProperty}" property!`, layer);
+            return;
+        }
+    }
+
+    // Clone object to prevent modifying the original
+    layer = clone(layer);
+
+    setDefault(player, "layers", {});
+    player.layers![layer.id] = applyPlayerData(
+        {
+            points: new Decimal(0),
+            unlocked: false,
+            resetTime: new Decimal(0),
+            upgrades: [],
+            achievements: [],
+            milestones: [],
+            infoboxes: {},
+            buyables: getStartingBuyables(layer.buyables?.data),
+            clickables: getStartingClickables(layer.clickables?.data),
+            challenges: getStartingChallenges(layer.challenges?.data),
+            grids: {},
+            confirmRespecBuyables: false,
+            ...(layer.startData?.() || {})
+        },
+        player.layers![layer.id]
+    );
+
+    // Set default property values
+    layer = Object.assign({}, defaultLayerProperties, layer);
+    layer.layer = layer.id;
+    if (layer.type === "static" && layer.base == undefined) {
+        layer.base = 2;
+    }
+
+    // Process each feature
+    const uncachedProperties = ["startData", "click", "update", "reset", "hardReset"];
+    for (const property of uncachedProperties) {
+        if (layer[property] && !isRef(layer.property) && isFunction(layer[property])) {
+            (layer[property] as CacheableFunction).forceCached = false;
+        }
+    }
+    if (layer.upgrades) {
+        setupFeatures<
+            RawGridFeatures<GridFeatures<Upgrade>, Upgrade>,
+            GridFeatures<Upgrade>,
+            Upgrade
+        >(layer.id, layer.upgrades!);
+        setRowCol(layer.upgrades);
+        for (const id in layer.upgrades.data) {
+            layer.upgrades.data[id].bought = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    playerProxy.layers[this.layer].upgrades.some(
+                        (upgrade: string | number) => upgrade == id
+                    )
+                );
+            };
+            setDefault(layer.upgrades.data[id], "canAfford", function() {
+                if (this.currencyInternalName) {
+                    const name = this.currencyInternalName;
+                    if (this.currencyLocation) {
+                        return !Decimal.lt(this.currencyLocation[name], this.cost);
+                    } else if (this.currencyLayer) {
+                        return !Decimal.lt(
+                            playerProxy.layers[this.currencyLayer][name] as DecimalSource,
+                            this.cost
+                        );
+                    } else {
+                        return !Decimal.lt(playerProxy[name] as DecimalSource, this.cost);
+                    }
+                } else {
+                    return !playerProxy.layers[this.layer].points.lt(this.cost);
+                }
+            });
+            setDefault(
+                layer.upgrades.data[id],
+                "pay",
+                function() {
+                    if (this.bought || !this.canAfford) {
+                        return;
+                    }
+                    if (this.currencyInternalName) {
+                        const name = this.currencyInternalName;
+                        if (this.currencyLocation) {
+                            if (Decimal.lt(this.currencyLocation[name], this.cost)) {
+                                return;
+                            }
+                            this.currencyLocation[name] = Decimal.sub(
+                                this.currencyLocation[name],
+                                this.cost
+                            );
+                        } else if (this.currencyLayer) {
+                            const lr = this.currencyLayer;
+                            if (
+                                Decimal.lt(playerProxy.layers[lr][name] as DecimalSource, this.cost)
+                            ) {
+                                return;
+                            }
+                            playerProxy.layers[lr][name] = Decimal.sub(
+                                playerProxy.layers[lr][name] as DecimalSource,
+                                this.cost
+                            );
+                        } else {
+                            if (Decimal.lt(playerProxy[name] as DecimalSource, this.cost)) {
+                                return;
+                            }
+                            playerProxy[name] = Decimal.sub(
+                                playerProxy[name] as DecimalSource,
+                                this.cost
+                            );
+                        }
+                    } else {
+                        if (playerProxy.layers[this.layer].points.lt(this.cost)) {
+                            return;
+                        }
+                        playerProxy.layers[this.layer].points = playerProxy.layers[
+                            this.layer
+                        ].points.sub(this.cost);
+                    }
+                },
+                false
+            );
+            setDefault(
+                layer.upgrades.data[id],
+                "buy",
+                function() {
+                    if (this.bought || !this.canAfford) {
+                        return;
+                    }
+                    this.pay();
+                    playerProxy.layers[this.layer].upgrades.push(this.id);
+                    this.onPurchase?.();
+                },
+                false
+            );
+            setDefault(layer.upgrades.data[id], "onPurchase", undefined, false);
+        }
+    }
+    if (layer.achievements) {
+        setupFeatures<
+            RawGridFeatures<GridFeatures<Achievement>, Achievement>,
+            GridFeatures<Achievement>,
+            Achievement
+        >(layer.id, layer.achievements!);
+        setRowCol(layer.achievements);
+        for (const id in layer.achievements.data) {
+            layer.achievements.data[id].earned = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    playerProxy.layers[this.layer].achievements.some(
+                        (achievement: string | number) => achievement == id
+                    )
+                );
+            };
+            setDefault(layer.achievements.data[id], "onComplete", undefined, false);
+        }
+    }
+    if (layer.challenges) {
+        setupFeatures<
+            RawGridFeatures<GridFeatures<Challenge>, Challenge>,
+            GridFeatures<Challenge>,
+            Challenge
+        >(layer.id, layer.challenges);
+        setRowCol(layer.challenges);
+        layer.activeChallenge = function() {
+            return Object.values(this.challenges!.data).find(
+                (challenge: Challenge) => challenge.active
+            );
+        };
+        for (const id in layer.challenges.data) {
+            layer.challenges.data[id].shown = function() {
+                return (
+                    this.unlocked !== false && (playerProxy.hideChallenges === false || !this.maxed)
+                );
+            };
+            layer.challenges.data[id].completed = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    playerProxy.layers[this.layer].challenges[id]?.gt(0)
+                );
+            };
+            layer.challenges.data[id].completions = function() {
+                return playerProxy.layers[this.layer].challenges[id];
+            };
+            layer.challenges.data[id].maxed = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    Decimal.gte(playerProxy.layers[this.layer].challenges[id], this.completionLimit)
+                );
+            };
+            layer.challenges.data[id].active = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    playerProxy.layers[this.layer].activeChallenge === id
+                );
+            };
+            layer.challenges.data[id].toggle = noCache(function(this: Challenge) {
+                const exiting = playerProxy.layers[this.layer].activeChallenge === id;
+                if (exiting) {
+                    if (this.canComplete && !this.maxed) {
+                        let completions: boolean | DecimalSource = this.canComplete;
+                        if (completions === true) {
+                            completions = 1;
+                        }
+                        playerProxy.layers[this.layer].challenges[id] = Decimal.min(
+                            playerProxy.layers[this.layer].challenges[id].add(completions),
+                            this.completionLimit
+                        );
+                        this.onComplete?.();
+                    }
+                    playerProxy.layers[this.layer].activeChallenge = null;
+                    this.onExit?.();
+                    layers[this.layer].reset(true);
+                } else if (!exiting && this.canStart) {
+                    layers[this.layer].reset(true);
+                    playerProxy.layers[this.layer].activeChallenge = id;
+                    this.onEnter?.();
+                }
+            });
+            setDefault(layer.challenges.data[id], "onComplete", undefined, false);
+            setDefault(layer.challenges.data[id], "onEnter", undefined, false);
+            setDefault(layer.challenges.data[id], "onExit", undefined, false);
+            setDefault(layer.challenges.data[id], "canStart", true);
+            setDefault(layer.challenges.data[id], "completionLimit", new Decimal(1));
+            setDefault(layer.challenges.data[id], "mark", function() {
+                return Decimal.gt(this.completionLimit, 1) && this.maxed;
+            });
+            setDefault(layer.challenges.data[id], "canComplete", function() {
+                if (!this.active) {
+                    return false;
+                }
+                if (this.currencyInternalName) {
+                    const name = this.currencyInternalName;
+                    if (this.currencyLocation) {
+                        return !Decimal.lt(this.currencyLocation[name], this.goal);
+                    } else if (this.currencyLayer) {
+                        const lr = this.currencyLayer;
+                        return !Decimal.lt(
+                            playerProxy.layers[lr][name] as DecimalSource,
+                            this.goal
+                        );
+                    } else {
+                        return !Decimal.lt(playerProxy[name] as DecimalSource, this.goal);
+                    }
+                } else {
+                    return !playerProxy.points.lt(this.goal);
+                }
+            });
+        }
+    }
+    if (layer.buyables) {
+        setupFeatures<
+            RawGridFeatures<GridFeatures<Buyable>, Buyable>,
+            GridFeatures<Buyable>,
+            Buyable
+        >(layer.id, layer.buyables);
+        setRowCol(layer.buyables);
+        setDefault(layer.buyables, "respec", undefined, false);
+        setDefault(
+            layer.buyables,
+            "reset",
+            function(this: NonNullable<Layer["buyables"]>) {
+                playerProxy.layers[this.layer].buyables = getStartingBuyables(layer.buyables?.data);
+            },
+            false
+        );
+        for (const id in layer.buyables.data) {
+            layer.buyables.data[id].amount = function() {
+                return playerProxy.layers[this.layer].buyables[id];
+            };
+            layer.buyables.data[id].amountSet = function(amount: Decimal) {
+                playerProxy.layers[this.layer].buyables[id] = amount;
+            };
+            layer.buyables.data[id].canBuy = function() {
+                return (
+                    !layers[this.layer].deactivated &&
+                    this.unlocked !== false &&
+                    this.canAfford !== false &&
+                    Decimal.lt(playerProxy.layers[this.layer].buyables[id], this.purchaseLimit)
+                );
+            };
+            setDefault(layer.buyables.data[id], "purchaseLimit", new Decimal(Infinity));
+            setDefault(layer.buyables.data[id], "sellOne", undefined, false);
+            setDefault(layer.buyables.data[id], "sellAll", undefined, false);
+            if (layer.buyables.data[id].cost != undefined) {
+                setDefault(
+                    layer.buyables.data[id],
+                    "buy",
+                    function() {
+                        if (this.canBuy) {
+                            playerProxy.layers[this.layer].points = playerProxy.layers[
+                                this.layer
+                            ].points.sub(this.cost!);
+                            this.amount = this.amount.add(1);
+                        }
+                    },
+                    false
+                );
+            }
+        }
+    }
+    if (layer.clickables) {
+        setupFeatures<
+            RawGridFeatures<GridFeatures<Clickable>, Clickable>,
+            GridFeatures<Clickable>,
+            Clickable
+        >(layer.id, layer.clickables);
+        setRowCol(layer.clickables);
+        setDefault(layer.clickables, "masterButtonClick", undefined, false);
+        if (layer.clickables.masterButtonDisplay != undefined) {
+            setDefault(layer.clickables, "showMasterButton", true);
+        }
+        for (const id in layer.clickables.data) {
+            layer.clickables.data[id].state = function() {
+                return playerProxy.layers[this.layer].clickables[id];
+            };
+            layer.clickables.data[id].stateSet = function(state: State) {
+                playerProxy.layers[this.layer].clickables[id] = state;
+            };
+            setDefault(layer.clickables.data[id], "canClick", true);
+            setDefault(layer.clickables.data[id], "click", undefined, false);
+            setDefault(layer.clickables.data[id], "hold", undefined, false);
+        }
+    }
+    if (layer.milestones) {
+        setupFeatures<RawFeatures<Features<Milestone>, Milestone>, Features<Milestone>, Milestone>(
+            layer.id,
+            layer.milestones
+        );
+        for (const id in layer.milestones.data) {
+            layer.milestones.data[id].earned = function() {
+                return (
+                    !layer.deactivated &&
+                    playerProxy.layers[this.layer].milestones.some(
+                        (milestone: string | number) => milestone == id
+                    )
+                );
+            };
+            layer.milestones.data[id].shown = function() {
+                if (!this.unlocked) {
+                    return false;
+                }
+                switch (playerProxy.msDisplay) {
+                    default:
+                    case "all":
+                        return true;
+                    case "last":
+                        return (
+                            this.optionsDisplay ||
+                            !this.earned ||
+                            playerProxy.layers[this.layer].milestones[
+                                playerProxy.layers[this.layer].milestones.length - 1
+                            ] === this.id
+                        );
+                    case "configurable":
+                        return this.optionsDisplay || !this.earned;
+                    case "incomplete":
+                        return !this.earned;
+                    case "none":
+                        return false;
+                }
+            };
+            setDefault(layer.milestones.data[id], "done", false);
+        }
+    }
+    if (layer.grids) {
+        setupFeatures<RawFeatures<Features<Grid>, Grid>, Features<Grid>, Grid>(
+            layer.id,
+            layer.grids
+        );
+        for (const id in layer.grids.data) {
+            setDefault(player.layers![layer.id].grids, id, {});
+            layer.grids.data[id].getData = function(cell): State {
+                if (playerProxy.layers[this.layer].grids[id][cell] != undefined) {
+                    return playerProxy.layers[this.layer].grids[id][cell];
+                }
+                if (isFunction(this.getStartData)) {
+                    return (this.getStartData as (this: Grid, cell: string | number) => State)(
+                        cell
+                    );
+                }
+                return this.getStartData;
+            };
+            layer.grids.data[id].setData = function(cell, data) {
+                playerProxy.layers[this.layer].grids[id][cell] = data;
+            };
+            setDefault(layer.grids.data[id], "getUnlocked", true, false);
+            setDefault(layer.grids.data[id], "getCanClick", true, false);
+            setDefault(layer.grids.data[id], "getStartData", "", false);
+            setDefault(layer.grids.data[id], "getStyle", undefined, false);
+            setDefault(layer.grids.data[id], "click", undefined, false);
+            setDefault(layer.grids.data[id], "hold", undefined, false);
+            setDefault(layer.grids.data[id], "getTitle", undefined, false);
+            layer.grids.data[id] = createGridProxy(layer.grids.data[id]) as Grid;
+        }
+    }
+    if (layer.subtabs) {
+        layer.activeSubtab = function() {
+            if (
+                layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!] &&
+                layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!].unlocked !==
+                    false
+            ) {
+                return layers[this.layer].subtabs![playerProxy.subtabs[this.layer].mainTabs!];
+            }
+            // Default to first unlocked tab
+            return Object.values(layers[this.layer].subtabs!).find(
+                (subtab: Subtab) => subtab.unlocked !== false
+            );
+        };
+        setDefault(player, "subtabs", {});
+        setDefault(player.subtabs!, layer.id, {});
+        setDefault(player.subtabs![layer.id], "mainTabs", Object.keys(layer.subtabs)[0]);
+        for (const id in layer.subtabs) {
+            layer.subtabs[id].active = function() {
+                return playerProxy.subtabs[this.layer].mainTabs === this.id;
+            };
+        }
+    }
+    if (layer.microtabs) {
+        setDefault(player, "subtabs", {});
+        setDefault(player.subtabs!, layer.id, {});
+        for (const family in layer.microtabs) {
+            if (Object.keys(layer.microtabs[family]).length === 0) {
+                console.warn(
+                    "Cannot create microtab family with 0 tabs",
+                    layer.id,
+                    family,
+                    layer.microtabs[family]
+                );
+                continue;
+            }
+            layer.microtabs[family].activeMicrotab = function() {
+                if (
+                    this.data[playerProxy.subtabs[this.layer as string][family]] &&
+                    this.data[playerProxy.subtabs[this.layer as string][family]].unlocked !== false
+                ) {
+                    return this[playerProxy.subtabs[this.layer as string][family]];
+                }
+                // Default to first unlocked tab
+                const firstUnlocked: string | undefined = Object.keys(this).find(
+                    microtab =>
+                        microtab !== "activeMicrotab" && this.data[microtab].unlocked !== false
+                );
+                return firstUnlocked != undefined ? this[firstUnlocked] : undefined;
+            };
+            setDefault(
+                player.subtabs![layer.id],
+                family,
+                Object.keys(layer.microtabs[family]).find(tab => tab !== "activeMicrotab")!
+            );
+            layer.microtabs[family].layer = layer.id;
+            layer.microtabs[family].family = family;
+            for (const id in layer.microtabs[family].data) {
+                const microtab: RawFeature<Microtab> = layer.microtabs[family].data[id];
+                microtab.layer = layer.id;
+                microtab.family = family;
+                microtab.id = id;
+                microtab.active = function() {
+                    return playerProxy.subtabs[this.layer][this.family] === this.id;
+                };
+            }
+        }
+    }
+    if (layer.hotkeys) {
+        for (const id in layer.hotkeys) {
+            setDefault(layer.hotkeys[id], "press", undefined, false);
+            setDefault(layer.hotkeys[id], "unlocked", function() {
+                return layers[this.layer].unlocked;
+            });
+        }
+    }
+
+    // Create layer proxy
+    layer = createLayerProxy(layer) as Layer;
+
+    // Register layer
+    layers[layer.id] = layer as Layer;
+
+    // Register hotkeys
+    if (layers[layer.id].hotkeys) {
+        for (const hotkey of layers[layer.id].hotkeys!) {
+            hotkeys.push(hotkey);
+        }
+    }
+}
+
+export function removeLayer(layer: string): void {
+    // Un-set hotkeys
+    if (layers[layer].hotkeys) {
+        for (const hotkey of Object.values(layers[layer].hotkeys!)) {
+            const index = hotkeys.indexOf(hotkey);
+            if (index >= 0) {
+                hotkeys.splice(index, 1);
+            }
+        }
+    }
+
+    delete layers[layer];
+}
+
+export function reloadLayer(layer: Layer): void {
+    removeLayer(layer.id);
+
+    // Re-create layer
+    addLayer(layer);
+}
+
+function setRowCol<T extends GridFeatures<S>, S extends Feature>(features: RawGridFeatures<T, S>) {
+    if (features.rows && features.cols) {
+        return;
+    }
+    let maxRow = 0;
+    let maxCol = 0;
+    for (const id in features) {
+        const index = Number(id);
+        if (!isNaN(index)) {
+            if (Math.floor(index / 10) > maxRow) {
+                maxRow = Math.floor(index / 10);
+            }
+            if (index % 10 > maxCol) {
+                maxCol = index % 10;
+            }
+        }
+    }
+    features.rows = maxRow;
+    features.cols = maxCol;
+}
+
+function setupFeatures<T extends RawFeatures<R, S>, R extends Features<S>, S extends Feature>(
+    layer: string,
+    features: T
+) {
+    features.layer = layer;
+    for (const id in features.data) {
+        const feature = features.data[id];
+        (feature as Feature).id = id;
+        (feature as Feature).layer = layer;
+        if (feature.unlocked == undefined) {
+            (feature as Feature).unlocked = true;
+        }
+    }
+}
+
+function setDefault<T, K extends keyof T>(object: T, key: K, value: T[K], forceCached?: boolean) {
+    if (object[key] == undefined && value != undefined) {
+        object[key] = value;
+    }
+    if (object[key] != undefined && isFunction(object[key]) && forceCached != undefined) {
+        Object.assign(object[key], { forceCached });
+    }
+}
diff --git a/src/game/player.js b/src/game/player.js
deleted file mode 100644
index 59e2190..0000000
--- a/src/game/player.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { reactive } from 'vue';
-import { isPlainObject } from '../util/common';
-import Decimal from '../util/bignum';
-
-const state = reactive({});
-
-const playerHandler = {
-	get(target, key) {
-		if (key === '__state' || key === '__path') {
-			return target[key];
-		}
-		if (target.__state[key] == undefined) {
-			return;
-		}
-		if (isPlainObject(target.__state[key]) && !(target.__state[key] instanceof Decimal)) {
-			if (target.__state[key] !== target[key]?.__state) {
-				const path = [ ...target.__path, key ];
-				target[key] = new Proxy({ __state: target.__state[key], __path: path }, playerHandler);
-			}
-			return target[key];
-		}
-
-		return target.__state[key];
-	},
-	set(target, property, value, receiver) {
-		if (!state.hasNaN && ((typeof value === 'number' && isNaN(value)) || (value instanceof Decimal && (isNaN(value.sign) || isNaN(value.layer) || isNaN(value.mag))))) {
-			const currentValue = target.__state[property];
-			if (!((typeof currentValue === 'number' && isNaN(currentValue)) || (currentValue instanceof Decimal && (isNaN(currentValue.sign) || isNaN(currentValue.layer) || isNaN(currentValue.mag))))) {
-				state.autosave = false;
-				state.hasNaN = true;
-				state.NaNPath = [ ...target.__path, property ];
-				state.NaNReceiver = receiver;
-				console.error(`Attempted to set NaN value`, [ ...target.__path, property ], target.__state);
-				throw 'Attempted to set NaN value. See above for details';
-			}
-		}
-		target.__state[property] = value;
-		if (property === 'points') {
-			if (target.__state.best != undefined) {
-				target.__state.best = Decimal.max(target.__state.best, value);
-			}
-			if (target.__state.total != undefined) {
-				const diff = Decimal.sub(value, target.__state.points);
-				if (diff.gt(0)) {
-					target.__state.total = target.__state.total.add(diff);
-				}
-			}
-		}
-		return true;
-	}
-};
-export default window.player = new Proxy({ __state: state, __path: [ 'player' ] }, playerHandler);
diff --git a/src/game/player.ts b/src/game/player.ts
new file mode 100644
index 0000000..c1f0e2a
--- /dev/null
+++ b/src/game/player.ts
@@ -0,0 +1,112 @@
+import { Themes } from "@/data/themes";
+import { PlayerData } from "@/typings/player";
+import Decimal from "@/util/bignum";
+import { isPlainObject } from "@/util/common";
+import { reactive } from "vue";
+import { ImportingStatus, MilestoneDisplay } from "./enums";
+
+const state = reactive<PlayerData>({
+    id: "",
+    points: new Decimal(0),
+    oomps: new Decimal(0),
+    oompsMag: 0,
+    name: "",
+    tabs: [],
+    time: -1,
+    autosave: true,
+    offlineProd: true,
+    offlineTime: null,
+    timePlayed: new Decimal(0),
+    keepGoing: false,
+    lastTenTicks: [],
+    showTPS: true,
+    msDisplay: MilestoneDisplay.All,
+    hideChallenges: false,
+    theme: Themes.Paper,
+    subtabs: {},
+    minimized: {},
+    modID: "",
+    modVersion: "",
+    hasNaN: false,
+    NaNPath: [],
+    NaNReceiver: null,
+    importing: ImportingStatus.NotImporting,
+    saveToImport: "",
+    saveToExport: "",
+    layers: {}
+});
+
+const playerHandler: ProxyHandler<Record<string, any>> = {
+    get(target: Record<string, any>, key: string): any {
+        if (key === "__state" || key === "__path") {
+            return target[key];
+        }
+        if (target.__state[key] == undefined) {
+            return;
+        }
+        if (isPlainObject(target.__state[key]) && !(target.__state[key] instanceof Decimal)) {
+            if (target.__state[key] !== target[key]?.__state) {
+                const path = [...target.__path, key];
+                target[key] = new Proxy(
+                    { __state: target.__state[key], __path: path },
+                    playerHandler
+                );
+            }
+            return target[key];
+        }
+
+        return target.__state[key];
+    },
+    set(
+        target: Record<string, any>,
+        property: string,
+        value: any,
+        receiver: ProxyConstructor
+    ): boolean {
+        if (
+            !state.hasNaN &&
+            ((typeof value === "number" && isNaN(value)) ||
+                (value instanceof Decimal &&
+                    (isNaN(value.sign) || isNaN(value.layer) || isNaN(value.mag))))
+        ) {
+            const currentValue = target.__state[property];
+            if (
+                !(
+                    (typeof currentValue === "number" && isNaN(currentValue)) ||
+                    (currentValue instanceof Decimal &&
+                        (isNaN(currentValue.sign) ||
+                            isNaN(currentValue.layer) ||
+                            isNaN(currentValue.mag)))
+                )
+            ) {
+                state.autosave = false;
+                state.hasNaN = true;
+                state.NaNPath = [...target.__path, property];
+                state.NaNReceiver = (receiver as unknown) as Record<string, unknown>;
+                console.error(
+                    `Attempted to set NaN value`,
+                    [...target.__path, property],
+                    target.__state
+                );
+                throw "Attempted to set NaN value. See above for details";
+            }
+        }
+        target.__state[property] = value;
+        if (property === "points") {
+            if (target.__state.best != undefined) {
+                target.__state.best = Decimal.max(target.__state.best, value);
+            }
+            if (target.__state.total != undefined) {
+                const diff = Decimal.sub(value, target.__state.points);
+                if (diff.gt(0)) {
+                    target.__state.total = target.__state.total.add(diff);
+                }
+            }
+        }
+        return true;
+    }
+};
+export default window.player = new Proxy(
+    { __state: state, __path: ["player"] },
+    playerHandler
+) as PlayerData;
diff --git a/src/lib/break_eternity.js b/src/lib/break_eternity.js
deleted file mode 100644
index 857f825..0000000
--- a/src/lib/break_eternity.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/* eslint-disable */
-"use strict";function _instanceof(t,r){return null!=r&&"undefined"!=typeof Symbol&&r[Symbol.hasInstance]?!!r[Symbol.hasInstance](t):t instanceof r}function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}!function(t,r){"object"===("undefined"==typeof exports?"undefined":_typeof(exports))&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(t=t||self).Decimal=r()}(void 0,function(){var t=Math.log10(9e15),r=function(){for(var t=[],r=-323;r<=308;r++)t.push(Number("1e"+r));return function(r){return t[r+323]}}(),i=function(t){return h.fromValue_noAlloc(t)},e=function(t,r,i){return h.fromComponents(t,r,i)},n=function(t,r,i){return h.fromComponents_noNormalize(t,r,i)},a=function(t,r){var i=r+1,e=Math.ceil(Math.log10(Math.abs(t))),n=Math.round(t*Math.pow(10,i-e))*Math.pow(10,e-i);return parseFloat(n.toFixed(Math.max(i-e,0)))},s=function(t){return Math.sign(t)*Math.log10(Math.abs(t))},o=function(t){var r,i,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1e-10;if(!Number.isFinite(t))return t;if(0===t)return t;if(1===t)return.5671432904097838;r=t<10?0:Math.log(t)-Math.log(Math.log(t));for(var n=0;n<100;++n){if(i=(t*Math.exp(-r)+r*r)/(r+1),Math.abs(i-r)<e*Math.abs(i))return i;r=i}throw Error("Iteration failed to converge: "+t)},h=function(){function h(t){this.sign=Number.NaN,this.layer=Number.NaN,this.mag=Number.NaN,_instanceof(t,h)?this.fromDecimal(t):"number"==typeof t?this.fromNumber(t):"string"==typeof t?this.fromString(t):(this.sign=0,this.layer=0,this.mag=0)}Object.defineProperty(h.prototype,"m",{get:function(){if(0===this.sign)return 0;if(0===this.layer){var t,i=Math.floor(Math.log10(this.mag));return t=5e-324===this.mag?5:this.mag/r(i),this.sign*t}if(1===this.layer){var e=this.mag-Math.floor(this.mag);return this.sign*Math.pow(10,e)}return this.sign},set:function(t){this.layer<=2?this.fromMantissaExponent(t,this.e):(this.sign=Math.sign(t),0===this.sign&&(this.layer,this.exponent))},enumerable:!0,configurable:!0}),Object.defineProperty(h.prototype,"e",{get:function(){return 0===this.sign?0:0===this.layer?Math.floor(Math.log10(this.mag)):1===this.layer?Math.floor(this.mag):2===this.layer?Math.floor(Math.sign(this.mag)*Math.pow(10,Math.abs(this.mag))):this.mag*Number.POSITIVE_INFINITY},set:function(t){this.fromMantissaExponent(this.m,t)},enumerable:!0,configurable:!0}),Object.defineProperty(h.prototype,"s",{get:function(){return this.sign},set:function(t){0===t?(this.sign=0,this.layer=0,this.mag=0):this.sign=t},enumerable:!0,configurable:!0}),Object.defineProperty(h.prototype,"mantissa",{get:function(){return this.m},set:function(t){this.m=t},enumerable:!0,configurable:!0}),Object.defineProperty(h.prototype,"exponent",{get:function(){return this.e},set:function(t){this.e=t},enumerable:!0,configurable:!0}),h.fromComponents=function(t,r,i){return(new h).fromComponents(t,r,i)},h.fromComponents_noNormalize=function(t,r,i){return(new h).fromComponents_noNormalize(t,r,i)},h.fromMantissaExponent=function(t,r){return(new h).fromMantissaExponent(t,r)},h.fromMantissaExponent_noNormalize=function(t,r){return(new h).fromMantissaExponent_noNormalize(t,r)},h.fromDecimal=function(t){return(new h).fromDecimal(t)},h.fromNumber=function(t){return(new h).fromNumber(t)},h.fromString=function(t){return(new h).fromString(t)},h.fromValue=function(t){return(new h).fromValue(t)},h.fromValue_noAlloc=function(t){return _instanceof(t,h)?t:new h(t)},h.abs=function(t){return i(t).abs()},h.neg=function(t){return i(t).neg()},h.negate=function(t){return i(t).neg()},h.negated=function(t){return i(t).neg()},h.sign=function(t){return i(t).sign()},h.sgn=function(t){return i(t).sign()},h.round=function(t){return i(t).round()},h.floor=function(t){return i(t).floor()},h.ceil=function(t){return i(t).ceil()},h.trunc=function(t){return i(t).trunc()},h.add=function(t,r){return i(t).add(r)},h.plus=function(t,r){return i(t).add(r)},h.sub=function(t,r){return i(t).sub(r)},h.subtract=function(t,r){return i(t).sub(r)},h.minus=function(t,r){return i(t).sub(r)},h.mul=function(t,r){return i(t).mul(r)},h.multiply=function(t,r){return i(t).mul(r)},h.times=function(t,r){return i(t).mul(r)},h.div=function(t,r){return i(t).div(r)},h.divide=function(t,r){return i(t).div(r)},h.recip=function(t){return i(t).recip()},h.reciprocal=function(t){return i(t).recip()},h.reciprocate=function(t){return i(t).reciprocate()},h.cmp=function(t,r){return i(t).cmp(r)},h.cmpabs=function(t,r){return i(t).cmpabs(r)},h.compare=function(t,r){return i(t).cmp(r)},h.eq=function(t,r){return i(t).eq(r)},h.equals=function(t,r){return i(t).eq(r)},h.neq=function(t,r){return i(t).neq(r)},h.notEquals=function(t,r){return i(t).notEquals(r)},h.lt=function(t,r){return i(t).lt(r)},h.lte=function(t,r){return i(t).lte(r)},h.gt=function(t,r){return i(t).gt(r)},h.gte=function(t,r){return i(t).gte(r)},h.max=function(t,r){return i(t).max(r)},h.min=function(t,r){return i(t).min(r)},h.minabs=function(t,r){return i(t).minabs(r)},h.maxabs=function(t,r){return i(t).maxabs(r)},h.clamp=function(t,r,e){return i(t).clamp(r,e)},h.clampMin=function(t,r){return i(t).clampMin(r)},h.clampMax=function(t,r){return i(t).clampMax(r)},h.cmp_tolerance=function(t,r,e){return i(t).cmp_tolerance(r,e)},h.compare_tolerance=function(t,r,e){return i(t).cmp_tolerance(r,e)},h.eq_tolerance=function(t,r,e){return i(t).eq_tolerance(r,e)},h.equals_tolerance=function(t,r,e){return i(t).eq_tolerance(r,e)},h.neq_tolerance=function(t,r,e){return i(t).neq_tolerance(r,e)},h.notEquals_tolerance=function(t,r,e){return i(t).notEquals_tolerance(r,e)},h.lt_tolerance=function(t,r,e){return i(t).lt_tolerance(r,e)},h.lte_tolerance=function(t,r,e){return i(t).lte_tolerance(r,e)},h.gt_tolerance=function(t,r,e){return i(t).gt_tolerance(r,e)},h.gte_tolerance=function(t,r,e){return i(t).gte_tolerance(r,e)},h.pLog10=function(t){return i(t).pLog10()},h.absLog10=function(t){return i(t).absLog10()},h.log10=function(t){return i(t).log10()},h.log=function(t,r){return i(t).log(r)},h.log2=function(t){return i(t).log2()},h.ln=function(t){return i(t).ln()},h.logarithm=function(t,r){return i(t).logarithm(r)},h.pow=function(t,r){return i(t).pow(r)},h.pow10=function(t){return i(t).pow10()},h.root=function(t,r){return i(t).root(r)},h.factorial=function(t,r){return i(t).factorial()},h.gamma=function(t,r){return i(t).gamma()},h.lngamma=function(t,r){return i(t).lngamma()},h.exp=function(t){return i(t).exp()},h.sqr=function(t){return i(t).sqr()},h.sqrt=function(t){return i(t).sqrt()},h.cube=function(t){return i(t).cube()},h.cbrt=function(t){return i(t).cbrt()},h.tetrate=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:n(1,0,1);return i(t).tetrate(r,e)},h.iteratedexp=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:n(1,0,1);return i(t).iteratedexp(r,e)},h.iteratedlog=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:10,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;return i(t).iteratedlog(r,e)},h.layeradd10=function(t,r){return i(t).layeradd10(r)},h.layeradd=function(t,r){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:10;return i(t).layeradd(r,e)},h.slog=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:10;return i(t).slog(r)},h.lambertw=function(t){return i(t).lambertw()},h.ssqrt=function(t){return i(t).ssqrt()},h.pentate=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:n(1,0,1);return i(t).pentate(r,e)},h.affordGeometricSeries=function(t,r,e,n){return this.affordGeometricSeries_core(i(t),i(r),i(e),n)},h.sumGeometricSeries=function(t,r,e,n){return this.sumGeometricSeries_core(t,i(r),i(e),n)},h.affordArithmeticSeries=function(t,r,e,n){return this.affordArithmeticSeries_core(i(t),i(r),i(e),i(n))},h.sumArithmeticSeries=function(t,r,e,n){return this.sumArithmeticSeries_core(i(t),i(r),i(e),i(n))},h.efficiencyOfPurchase=function(t,r,e){return this.efficiencyOfPurchase_core(i(t),i(r),i(e))},h.randomDecimalForTesting=function(t){if(20*Math.random()<1)return n(0,0,0);var r=Math.random()>.5?1:-1;if(20*Math.random()<1)return n(r,0,1);var i=Math.floor(Math.random()*(t+1)),a=0===i?616*Math.random()-308:16*Math.random();Math.random()>.9&&(a=Math.trunc(a));var s=Math.pow(10,a);return Math.random()>.9&&(s=Math.trunc(s)),e(r,i,s)},h.affordGeometricSeries_core=function(t,r,i,e){var n=r.mul(i.pow(e));return h.floor(t.div(n).mul(i.sub(1)).add(1).log10().div(i.log10()))},h.sumGeometricSeries_core=function(t,r,i,e){return r.mul(i.pow(e)).mul(h.sub(1,i.pow(t))).div(h.sub(1,i))},h.affordArithmeticSeries_core=function(t,r,i,e){var n=r.add(e.mul(i)).sub(i.div(2)),a=n.pow(2);return n.neg().add(a.add(i.mul(t).mul(2)).sqrt()).div(i).floor()},h.sumArithmeticSeries_core=function(t,r,i,e){var n=r.add(e.mul(i));return t.div(2).mul(n.mul(2).plus(t.sub(1).mul(i)))},h.efficiencyOfPurchase_core=function(t,r,i){return t.div(r).add(t.div(i))},h.prototype.normalize=function(){if(0===this.sign||0===this.mag&&0===this.layer)return this.sign=0,this.mag=0,this.layer=0,this;if(0===this.layer&&this.mag<0&&(this.mag=-this.mag,this.sign=-this.sign),0===this.layer&&this.mag<1/9e15)return this.layer+=1,this.mag=Math.log10(this.mag),this;var r=Math.abs(this.mag),i=Math.sign(this.mag);if(r>=9e15)return this.layer+=1,this.mag=i*Math.log10(r),this;for(;r<t&&this.layer>0;)this.layer-=1,0===this.layer?this.mag=Math.pow(10,this.mag):(this.mag=i*Math.pow(10,r),r=Math.abs(this.mag),i=Math.sign(this.mag));return 0===this.layer&&(this.mag<0?(this.mag=-this.mag,this.sign=-this.sign):0===this.mag&&(this.sign=0)),this},h.prototype.fromComponents=function(t,r,i){return this.sign=t,this.layer=r,this.mag=i,this.normalize(),this},h.prototype.fromComponents_noNormalize=function(t,r,i){return this.sign=t,this.layer=r,this.mag=i,this},h.prototype.fromMantissaExponent=function(t,r){return this.layer=1,this.sign=Math.sign(t),t=Math.abs(t),this.mag=r+Math.log10(t),this.normalize(),this},h.prototype.fromMantissaExponent_noNormalize=function(t,r){return this.fromMantissaExponent(t,r),this},h.prototype.fromDecimal=function(t){return this.sign=t.sign,this.layer=t.layer,this.mag=t.mag,this},h.prototype.fromNumber=function(t){return this.mag=Math.abs(t),this.sign=Math.sign(t),this.layer=0,this.normalize(),this};h.prototype.fromString=function(t){var r=(t=t.replace(",","")).split("^^^");if(2===r.length){var n=parseFloat(r[0]),a=parseFloat(r[1]),o=1;if(2===(l=r[1].split(";")).length){o=parseFloat(l[1]);isFinite(o)||(o=1)}if(isFinite(n)&&isFinite(a)){var u=h.pentate(n,a,o);return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}}var g=t.split("^^");if(2===g.length){var l;n=parseFloat(g[0]),a=parseFloat(g[1]);if(2===(l=g[1].split(";")).length){o=parseFloat(l[1]);isFinite(o)||(o=1)}if(isFinite(n)&&isFinite(a)){u=h.tetrate(n,a,o);return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}}var m,f=t.split("^");if(2===f.length){n=parseFloat(f[0]);var c=parseFloat(f[1]);if(isFinite(n)&&isFinite(c)){u=h.pow(n,c);return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}}if(2===(m=(t=t.trim().toLowerCase()).split("pt")).length){n=10,a=parseFloat(m[0]),m[1]=m[1].replace("(",""),m[1]=m[1].replace(")","");o=parseFloat(m[1]);if(isFinite(o)||(o=1),isFinite(n)&&isFinite(a)){u=h.tetrate(n,a,o);return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}}if(2===(m=t.split("p")).length){n=10,a=parseFloat(m[0]),m[1]=m[1].replace("(",""),m[1]=m[1].replace(")","");o=parseFloat(m[1]);if(isFinite(o)||(o=1),isFinite(n)&&isFinite(a)){u=h.tetrate(n,a,o);return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}}var p=t.split("e"),y=p.length-1;if(0===y){var d=parseFloat(t);if(isFinite(d))return this.fromNumber(d)}else if(1===y){d=parseFloat(t);if(isFinite(d)&&0!==d)return this.fromNumber(d)}var M=t.split("e^");if(2===M.length){this.sign=1,"-"==M[0].charAt(0)&&(this.sign=-1);for(var b="",N=0;N<M[1].length;++N){var v=M[1].charCodeAt(N);if(!(v>=43&&v<=57||101===v))return this.layer=parseFloat(b),this.mag=parseFloat(M[1].substr(N+1)),this.normalize(),this;b+=M[1].charAt(N)}}if(y<1)return this.sign=0,this.layer=0,this.mag=0,this;var _=parseFloat(p[0]);if(0===_)return this.sign=0,this.layer=0,this.mag=0,this;c=parseFloat(p[p.length-1]);if(y>=2){var F=parseFloat(p[p.length-2]);isFinite(F)&&(c*=Math.sign(F),c+=s(F))}if(isFinite(_))if(1===y)this.sign=Math.sign(_),this.layer=1,this.mag=c+Math.log10(Math.abs(_));else{if(this.sign=Math.sign(_),this.layer=y,2===y){u=h.mul(e(1,2,c),i(_));return this.sign=u.sign,this.layer=u.layer,this.mag=u.mag,this}this.mag=c}else this.sign="-"===p[0]?-1:1,this.layer=y,this.mag=c;return this.normalize(),this},h.prototype.fromValue=function(t){return _instanceof(t,h)?this.fromDecimal(t):"number"==typeof t?this.fromNumber(t):"string"==typeof t?this.fromString(t):(this.sign=0,this.layer=0,this.mag=0,this)},h.prototype.toNumber=function(){return Number.isFinite(this.layer)?0===this.layer?this.sign*this.mag:1===this.layer?this.sign*Math.pow(10,this.mag):this.mag>0?this.sign>0?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:0:Number.NaN},h.prototype.mantissaWithDecimalPlaces=function(t){return isNaN(this.m)?Number.NaN:0===this.m?0:a(this.m,t)},h.prototype.magnitudeWithDecimalPlaces=function(t){return isNaN(this.mag)?Number.NaN:0===this.mag?0:a(this.mag,t)},h.prototype.toString=function(){return 0===this.layer?this.mag<1e21&&this.mag>1e-7||0===this.mag?(this.sign*this.mag).toString():this.m+"e"+this.e:1===this.layer?this.m+"e"+this.e:this.layer<=5?(-1===this.sign?"-":"")+"e".repeat(this.layer)+this.mag:(-1===this.sign?"-":"")+"(e^"+this.layer+")"+this.mag},h.prototype.toExponential=function(t){return 0===this.layer?(this.sign*this.mag).toExponential(t):this.toStringWithDecimalPlaces(t)},h.prototype.toFixed=function(t){return 0===this.layer?(this.sign*this.mag).toFixed(t):this.toStringWithDecimalPlaces(t)},h.prototype.toPrecision=function(t){return this.e<=-7?this.toExponential(t-1):t>this.e?this.toFixed(t-this.exponent-1):this.toExponential(t-1)},h.prototype.valueOf=function(){return this.toString()},h.prototype.toJSON=function(){return this.toString()},h.prototype.toStringWithDecimalPlaces=function(t){return 0===this.layer?this.mag<1e21&&this.mag>1e-7||0===this.mag?(this.sign*this.mag).toFixed(t):a(this.m,t)+"e"+a(this.e,t):1===this.layer?a(this.m,t)+"e"+a(this.e,t):this.layer<=5?(-1===this.sign?"-":"")+"e".repeat(this.layer)+a(this.mag,t):(-1===this.sign?"-":"")+"(e^"+this.layer+")"+a(this.mag,t)},h.prototype.abs=function(){return n(0===this.sign?0:1,this.layer,this.mag)},h.prototype.neg=function(){return n(-this.sign,this.layer,this.mag)},h.prototype.negate=function(){return this.neg()},h.prototype.negated=function(){return this.neg()},h.prototype.sign=function(){return this.sign},h.prototype.sgn=function(){return this.sign},h.prototype.round=function(){return this.mag<0?h.dZero:0===this.layer?e(this.sign,0,Math.round(this.mag)):this},h.prototype.floor=function(){return this.mag<0?h.dZero:0===this.layer?e(this.sign,0,Math.floor(this.mag)):this},h.prototype.ceil=function(){return this.mag<0?h.dZero:0===this.layer?e(this.sign,0,Math.ceil(this.mag)):this},h.prototype.trunc=function(){return this.mag<0?h.dZero:0===this.layer?e(this.sign,0,Math.trunc(this.mag)):this},h.prototype.add=function(t){var r,a,s=i(t);if(!Number.isFinite(this.layer))return this;if(!Number.isFinite(s.layer))return s;if(0===this.sign)return s;if(0===s.sign)return this;if(this.sign===-s.sign&&this.layer===s.layer&&this.mag===s.mag)return n(0,0,0);if(this.layer>=2||s.layer>=2)return this.maxabs(s);if(h.cmpabs(this,s)>0?(r=this,a=s):(r=s,a=this),0===r.layer&&0===a.layer)return i(r.sign*r.mag+a.sign*a.mag);var o=r.layer*Math.sign(r.mag),u=a.layer*Math.sign(a.mag);if(o-u>=2)return r;if(0===o&&-1===u){if(Math.abs(a.mag-Math.log10(r.mag))>17)return r;var g=Math.pow(10,Math.log10(r.mag)-a.mag),l=a.sign+r.sign*g;return e(Math.sign(l),1,a.mag+Math.log10(Math.abs(l)))}if(1===o&&0===u){if(Math.abs(r.mag-Math.log10(a.mag))>17)return r;g=Math.pow(10,r.mag-Math.log10(a.mag)),l=a.sign+r.sign*g;return e(Math.sign(l),1,Math.log10(a.mag)+Math.log10(Math.abs(l)))}if(Math.abs(r.mag-a.mag)>17)return r;g=Math.pow(10,r.mag-a.mag),l=a.sign+r.sign*g;return e(Math.sign(l),1,a.mag+Math.log10(Math.abs(l)))},h.prototype.plus=function(t){return this.add(t)},h.prototype.sub=function(t){return this.add(i(t).neg())},h.prototype.subtract=function(t){return this.sub(t)},h.prototype.minus=function(t){return this.sub(t)},h.prototype.mul=function(t){var r,a,s=i(t);if(!Number.isFinite(this.layer))return this;if(!Number.isFinite(s.layer))return s;if(0===this.sign||0===s.sign)return n(0,0,0);if(this.layer===s.layer&&this.mag===-s.mag)return n(this.sign*s.sign,0,1);if(this.layer>s.layer||this.layer==s.layer&&Math.abs(this.mag)>Math.abs(s.mag)?(r=this,a=s):(r=s,a=this),0===r.layer&&0===a.layer)return i(r.sign*a.sign*r.mag*a.mag);if(r.layer>=3||r.layer-a.layer>=2)return e(r.sign*a.sign,r.layer,r.mag);if(1===r.layer&&0===a.layer)return e(r.sign*a.sign,1,r.mag+Math.log10(a.mag));if(1===r.layer&&1===a.layer)return e(r.sign*a.sign,1,r.mag+a.mag);if(2===r.layer&&1===a.layer){var o=e(Math.sign(r.mag),r.layer-1,Math.abs(r.mag)).add(e(Math.sign(a.mag),a.layer-1,Math.abs(a.mag)));return e(r.sign*a.sign,o.layer+1,o.sign*o.mag)}if(2===r.layer&&2===a.layer){o=e(Math.sign(r.mag),r.layer-1,Math.abs(r.mag)).add(e(Math.sign(a.mag),a.layer-1,Math.abs(a.mag)));return e(r.sign*a.sign,o.layer+1,o.sign*o.mag)}throw Error("Bad arguments to mul: "+this+", "+t)},h.prototype.multiply=function(t){return this.mul(t)},h.prototype.times=function(t){return this.mul(t)},h.prototype.div=function(t){var r=i(t);return this.mul(r.recip())},h.prototype.divide=function(t){return this.div(t)},h.prototype.divideBy=function(t){return this.div(t)},h.prototype.dividedBy=function(t){return this.div(t)},h.prototype.recip=function(){return 0===this.mag?h.dNaN:0===this.layer?e(this.sign,0,1/this.mag):e(this.sign,this.layer,-this.mag)},h.prototype.reciprocal=function(){return this.recip()},h.prototype.reciprocate=function(){return this.recip()},h.prototype.cmp=function(t){var r=i(t);return this.sign>r.sign?1:this.sign<r.sign?-1:this.sign*this.cmpabs(t)},h.prototype.cmpabs=function(t){var r=i(t),e=this.mag>0?this.layer:-this.layer,n=r.mag>0?r.layer:-r.layer;return e>n?1:e<n?-1:this.mag>r.mag?1:this.mag<r.mag?-1:0},h.prototype.compare=function(t){return this.cmp(t)},h.prototype.eq=function(t){var r=i(t);return this.sign===r.sign&&this.layer===r.layer&&this.mag===r.mag},h.prototype.equals=function(t){return this.eq(t)},h.prototype.neq=function(t){return!this.eq(t)},h.prototype.notEquals=function(t){return this.neq(t)},h.prototype.lt=function(t){i(t);return-1===this.cmp(t)},h.prototype.lte=function(t){return!this.gt(t)},h.prototype.gt=function(t){i(t);return 1===this.cmp(t)},h.prototype.gte=function(t){return!this.lt(t)},h.prototype.max=function(t){var r=i(t);return this.lt(r)?r:this},h.prototype.min=function(t){var r=i(t);return this.gt(r)?r:this},h.prototype.maxabs=function(t){var r=i(t);return this.cmpabs(r)<0?r:this},h.prototype.minabs=function(t){var r=i(t);return this.cmpabs(r)>0?r:this},h.prototype.clamp=function(t,r){return this.max(t).min(r)},h.prototype.clampMin=function(t){return this.max(t)},h.prototype.clampMax=function(t){return this.min(t)},h.prototype.cmp_tolerance=function(t,r){var e=i(t);return this.eq_tolerance(e,r)?0:this.cmp(e)},h.prototype.compare_tolerance=function(t,r){return this.cmp_tolerance(t,r)},h.prototype.eq_tolerance=function(t,r){var e=i(t);if(null==r&&(r=1e-7),this.sign!==e.sign)return!1;if(Math.abs(this.layer-e.layer)>1)return!1;var n=this.mag,a=e.mag;return this.layer>e.layer&&(a=s(a)),this.layer<e.layer&&(n=s(n)),Math.abs(n-a)<=r*Math.max(Math.abs(n),Math.abs(a))},h.prototype.equals_tolerance=function(t,r){return this.eq_tolerance(t,r)},h.prototype.neq_tolerance=function(t,r){return!this.eq_tolerance(t,r)},h.prototype.notEquals_tolerance=function(t,r){return this.neq_tolerance(t,r)},h.prototype.lt_tolerance=function(t,r){var e=i(t);return!this.eq_tolerance(e,r)&&this.lt(e)},h.prototype.lte_tolerance=function(t,r){var e=i(t);return this.eq_tolerance(e,r)||this.lt(e)},h.prototype.gt_tolerance=function(t,r){var e=i(t);return!this.eq_tolerance(e,r)&&this.gt(e)},h.prototype.gte_tolerance=function(t,r){var e=i(t);return this.eq_tolerance(e,r)||this.gt(e)},h.prototype.pLog10=function(){return this.lt(h.dZero)?h.dZero:this.log10()},h.prototype.absLog10=function(){return 0===this.sign?h.dNaN:this.layer>0?e(Math.sign(this.mag),this.layer-1,Math.abs(this.mag)):e(1,0,Math.log10(this.mag))},h.prototype.log10=function(){return this.sign<=0?h.dNaN:this.layer>0?e(Math.sign(this.mag),this.layer-1,Math.abs(this.mag)):e(this.sign,0,Math.log10(this.mag))},h.prototype.log=function(t){return t=i(t),this.sign<=0?h.dNaN:t.sign<=0?h.dNaN:1===t.sign&&0===t.layer&&1===t.mag?h.dNaN:0===this.layer&&0===t.layer?e(this.sign,0,Math.log(this.mag)/Math.log(t.mag)):h.div(this.log10(),t.log10())},h.prototype.log2=function(){return this.sign<=0?h.dNaN:0===this.layer?e(this.sign,0,Math.log2(this.mag)):1===this.layer?e(Math.sign(this.mag),0,3.321928094887362*Math.abs(this.mag)):2===this.layer?e(Math.sign(this.mag),1,Math.abs(this.mag)+.5213902276543247):e(Math.sign(this.mag),this.layer-1,Math.abs(this.mag))},h.prototype.ln=function(){return this.sign<=0?h.dNaN:0===this.layer?e(this.sign,0,Math.log(this.mag)):1===this.layer?e(Math.sign(this.mag),0,2.302585092994046*Math.abs(this.mag)):2===this.layer?e(Math.sign(this.mag),1,Math.abs(this.mag)+.36221568869946325):e(Math.sign(this.mag),this.layer-1,Math.abs(this.mag))},h.prototype.logarithm=function(t){return this.log(t)},h.prototype.pow=function(t){var r=this,e=i(t);if(0===r.sign)return r;if(1===r.sign&&0===r.layer&&1===r.mag)return r;if(0===e.sign)return n(1,0,1);if(1===e.sign&&0===e.layer&&1===e.mag)return r;var a=r.absLog10().mul(e).pow10();return-1===this.sign&&e.toNumber()%2==1?a.neg():a},h.prototype.pow10=function(){if(!Number.isFinite(this.layer)||!Number.isFinite(this.mag))return h.dNaN;var t=this;if(0===t.layer){var r=Math.pow(10,t.sign*t.mag);if(Number.isFinite(r)&&Math.abs(r)>.1)return e(1,0,r);if(0===t.sign)return h.dOne;t=n(t.sign,t.layer+1,Math.log10(t.mag))}return t.sign>0&&t.mag>0?e(t.sign,t.layer+1,t.mag):t.sign<0&&t.mag>0?e(-t.sign,t.layer+1,-t.mag):h.dOne},h.prototype.pow_base=function(t){return i(t).pow(this)},h.prototype.root=function(t){var r=i(t);return this.pow(r.recip())},h.prototype.factorial=function(){return this.mag<0?this.toNumber().add(1).gamma():0===this.layer?this.add(1).gamma():1===this.layer?h.exp(h.mul(this,h.ln(this).sub(1))):h.exp(this)},h.prototype.gamma=function(){if(this.mag<0)return this.recip();if(0===this.layer){if(this.lt(n(1,0,24)))return i(function(t){if(!isFinite(t))return t;if(t<-50)return t===Math.trunc(t)?Number.NEGATIVE_INFINITY:0;for(var r=1;t<10;)r*=t,++t;var i=.9189385332046727;i+=(.5+(t-=1))*Math.log(t),i-=t;var e=t*t,n=t;return i+=1/(12*n),i+=1/(360*(n*=e)),i+=1/(1260*(n*=e)),i+=1/(1680*(n*=e)),i+=1/(1188*(n*=e)),i+=691/(360360*(n*=e)),i+=7/(1092*(n*=e)),i+=3617/(122400*(n*=e)),Math.exp(i)/r}(this.sign*this.mag));var t=this.mag-1,r=.9189385332046727;r+=(t+.5)*Math.log(t);var e=t*t,a=t,s=12*a,o=1/s,u=(r-=t)+o;if(u===r)return h.exp(r);if((u=(r=u)-(o=1/(s=360*(a*=e))))===r)return h.exp(r);r=u;var g=1/(s=1260*(a*=e));return r+=g,r-=g=1/(s=1680*(a*=e)),h.exp(r)}return 1===this.layer?h.exp(h.mul(this,h.ln(this).sub(1))):h.exp(this)},h.prototype.lngamma=function(){return this.gamma().ln()},h.prototype.exp=function(){return this.mag<0?h.dOne:0===this.layer&&this.mag<=709.7?i(Math.exp(this.sign*this.mag)):0===this.layer?e(1,1,this.sign*Math.log10(Math.E)*this.mag):1===this.layer?e(1,2,this.sign*(Math.log10(.4342944819032518)+this.mag)):e(1,this.layer+1,this.sign*this.mag)},h.prototype.sqr=function(){return this.pow(2)},h.prototype.sqrt=function(){if(0===this.layer)return i(Math.sqrt(this.sign*this.mag));if(1===this.layer)return e(1,2,Math.log10(this.mag)-.3010299956639812);var t=h.div(n(this.sign,this.layer-1,this.mag),n(1,0,2));return t.layer+=1,t.normalize(),t},h.prototype.cube=function(){return this.pow(3)},h.prototype.cbrt=function(){return this.pow(1/3)},h.prototype.tetrate=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:2,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n(1,0,1);if(t===Number.POSITIVE_INFINITY){var e=h.ln(this).neg();return e.lambertw().div(e)}if(t<0)return h.iteratedlog(r,this,-t);r=i(r);var a=t-(t=Math.trunc(t));0!==a&&(r.eq(h.dOne)?(++t,r=new h(a)):r=this.eq(10)?r.layeradd10(a):r.layeradd(a,this));for(var s=0;s<t;++s){if(r=this.pow(r),!isFinite(r.layer)||!isFinite(r.mag))return r;if(r.layer-this.layer>3)return n(r.sign,r.layer+(t-s-1),r.mag);if(s>100)return r}return r},h.prototype.iteratedexp=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:2,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n(1,0,1);return this.tetrate(t,r)},h.prototype.iteratedlog=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;if(r<0)return h.tetrate(t,-r,this);t=i(t);var e=i(this),n=r-(r=Math.trunc(r));if(e.layer-t.layer>3){var a=Math.min(r,e.layer-t.layer-3);r-=a,e.layer-=a}for(var s=0;s<r;++s){if(e=e.log(t),!isFinite(e.layer)||!isFinite(e.mag))return e;if(s>100)return e}return n>0&&n<1&&(e=t.eq(10)?e.layeradd10(-n):e.layeradd(-n,t)),e},h.prototype.slog=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:10;if(this.mag<0)return h.dNegOne;t=i(t);var r=0,e=i(this);if(e.layer-t.layer>3){var n=e.layer-t.layer-3;r+=n,e.layer-=n}for(var a=0;a<100;++a)if(e.lt(h.dZero))e=h.pow(t,e),r-=1;else{if(e.lte(h.dOne))return i(r+e.toNumber()-1);r+=1,e=h.log(e,t)}return i(r)},h.prototype.layeradd10=function(t){t=h.fromValue_noAlloc(t).toNumber();var r,e=i(this);t>=1&&(t-=r=Math.trunc(t),e.layer+=r);if(t<=-1&&(t-=r=Math.trunc(t),e.layer+=r,e.layer<0))for(var n=0;n<100;++n){if(e.layer++,e.mag=Math.log10(e.mag),!isFinite(e.mag))return e;if(e.layer>=0)break}if(t>0){for(var a=0;Number.isFinite(e.mag)&&e.mag<10;)e.mag=Math.pow(10,e.mag),++a;for(e.mag>1e10&&(e.mag=Math.log10(e.mag),e.layer++),(s=Math.log10(Math.log(1e10)/Math.log(e.mag),10))<t&&(e.mag=Math.log10(1e10),e.layer++,t-=s),e.mag=Math.pow(e.mag,Math.pow(10,t));a>0;)e.mag=Math.log10(e.mag),--a}else if(t<0){for(a=0;Number.isFinite(e.mag)&&e.mag<10;)e.mag=Math.pow(10,e.mag),++a;var s;for(e.mag>1e10&&(e.mag=Math.log10(e.mag),e.layer++),(s=Math.log10(1/Math.log10(e.mag)))>t&&(e.mag=1e10,e.layer--,t-=s),e.mag=Math.pow(e.mag,Math.pow(10,t));a>0;)e.mag=Math.log10(e.mag),--a}for(;e.layer<0;)e.layer++,e.mag=Math.log10(e.mag);return e.normalize(),e},h.prototype.layeradd=function(t,r){var i=this.slog(r).toNumber()+t;return i>=0?h.tetrate(r,i):Number.isFinite(i)?i>=-1?h.log(h.tetrate(r,i+1),r):void h.log(h.log(h.tetrate(r,i+2),r),r):h.dNaN},h.prototype.lambertw=function(){if(this.lt(-.3678794411710499))throw Error("lambertw is unimplemented for results less than -1, sorry!");return this.mag<0?i(o(this.toNumber())):0===this.layer?i(o(this.sign*this.mag)):1===this.layer?u(this):2===this.layer?u(this):this.layer>=3?n(this.sign,this.layer-1,this.mag):void 0};var u=function(t){var r,i,e,n,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1e-10;if(!Number.isFinite(t.mag))return t;if(0===t)return t;if(1===t)return.5671432904097838;h.abs(t);r=h.ln(t);for(var s=0;s<100;++s){if(i=h.exp(-r),e=r.sub(t.mul(i)),n=r.sub(e.div(r.add(1).sub(r.add(2).mul(e).div(h.mul(2,r).add(2))))),h.abs(n.sub(r)).lt(h.abs(n).mul(a)))return n;r=n}throw Error("Iteration failed to converge: "+t)};return h.prototype.ssqrt=function(){if(1==this.sign&&this.layer>=3)return n(this.sign,this.layer-1,this.mag);var t=this.ln();return t.div(t.lambertw())},h.prototype.pentate=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:2,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:n(1,0,1);r=i(r);var e=t-(t=Math.trunc(t));0!==e&&(r.eq(h.dOne)?(++t,r=new h(e)):r=this.eq(10)?r.layeradd10(e):r.layeradd(e,this));for(var a=0;a<t;++a){if(r=this.tetrate(r),!isFinite(r.layer)||!isFinite(r.mag))return r;if(a>10)return r}return r},h.prototype.sin=function(){return this.mag<0?this:0===this.layer?i(Math.sin(this.sign*this.mag)):n(0,0,0)},h.prototype.cos=function(){return this.mag<0?h.dOne:0===this.layer?i(Math.cos(this.sign*this.mag)):n(0,0,0)},h.prototype.tan=function(){return this.mag<0?this:0===this.layer?i(Math.tan(this.sign*this.mag)):n(0,0,0)},h.prototype.asin=function(){return this.mag<0?this:0===this.layer?i(Math.asin(this.sign*this.mag)):n(Number.NaN,Number.NaN,Number.NaN)},h.prototype.acos=function(){return this.mag<0?i(Math.acos(this.toNumber())):0===this.layer?i(Math.acos(this.sign*this.mag)):n(Number.NaN,Number.NaN,Number.NaN)},h.prototype.atan=function(){return this.mag<0?this:0===this.layer?i(Math.atan(this.sign*this.mag)):i(Math.atan(Infinity*this.sign))},h.prototype.sinh=function(){return this.exp().sub(this.negate().exp()).div(2)},h.prototype.cosh=function(){return this.exp().add(this.negate().exp()).div(2)},h.prototype.tanh=function(){return this.sinh().div(this.cosh())},h.prototype.asinh=function(){return h.ln(this.add(this.sqr().add(1).sqrt()))},h.prototype.acosh=function(){return h.ln(this.add(this.sqr().sub(1).sqrt()))},h.prototype.atanh=function(){return this.abs().gte(1)?n(Number.NaN,Number.NaN,Number.NaN):h.ln(this.add(1).div(i(1).sub(this))).div(2)},h.prototype.ascensionPenalty=function(t){return 0===t?this:this.root(h.pow(10,t))},h.prototype.egg=function(){return this.add(9)},h.prototype.lessThanOrEqualTo=function(t){return this.cmp(t)<1},h.prototype.lessThan=function(t){return this.cmp(t)<0},h.prototype.greaterThanOrEqualTo=function(t){return this.cmp(t)>-1},h.prototype.greaterThan=function(t){return this.cmp(t)>0},h}();return h.dZero=n(0,0,0),h.dOne=n(1,0,1),h.dNegOne=n(-1,0,1),h.dTwo=n(1,0,2),h.dTen=n(1,0,10),h.dNaN=n(Number.NaN,Number.NaN,Number.NaN),h.dInf=n(1,Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY),h.dNegInf=n(-1,Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY),h.dNumberMax=e(1,0,Number.MAX_VALUE),h.dNumberMin=e(1,0,Number.MIN_VALUE),h});
\ No newline at end of file
diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts
new file mode 100644
index 0000000..91fdd71
--- /dev/null
+++ b/src/lib/break_eternity.ts
@@ -0,0 +1,2717 @@
+/* eslint-disable @typescript-eslint/no-this-alias */
+/* eslint-disable prettier/prettier */
+export type CompareResult = -1 | 0 | 1;
+
+const MAX_SIGNIFICANT_DIGITS = 17; //Maximum number of digits of precision to assume in Number
+
+const EXP_LIMIT = 9e15; //If we're ABOVE this value, increase a layer. (9e15 is close to the largest integer that can fit in a Number.)
+
+const LAYER_DOWN: number = Math.log10(9e15);
+
+const FIRST_NEG_LAYER = 1 / 9e15; //At layer 0, smaller non-zero numbers than this become layer 1 numbers with negative mag. After that the pattern continues as normal.
+
+const NUMBER_EXP_MAX = 308; //The largest exponent that can appear in a Number, though not all mantissas are valid here.
+
+const NUMBER_EXP_MIN = -324; //The smallest exponent that can appear in a Number, though not all mantissas are valid here.
+
+const MAX_ES_IN_A_ROW = 5; //For default toString behaviour, when to swap from eee... to (e^n) syntax.
+
+const IGNORE_COMMAS = true;
+const COMMAS_ARE_DECIMAL_POINTS = false;
+
+const powerOf10 = (function () {
+  // We need this lookup table because Math.pow(10, exponent)
+  // when exponent's absolute value is large is slightly inaccurate.
+  // You can fix it with the power of math... or just make a lookup table.
+  // Faster AND simpler
+  const powersOf10: number[] = [];
+
+  for (let i = NUMBER_EXP_MIN + 1; i <= NUMBER_EXP_MAX; i++) {
+    powersOf10.push(Number("1e" + i));
+  }
+
+  const indexOf0InPowersOf10 = 323;
+  return function (power: number) {
+    return powersOf10[power + indexOf0InPowersOf10];
+  };
+})();
+
+const D = function D(value: DecimalSource): Decimal {
+  return Decimal.fromValue_noAlloc(value);
+};
+
+const FC = function (sign: number, layer: number, mag: number) {
+  return Decimal.fromComponents(sign, layer, mag);
+};
+
+const FC_NN = function FC_NN(sign: number, layer: number, mag: number) {
+  return Decimal.fromComponents_noNormalize(sign, layer, mag);
+};
+
+const ME = function ME(mantissa: number, exponent: number) {
+  return Decimal.fromMantissaExponent(mantissa, exponent);
+};
+
+const ME_NN = function ME_NN(mantissa: number, exponent: number) {
+  return Decimal.fromMantissaExponent_noNormalize(mantissa, exponent);
+};
+
+const decimalPlaces = function decimalPlaces(value: number, places: number): number {
+  const len = places + 1;
+  const numDigits = Math.ceil(Math.log10(Math.abs(value)));
+  const rounded = Math.round(value * Math.pow(10, len - numDigits)) * Math.pow(10, numDigits - len);
+  return parseFloat(rounded.toFixed(Math.max(len - numDigits, 0)));
+};
+
+const f_maglog10 = function (n: number) {
+  return Math.sign(n) * Math.log10(Math.abs(n));
+};
+
+//from HyperCalc source code
+const f_gamma = function (n: number) {
+  if (!isFinite(n)) {
+    return n;
+  }
+  if (n < -50) {
+    if (n === Math.trunc(n)) {
+      return Number.NEGATIVE_INFINITY;
+    }
+    return 0;
+  }
+
+  let scal1 = 1;
+  while (n < 10) {
+    scal1 = scal1 * n;
+    ++n;
+  }
+
+  n -= 1;
+  let l = 0.9189385332046727; //0.5*Math.log(2*Math.PI)
+  l = l + (n + 0.5) * Math.log(n);
+  l = l - n;
+  const n2 = n * n;
+  let np = n;
+  l = l + 1 / (12 * np);
+  np = np * n2;
+  l = l + 1 / (360 * np);
+  np = np * n2;
+  l = l + 1 / (1260 * np);
+  np = np * n2;
+  l = l + 1 / (1680 * np);
+  np = np * n2;
+  l = l + 1 / (1188 * np);
+  np = np * n2;
+  l = l + 691 / (360360 * np);
+  np = np * n2;
+  l = l + 7 / (1092 * np);
+  np = np * n2;
+  l = l + 3617 / (122400 * np);
+
+  return Math.exp(l) / scal1;
+};
+
+const _twopi = 6.2831853071795864769252842; // 2*pi
+const _EXPN1 = 0.36787944117144232159553; // exp(-1)
+const OMEGA = 0.56714329040978387299997; // W(1, 0)
+//from https://math.stackexchange.com/a/465183
+// The evaluation can become inaccurate very close to the branch point
+const f_lambertw = function (z: number, tol = 1e-10): number {
+  let w;
+  let wn;
+
+  if (!Number.isFinite(z)) {
+    return z;
+  }
+  if (z === 0) {
+    return z;
+  }
+  if (z === 1) {
+    return OMEGA;
+  }
+
+  if (z < 10) {
+    w = 0;
+  } else {
+    w = Math.log(z) - Math.log(Math.log(z));
+  }
+
+  for (let i = 0; i < 100; ++i) {
+    wn = (z * Math.exp(-w) + w * w) / (w + 1);
+    if (Math.abs(wn - w) < tol * Math.abs(wn)) {
+      return wn;
+    } else {
+      w = wn;
+    }
+  }
+
+  throw Error(`Iteration failed to converge: ${z.toString()}`);
+  //return Number.NaN;
+};
+
+//from https://github.com/scipy/scipy/blob/8dba340293fe20e62e173bdf2c10ae208286692f/scipy/special/lambertw.pxd
+// The evaluation can become inaccurate very close to the branch point
+// at ``-1/e``. In some corner cases, `lambertw` might currently
+// fail to converge, or can end up on the wrong branch.
+function d_lambertw(z: Decimal, tol = 1e-10): Decimal {
+  let w;
+  let ew, wew, wewz, wn;
+
+  if (!Number.isFinite(z.mag)) {
+    return z;
+  }
+  if (z === Decimal.dZero) {
+    return z;
+  }
+  if (z === Decimal.dOne) {
+    //Split out this case because the asymptotic series blows up
+    return D(OMEGA);
+  }
+
+  const absz = Decimal.abs(z);
+  //Get an initial guess for Halley's method
+  w = Decimal.ln(z);
+
+  //Halley's method; see 5.9 in [1]
+
+  for (let i = 0; i < 100; ++i) {
+    ew = Decimal.exp(-w);
+    wewz = w.sub(z.mul(ew));
+    wn = w.sub(wewz.div(w.add(1).sub(w.add(2).mul(wewz).div(Decimal.mul(2, w).add(2)))));
+    if (Decimal.abs(wn.sub(w)).lt(Decimal.abs(wn).mul(tol))) {
+      return wn;
+    } else {
+      w = wn;
+    }
+  }
+
+  throw Error(`Iteration failed to converge: ${z.toString()}`);
+  //return Decimal.dNaN;
+}
+
+export type DecimalSource = Decimal | number | string;
+
+/**
+ * The Decimal's value is simply mantissa * 10^exponent.
+ */
+export default class Decimal {
+  public static readonly dZero = FC_NN(0, 0, 0);
+  public static readonly dOne = FC_NN(1, 0, 1);
+  public static readonly dNegOne = FC_NN(-1, 0, 1);
+  public static readonly dTwo = FC_NN(1, 0, 2);
+  public static readonly dTen = FC_NN(1, 0, 10);
+  public static readonly dNaN = FC_NN(Number.NaN, Number.NaN, Number.NaN);
+  public static readonly dInf = FC_NN(1, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
+  public static readonly dNegInf = FC_NN(-1, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
+  public static readonly dNumberMax = FC(1, 0, Number.MAX_VALUE);
+  public static readonly dNumberMin = FC(1, 0, Number.MIN_VALUE);
+
+  public sign: number = Number.NaN;
+  public mag: number = Number.NaN;
+  public layer: number = Number.NaN;
+
+  constructor(value?: DecimalSource) {
+    this.sign = Number.NaN;
+    this.layer = Number.NaN;
+    this.mag = Number.NaN;
+
+    if (value instanceof Decimal) {
+      this.fromDecimal(value);
+    } else if (typeof value === "number") {
+      this.fromNumber(value);
+    } else if (typeof value === "string") {
+      this.fromString(value);
+    } else {
+      this.sign = 0;
+      this.layer = 0;
+      this.mag = 0;
+    }
+  }
+
+  get m(): number {
+    if (this.sign === 0) {
+      return 0;
+    } else if (this.layer === 0) {
+      const exp = Math.floor(Math.log10(this.mag));
+      //handle special case 5e-324
+      let man;
+      if (this.mag === 5e-324) {
+        man = 5;
+      } else {
+        man = this.mag / powerOf10(exp);
+      }
+      return this.sign * man;
+    } else if (this.layer === 1) {
+      const residue = this.mag - Math.floor(this.mag);
+      return this.sign * Math.pow(10, residue);
+    } else {
+      //mantissa stops being relevant past 1e9e15 / ee15.954
+      return this.sign;
+    }
+  }
+
+  set m(value: number) {
+    if (this.layer <= 2) {
+      this.fromMantissaExponent(value, this.e);
+    } else {
+      //don't even pretend mantissa is meaningful
+      this.sign = Math.sign(value);
+      if (this.sign === 0) {
+        this.layer === 0;
+        this.exponent === 0;
+      }
+    }
+  }
+
+  get e(): number {
+    if (this.sign === 0) {
+      return 0;
+    } else if (this.layer === 0) {
+      return Math.floor(Math.log10(this.mag));
+    } else if (this.layer === 1) {
+      return Math.floor(this.mag);
+    } else if (this.layer === 2) {
+      return Math.floor(Math.sign(this.mag) * Math.pow(10, Math.abs(this.mag)));
+    } else {
+      return this.mag * Number.POSITIVE_INFINITY;
+    }
+  }
+  set e(value: number) {
+    this.fromMantissaExponent(this.m, value);
+  }
+
+  get s(): number {
+    return this.sign;
+  }
+  set s(value: number) {
+    if (value === 0) {
+      this.sign = 0;
+      this.layer = 0;
+      this.mag = 0;
+    } else {
+      this.sign = value;
+    }
+  }
+
+  // Object.defineProperty(Decimal.prototype, "mantissa", {
+  get mantissa(): number {
+    return this.m;
+  }
+
+  set mantissa(value: number) {
+    this.m = value;
+  }
+
+  get exponent(): number {
+    return this.e;
+  }
+  set exponent(value: number) {
+    this.e = value;
+  }
+
+  public static fromComponents(sign: number, layer: number, mag: number): Decimal {
+    return new Decimal().fromComponents(sign, layer, mag);
+  }
+
+  public static fromComponents_noNormalize(sign: number, layer: number, mag: number): Decimal {
+    return new Decimal().fromComponents_noNormalize(sign, layer, mag);
+  }
+
+  public static fromMantissaExponent(mantissa: number, exponent: number): Decimal {
+    return new Decimal().fromMantissaExponent(mantissa, exponent);
+  }
+
+  public static fromMantissaExponent_noNormalize(mantissa: number, exponent: number): Decimal {
+    return new Decimal().fromMantissaExponent_noNormalize(mantissa, exponent);
+  }
+
+  public static fromDecimal(value: Decimal): Decimal {
+    return new Decimal().fromDecimal(value);
+  }
+
+  public static fromNumber(value: number): Decimal {
+    return new Decimal().fromNumber(value);
+  }
+
+  public static fromString(value: string): Decimal {
+    return new Decimal().fromString(value);
+  }
+
+  public static fromValue(value: DecimalSource): Decimal {
+    return new Decimal().fromValue(value);
+  }
+
+  public static fromValue_noAlloc(value: DecimalSource): Decimal {
+    return value instanceof Decimal ? value : new Decimal(value);
+  }
+
+  public static abs(value: DecimalSource): Decimal {
+    return D(value).abs();
+  }
+
+  public static neg(value: DecimalSource): Decimal {
+    return D(value).neg();
+  }
+
+  public static negate(value: DecimalSource): Decimal {
+    return D(value).neg();
+  }
+
+  public static negated(value: DecimalSource): Decimal {
+    return D(value).neg();
+  }
+
+  public static sign(value: DecimalSource): number {
+    return D(value).sign;
+  }
+
+  public static sgn(value: DecimalSource): number {
+    return D(value).sign;
+  }
+
+  public static round(value: DecimalSource): Decimal {
+    return D(value).round();
+  }
+
+  public static floor(value: DecimalSource): Decimal {
+    return D(value).floor();
+  }
+
+  public static ceil(value: DecimalSource): Decimal {
+    return D(value).ceil();
+  }
+
+  public static trunc(value: DecimalSource): Decimal {
+    return D(value).trunc();
+  }
+
+  public static add(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).add(other);
+  }
+
+  public static plus(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).add(other);
+  }
+
+  public static sub(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).sub(other);
+  }
+
+  public static subtract(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).sub(other);
+  }
+
+  public static minus(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).sub(other);
+  }
+
+  public static mul(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).mul(other);
+  }
+
+  public static multiply(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).mul(other);
+  }
+
+  public static times(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).mul(other);
+  }
+
+  public static div(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).div(other);
+  }
+
+  public static divide(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).div(other);
+  }
+
+  public static recip(value: DecimalSource): Decimal {
+    return D(value).recip();
+  }
+
+  public static reciprocal(value: DecimalSource): Decimal {
+    return D(value).recip();
+  }
+
+  public static reciprocate(value: DecimalSource): Decimal {
+    return D(value).reciprocate();
+  }
+
+  public static cmp(value: DecimalSource, other: DecimalSource): CompareResult {
+    return D(value).cmp(other);
+  }
+
+  public static cmpabs(value: DecimalSource, other: DecimalSource): CompareResult {
+    return D(value).cmpabs(other);
+  }
+
+  public static compare(value: DecimalSource, other: DecimalSource): CompareResult {
+    return D(value).cmp(other);
+  }
+
+  public static eq(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).eq(other);
+  }
+
+  public static equals(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).eq(other);
+  }
+
+  public static neq(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).neq(other);
+  }
+
+  public static notEquals(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).notEquals(other);
+  }
+
+  public static lt(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).lt(other);
+  }
+
+  public static lte(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).lte(other);
+  }
+
+  public static gt(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).gt(other);
+  }
+
+  public static gte(value: DecimalSource, other: DecimalSource): boolean {
+    return D(value).gte(other);
+  }
+
+  public static max(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).max(other);
+  }
+
+  public static min(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).min(other);
+  }
+
+  public static minabs(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).minabs(other);
+  }
+
+  public static maxabs(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).maxabs(other);
+  }
+
+  public static clamp(value: DecimalSource, min: DecimalSource, max: DecimalSource): Decimal {
+    return D(value).clamp(min, max);
+  }
+
+  public static clampMin(value: DecimalSource, min: DecimalSource): Decimal {
+    return D(value).clampMin(min);
+  }
+
+  public static clampMax(value: DecimalSource, max: DecimalSource): Decimal {
+    return D(value).clampMax(max);
+  }
+
+  public static cmp_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): CompareResult {
+    return D(value).cmp_tolerance(other, tolerance);
+  }
+
+  public static compare_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): CompareResult {
+    return D(value).cmp_tolerance(other, tolerance);
+  }
+
+  public static eq_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).eq_tolerance(other, tolerance);
+  }
+
+  public static equals_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).eq_tolerance(other, tolerance);
+  }
+
+  public static neq_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).neq_tolerance(other, tolerance);
+  }
+
+  public static notEquals_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).notEquals_tolerance(other, tolerance);
+  }
+
+  public static lt_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).lt_tolerance(other, tolerance);
+  }
+
+  public static lte_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).lte_tolerance(other, tolerance);
+  }
+
+  public static gt_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).gt_tolerance(other, tolerance);
+  }
+
+  public static gte_tolerance(
+    value: DecimalSource,
+    other: DecimalSource,
+    tolerance: number
+  ): boolean {
+    return D(value).gte_tolerance(other, tolerance);
+  }
+
+  public static pLog10(value: DecimalSource): Decimal {
+    return D(value).pLog10();
+  }
+
+  public static absLog10(value: DecimalSource): Decimal {
+    return D(value).absLog10();
+  }
+
+  public static log10(value: DecimalSource): Decimal {
+    return D(value).log10();
+  }
+
+  public static log(value: DecimalSource, base: DecimalSource): Decimal {
+    return D(value).log(base);
+  }
+
+  public static log2(value: DecimalSource): Decimal {
+    return D(value).log2();
+  }
+
+  public static ln(value: DecimalSource): Decimal {
+    return D(value).ln();
+  }
+
+  public static logarithm(value: DecimalSource, base: DecimalSource): Decimal {
+    return D(value).logarithm(base);
+  }
+
+  public static pow(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).pow(other);
+  }
+
+  public static pow10(value: DecimalSource): Decimal {
+    return D(value).pow10();
+  }
+
+  public static root(value: DecimalSource, other: DecimalSource): Decimal {
+    return D(value).root(other);
+  }
+
+  public static factorial(value: DecimalSource, _other?: never): Decimal {
+    return D(value).factorial();
+  }
+
+  public static gamma(value: DecimalSource, _other?: never): Decimal {
+    return D(value).gamma();
+  }
+
+  public static lngamma(value: DecimalSource, _other?: never): Decimal {
+    return D(value).lngamma();
+  }
+
+  public static exp(value: DecimalSource): Decimal {
+    return D(value).exp();
+  }
+
+  public static sqr(value: DecimalSource): Decimal {
+    return D(value).sqr();
+  }
+
+  public static sqrt(value: DecimalSource): Decimal {
+    return D(value).sqrt();
+  }
+
+  public static cube(value: DecimalSource): Decimal {
+    return D(value).cube();
+  }
+
+  public static cbrt(value: DecimalSource): Decimal {
+    return D(value).cbrt();
+  }
+
+  public static tetrate(
+    value: DecimalSource,
+    height = 2,
+    payload: DecimalSource = FC_NN(1, 0, 1)
+  ): Decimal {
+    return D(value).tetrate(height, payload);
+  }
+
+  public static iteratedexp(value: DecimalSource, height = 2, payload = FC_NN(1, 0, 1)): Decimal {
+    return D(value).iteratedexp(height, payload);
+  }
+
+  public static iteratedlog(value: DecimalSource, base: DecimalSource = 10, times = 1): Decimal {
+    return D(value).iteratedlog(base, times);
+  }
+
+  public static layeradd10(value: DecimalSource, diff: DecimalSource): Decimal {
+    return D(value).layeradd10(diff);
+  }
+
+  public static layeradd(value: DecimalSource, diff: number, base = 10): Decimal {
+    return D(value).layeradd(diff, base);
+  }
+
+  public static slog(value: DecimalSource, base = 10): Decimal {
+    return D(value).slog(base);
+  }
+
+  public static lambertw(value: DecimalSource): Decimal {
+    return D(value).lambertw();
+  }
+
+  public static ssqrt(value: DecimalSource): Decimal {
+    return D(value).ssqrt();
+  }
+
+  public static pentate(
+    value: DecimalSource,
+    height = 2,
+    payload: DecimalSource = FC_NN(1, 0, 1)
+  ): Decimal {
+    return D(value).pentate(height, payload);
+  }
+
+  /**
+   * If you're willing to spend 'resourcesAvailable' and want to buy something
+   * with exponentially increasing cost each purchase (start at priceStart,
+   * multiply by priceRatio, already own currentOwned), how much of it can you buy?
+   * Adapted from Trimps source code.
+   */
+
+  public static affordGeometricSeries(
+    resourcesAvailable: DecimalSource,
+    priceStart: DecimalSource,
+    priceRatio: DecimalSource,
+    currentOwned: DecimalSource
+  ): Decimal {
+    return this.affordGeometricSeries_core(
+      D(resourcesAvailable),
+      D(priceStart),
+      D(priceRatio),
+      currentOwned
+    );
+  }
+  /**
+   * How much resource would it cost to buy (numItems) items if you already have currentOwned,
+   * the initial price is priceStart and it multiplies by priceRatio each purchase?
+   */
+
+  public static sumGeometricSeries(
+    numItems: DecimalSource,
+    priceStart: DecimalSource,
+    priceRatio: DecimalSource,
+    currentOwned: DecimalSource
+  ): Decimal {
+    return this.sumGeometricSeries_core(numItems, D(priceStart), D(priceRatio), currentOwned);
+  }
+  /**
+   * If you're willing to spend 'resourcesAvailable' and want to buy something with additively
+   * increasing cost each purchase (start at priceStart, add by priceAdd, already own currentOwned),
+   * how much of it can you buy?
+   */
+
+  public static affordArithmeticSeries(
+    resourcesAvailable: DecimalSource,
+    priceStart: DecimalSource,
+    priceAdd: DecimalSource,
+    currentOwned: DecimalSource
+  ): Decimal {
+    return this.affordArithmeticSeries_core(
+      D(resourcesAvailable),
+      D(priceStart),
+      D(priceAdd),
+      D(currentOwned)
+    );
+  }
+  /**
+   * How much resource would it cost to buy (numItems) items if you already have currentOwned,
+   * the initial price is priceStart and it adds priceAdd each purchase?
+   * Adapted from http://www.mathwords.com/a/arithmetic_series.htm
+   */
+
+  public static sumArithmeticSeries(
+    numItems: DecimalSource,
+    priceStart: DecimalSource,
+    priceAdd: DecimalSource,
+    currentOwned: DecimalSource
+  ): Decimal {
+    return this.sumArithmeticSeries_core(D(numItems), D(priceStart), D(priceAdd), D(currentOwned));
+  }
+  /**
+   * When comparing two purchases that cost (resource) and increase your resource/sec by (deltaRpS),
+   * the lowest efficiency score is the better one to purchase.
+   * From Frozen Cookies:
+   * http://cookieclicker.wikia.com/wiki/Frozen_Cookies_(JavaScript_Add-on)#Efficiency.3F_What.27s_that.3F
+   */
+
+  public static efficiencyOfPurchase(
+    cost: DecimalSource,
+    currentRpS: DecimalSource,
+    deltaRpS: DecimalSource
+  ): Decimal {
+    return this.efficiencyOfPurchase_core(D(cost), D(currentRpS), D(deltaRpS));
+  }
+
+  public static randomDecimalForTesting(maxLayers: number): Decimal {
+    // NOTE: This doesn't follow any kind of sane random distribution, so use this for testing purposes only.
+    //5% of the time, return 0
+    if (Math.random() * 20 < 1) {
+      return FC_NN(0, 0, 0);
+    }
+
+    const randomsign = Math.random() > 0.5 ? 1 : -1;
+
+    //5% of the time, return 1 or -1
+    if (Math.random() * 20 < 1) {
+      return FC_NN(randomsign, 0, 1);
+    }
+
+    //pick a random layer
+    const layer = Math.floor(Math.random() * (maxLayers + 1));
+
+    let randomexp = layer === 0 ? Math.random() * 616 - 308 : Math.random() * 16;
+    //10% of the time, make it a simple power of 10
+    if (Math.random() > 0.9) {
+      randomexp = Math.trunc(randomexp);
+    }
+    let randommag = Math.pow(10, randomexp);
+    //10% of the time, trunc mag
+    if (Math.random() > 0.9) {
+      randommag = Math.trunc(randommag);
+    }
+    return FC(randomsign, layer, randommag);
+  }
+
+  public static affordGeometricSeries_core(
+    resourcesAvailable: Decimal,
+    priceStart: Decimal,
+    priceRatio: Decimal,
+    currentOwned: DecimalSource
+  ): Decimal {
+    const actualStart = priceStart.mul(priceRatio.pow(currentOwned));
+    return Decimal.floor(
+      resourcesAvailable
+        .div(actualStart)
+        .mul(priceRatio.sub(1))
+        .add(1)
+        .log10()
+        .div(priceRatio.log10())
+    );
+  }
+
+  public static sumGeometricSeries_core(
+    numItems: DecimalSource,
+    priceStart: Decimal,
+    priceRatio: Decimal,
+    currentOwned: DecimalSource
+  ): Decimal {
+    return priceStart
+      .mul(priceRatio.pow(currentOwned))
+      .mul(Decimal.sub(1, priceRatio.pow(numItems)))
+      .div(Decimal.sub(1, priceRatio));
+  }
+
+  public static affordArithmeticSeries_core(
+    resourcesAvailable: Decimal,
+    priceStart: Decimal,
+    priceAdd: Decimal,
+    currentOwned: Decimal
+  ): Decimal {
+    // n = (-(a-d/2) + sqrt((a-d/2)^2+2dS))/d
+    // where a is actualStart, d is priceAdd and S is resourcesAvailable
+    // then floor it and you're done!
+    const actualStart = priceStart.add(currentOwned.mul(priceAdd));
+    const b = actualStart.sub(priceAdd.div(2));
+    const b2 = b.pow(2);
+    return b
+      .neg()
+      .add(b2.add(priceAdd.mul(resourcesAvailable).mul(2)).sqrt())
+      .div(priceAdd)
+      .floor();
+  }
+
+  public static sumArithmeticSeries_core(
+    numItems: Decimal,
+    priceStart: Decimal,
+    priceAdd: Decimal,
+    currentOwned: Decimal
+  ): Decimal {
+    const actualStart = priceStart.add(currentOwned.mul(priceAdd)); // (n/2)*(2*a+(n-1)*d)
+
+    return numItems.div(2).mul(actualStart.mul(2).plus(numItems.sub(1).mul(priceAdd)));
+  }
+
+  public static efficiencyOfPurchase_core(
+    cost: Decimal,
+    currentRpS: Decimal,
+    deltaRpS: Decimal
+  ): Decimal {
+    return cost.div(currentRpS).add(cost.div(deltaRpS));
+  }
+
+  public normalize(): this {
+    /*
+    PSEUDOCODE:
+    Whenever we are partially 0 (sign is 0 or mag and layer is 0), make it fully 0.
+    Whenever we are at or hit layer 0, extract sign from negative mag.
+    If layer === 0 and mag < FIRST_NEG_LAYER (1/9e15), shift to 'first negative layer' (add layer, log10 mag).
+    While abs(mag) > EXP_LIMIT (9e15), layer += 1, mag = maglog10(mag).
+    While abs(mag) < LAYER_DOWN (15.954) and layer > 0, layer -= 1, mag = pow(10, mag).
+
+    When we're done, all of the following should be true OR one of the numbers is not IsFinite OR layer is not IsInteger (error state):
+    Any 0 is totally zero (0, 0, 0).
+    Anything layer 0 has mag 0 OR mag > 1/9e15 and < 9e15.
+    Anything layer 1 or higher has abs(mag) >= 15.954 and < 9e15.
+    We will assume in calculations that all Decimals are either erroneous or satisfy these criteria. (Otherwise: Garbage in, garbage out.)
+    */
+    if (this.sign === 0 || (this.mag === 0 && this.layer === 0)) {
+      this.sign = 0;
+      this.mag = 0;
+      this.layer = 0;
+      return this;
+    }
+
+    if (this.layer === 0 && this.mag < 0) {
+      //extract sign from negative mag at layer 0
+      this.mag = -this.mag;
+      this.sign = -this.sign;
+    }
+
+    //Handle shifting from layer 0 to negative layers.
+    if (this.layer === 0 && this.mag < FIRST_NEG_LAYER) {
+      this.layer += 1;
+      this.mag = Math.log10(this.mag);
+      return this;
+    }
+
+    let absmag = Math.abs(this.mag);
+    let signmag = Math.sign(this.mag);
+
+    if (absmag >= EXP_LIMIT) {
+      this.layer += 1;
+      this.mag = signmag * Math.log10(absmag);
+      return this;
+    } else {
+      while (absmag < LAYER_DOWN && this.layer > 0) {
+        this.layer -= 1;
+        if (this.layer === 0) {
+          this.mag = Math.pow(10, this.mag);
+        } else {
+          this.mag = signmag * Math.pow(10, absmag);
+          absmag = Math.abs(this.mag);
+          signmag = Math.sign(this.mag);
+        }
+      }
+      if (this.layer === 0) {
+        if (this.mag < 0) {
+          //extract sign from negative mag at layer 0
+          this.mag = -this.mag;
+          this.sign = -this.sign;
+        } else if (this.mag === 0) {
+          //excessive rounding can give us all zeroes
+          this.sign = 0;
+        }
+      }
+    }
+
+    return this;
+  }
+
+  public fromComponents(sign: number, layer: number, mag: number): this {
+    this.sign = sign;
+    this.layer = layer;
+    this.mag = mag;
+
+    this.normalize();
+    return this;
+  }
+
+  public fromComponents_noNormalize(sign: number, layer: number, mag: number): this {
+    this.sign = sign;
+    this.layer = layer;
+    this.mag = mag;
+    return this;
+  }
+
+  public fromMantissaExponent(mantissa: number, exponent: number): this {
+    this.layer = 1;
+    this.sign = Math.sign(mantissa);
+    mantissa = Math.abs(mantissa);
+    this.mag = exponent + Math.log10(mantissa);
+
+    this.normalize();
+    return this;
+  }
+
+  public fromMantissaExponent_noNormalize(mantissa: number, exponent: number): this {
+    //The idea of 'normalizing' a break_infinity.js style Decimal doesn't really apply. So just do the same thing.
+    this.fromMantissaExponent(mantissa, exponent);
+    return this;
+  }
+
+  public fromDecimal(value: Decimal): this {
+    this.sign = value.sign;
+    this.layer = value.layer;
+    this.mag = value.mag;
+    return this;
+  }
+
+  public fromNumber(value: number): this {
+    this.mag = Math.abs(value);
+    this.sign = Math.sign(value);
+    this.layer = 0;
+    this.normalize();
+    return this;
+  }
+
+  public fromString(value: string): Decimal {
+    if (IGNORE_COMMAS) {
+      value = value.replace(",", "");
+    } else if (COMMAS_ARE_DECIMAL_POINTS) {
+      value = value.replace(",", ".");
+    }
+
+    //Handle x^^^y format.
+    const pentationparts = value.split("^^^");
+    if (pentationparts.length === 2) {
+      const base = parseFloat(pentationparts[0]);
+      const height = parseFloat(pentationparts[1]);
+      const heightparts = pentationparts[1].split(";");
+      let payload = 1;
+      if (heightparts.length === 2) {
+        payload = parseFloat(heightparts[1]);
+        if (!isFinite(payload)) {
+          payload = 1;
+        }
+      }
+      if (isFinite(base) && isFinite(height)) {
+        const result = Decimal.pentate(base, height, payload);
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      }
+    }
+
+    //Handle x^^y format.
+    const tetrationparts = value.split("^^");
+    if (tetrationparts.length === 2) {
+      const base = parseFloat(tetrationparts[0]);
+      const height = parseFloat(tetrationparts[1]);
+      const heightparts = tetrationparts[1].split(";");
+      let payload = 1;
+      if (heightparts.length === 2) {
+        payload = parseFloat(heightparts[1]);
+        if (!isFinite(payload)) {
+          payload = 1;
+        }
+      }
+      if (isFinite(base) && isFinite(height)) {
+        const result = Decimal.tetrate(base, height, payload);
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      }
+    }
+
+    //Handle x^y format.
+    const powparts = value.split("^");
+    if (powparts.length === 2) {
+      const base = parseFloat(powparts[0]);
+      const exponent = parseFloat(powparts[1]);
+      if (isFinite(base) && isFinite(exponent)) {
+        const result = Decimal.pow(base, exponent);
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      }
+    }
+
+    //Handle various cases involving it being a Big Number.
+    value = value.trim().toLowerCase();
+
+    //handle X PT Y format.
+    let base;
+    let height;
+    let ptparts = value.split("pt");
+    if (ptparts.length === 2) {
+      base = 10;
+      height = parseFloat(ptparts[0]);
+      ptparts[1] = ptparts[1].replace("(", "");
+      ptparts[1] = ptparts[1].replace(")", "");
+      let payload = parseFloat(ptparts[1]);
+      if (!isFinite(payload)) {
+        payload = 1;
+      }
+      if (isFinite(base) && isFinite(height)) {
+        const result = Decimal.tetrate(base, height, payload);
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      }
+    }
+
+    //handle XpY format (it's the same thing just with p).
+    ptparts = value.split("p");
+    if (ptparts.length === 2) {
+      base = 10;
+      height = parseFloat(ptparts[0]);
+      ptparts[1] = ptparts[1].replace("(", "");
+      ptparts[1] = ptparts[1].replace(")", "");
+      let payload = parseFloat(ptparts[1]);
+      if (!isFinite(payload)) {
+        payload = 1;
+      }
+      if (isFinite(base) && isFinite(height)) {
+        const result = Decimal.tetrate(base, height, payload);
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      }
+    }
+
+    const parts = value.split("e");
+    const ecount = parts.length - 1;
+
+    //Handle numbers that are exactly floats (0 or 1 es).
+    if (ecount === 0) {
+      const numberAttempt = parseFloat(value);
+      if (isFinite(numberAttempt)) {
+        return this.fromNumber(numberAttempt);
+      }
+    } else if (ecount === 1) {
+      //Very small numbers ("2e-3000" and so on) may look like valid floats but round to 0.
+      const numberAttempt = parseFloat(value);
+      if (isFinite(numberAttempt) && numberAttempt !== 0) {
+        return this.fromNumber(numberAttempt);
+      }
+    }
+
+    //Handle new (e^N)X format.
+    const newparts = value.split("e^");
+    if (newparts.length === 2) {
+      this.sign = 1;
+      if (newparts[0].charAt(0) == "-") {
+        this.sign = -1;
+      }
+      let layerstring = "";
+      for (let i = 0; i < newparts[1].length; ++i) {
+        const chrcode = newparts[1].charCodeAt(i);
+        if ((chrcode >= 43 && chrcode <= 57) || chrcode === 101) {
+          //is "0" to "9" or "+" or "-" or "." or "e" (or "," or "/")
+          layerstring += newparts[1].charAt(i);
+        } //we found the end of the layer count
+        else {
+          this.layer = parseFloat(layerstring);
+          this.mag = parseFloat(newparts[1].substr(i + 1));
+          this.normalize();
+          return this;
+        }
+      }
+    }
+
+    if (ecount < 1) {
+      this.sign = 0;
+      this.layer = 0;
+      this.mag = 0;
+      return this;
+    }
+    const mantissa = parseFloat(parts[0]);
+    if (mantissa === 0) {
+      this.sign = 0;
+      this.layer = 0;
+      this.mag = 0;
+      return this;
+    }
+    let exponent = parseFloat(parts[parts.length - 1]);
+    //handle numbers like AeBeC and AeeeeBeC
+    if (ecount >= 2) {
+      const me = parseFloat(parts[parts.length - 2]);
+      if (isFinite(me)) {
+        exponent *= Math.sign(me);
+        exponent += f_maglog10(me);
+      }
+    }
+
+    //Handle numbers written like eee... (N es) X
+    if (!isFinite(mantissa)) {
+      this.sign = parts[0] === "-" ? -1 : 1;
+      this.layer = ecount;
+      this.mag = exponent;
+    }
+    //Handle numbers written like XeY
+    else if (ecount === 1) {
+      this.sign = Math.sign(mantissa);
+      this.layer = 1;
+      //Example: 2e10 is equal to 10^log10(2e10) which is equal to 10^(10+log10(2))
+      this.mag = exponent + Math.log10(Math.abs(mantissa));
+    }
+    //Handle numbers written like Xeee... (N es) Y
+    else {
+      this.sign = Math.sign(mantissa);
+      this.layer = ecount;
+      if (ecount === 2) {
+        const result = Decimal.mul(FC(1, 2, exponent), D(mantissa));
+        this.sign = result.sign;
+        this.layer = result.layer;
+        this.mag = result.mag;
+        return this;
+      } else {
+        //at eee and above, mantissa is too small to be recognizable!
+        this.mag = exponent;
+      }
+    }
+
+    this.normalize();
+    return this;
+  }
+
+  public fromValue(value: DecimalSource): Decimal {
+    if (value instanceof Decimal) {
+      return this.fromDecimal(value);
+    }
+
+    if (typeof value === "number") {
+      return this.fromNumber(value);
+    }
+
+    if (typeof value === "string") {
+      return this.fromString(value);
+    }
+
+    this.sign = 0;
+    this.layer = 0;
+    this.mag = 0;
+    return this;
+  }
+
+  public toNumber(): number {
+    if (!Number.isFinite(this.layer)) {
+      return Number.NaN;
+    }
+    if (this.layer === 0) {
+      return this.sign * this.mag;
+    } else if (this.layer === 1) {
+      return this.sign * Math.pow(10, this.mag);
+    } //overflow for any normalized Decimal
+    else {
+      return this.mag > 0
+        ? this.sign > 0
+          ? Number.POSITIVE_INFINITY
+          : Number.NEGATIVE_INFINITY
+        : 0;
+    }
+  }
+
+  public mantissaWithDecimalPlaces(places: number): number {
+    // https://stackoverflow.com/a/37425022
+    if (isNaN(this.m)) {
+      return Number.NaN;
+    }
+
+    if (this.m === 0) {
+      return 0;
+    }
+
+    return decimalPlaces(this.m, places);
+  }
+
+  public magnitudeWithDecimalPlaces(places: number): number {
+    // https://stackoverflow.com/a/37425022
+    if (isNaN(this.mag)) {
+      return Number.NaN;
+    }
+
+    if (this.mag === 0) {
+      return 0;
+    }
+
+    return decimalPlaces(this.mag, places);
+  }
+
+  public toString(): string {
+    if (this.layer === 0) {
+      if ((this.mag < 1e21 && this.mag > 1e-7) || this.mag === 0) {
+        return (this.sign * this.mag).toString();
+      }
+      return this.m + "e" + this.e;
+    } else if (this.layer === 1) {
+      return this.m + "e" + this.e;
+    } else {
+      //layer 2+
+      if (this.layer <= MAX_ES_IN_A_ROW) {
+        return (this.sign === -1 ? "-" : "") + "e".repeat(this.layer) + this.mag;
+      } else {
+        return (this.sign === -1 ? "-" : "") + "(e^" + this.layer + ")" + this.mag;
+      }
+    }
+  }
+
+  public toExponential(places: number): string {
+    if (this.layer === 0) {
+      return (this.sign * this.mag).toExponential(places);
+    }
+    return this.toStringWithDecimalPlaces(places);
+  }
+
+  public toFixed(places: number): string {
+    if (this.layer === 0) {
+      return (this.sign * this.mag).toFixed(places);
+    }
+    return this.toStringWithDecimalPlaces(places);
+  }
+
+  public toPrecision(places: number): string {
+    if (this.e <= -7) {
+      return this.toExponential(places - 1);
+    }
+
+    if (places > this.e) {
+      return this.toFixed(places - this.exponent - 1);
+    }
+
+    return this.toExponential(places - 1);
+  }
+
+  public valueOf(): string {
+    return this.toString();
+  }
+
+  public toJSON(): string {
+    return this.toString();
+  }
+
+  public toStringWithDecimalPlaces(places: number): string {
+    if (this.layer === 0) {
+      if ((this.mag < 1e21 && this.mag > 1e-7) || this.mag === 0) {
+        return (this.sign * this.mag).toFixed(places);
+      }
+      return decimalPlaces(this.m, places) + "e" + decimalPlaces(this.e, places);
+    } else if (this.layer === 1) {
+      return decimalPlaces(this.m, places) + "e" + decimalPlaces(this.e, places);
+    } else {
+      //layer 2+
+      if (this.layer <= MAX_ES_IN_A_ROW) {
+        return (
+          (this.sign === -1 ? "-" : "") + "e".repeat(this.layer) + decimalPlaces(this.mag, places)
+        );
+      } else {
+        return (
+          (this.sign === -1 ? "-" : "") + "(e^" + this.layer + ")" + decimalPlaces(this.mag, places)
+        );
+      }
+    }
+  }
+
+  public abs(): Decimal {
+    return FC_NN(this.sign === 0 ? 0 : 1, this.layer, this.mag);
+  }
+
+  public neg(): Decimal {
+    return FC_NN(-this.sign, this.layer, this.mag);
+  }
+
+  public negate(): Decimal {
+    return this.neg();
+  }
+
+  public negated(): Decimal {
+    return this.neg();
+  }
+
+  // public sign () {
+  //     return this.sign;
+  //   }
+
+  public sgn(): number {
+    return this.sign;
+  }
+
+  public round(): this | Decimal {
+    if (this.mag < 0) {
+      return Decimal.dZero;
+    }
+    if (this.layer === 0) {
+      return FC(this.sign, 0, Math.round(this.mag));
+    }
+    return this;
+  }
+
+  public floor(): this | Decimal {
+    if (this.mag < 0) {
+      return Decimal.dZero;
+    }
+    if (this.layer === 0) {
+      return FC(this.sign, 0, Math.floor(this.mag));
+    }
+    return this;
+  }
+
+  public ceil(): this | Decimal {
+    if (this.mag < 0) {
+      return Decimal.dZero;
+    }
+    if (this.layer === 0) {
+      return FC(this.sign, 0, Math.ceil(this.mag));
+    }
+    return this;
+  }
+
+  public trunc(): this | Decimal {
+    if (this.mag < 0) {
+      return Decimal.dZero;
+    }
+    if (this.layer === 0) {
+      return FC(this.sign, 0, Math.trunc(this.mag));
+    }
+    return this;
+  }
+
+  public add(value: DecimalSource): this | Decimal {
+    const decimal = D(value);
+
+    //inf/nan check
+    if (!Number.isFinite(this.layer)) {
+      return this;
+    }
+    if (!Number.isFinite(decimal.layer)) {
+      return decimal;
+    }
+
+    //Special case - if one of the numbers is 0, return the other number.
+    if (this.sign === 0) {
+      return decimal;
+    }
+    if (decimal.sign === 0) {
+      return this;
+    }
+
+    //Special case - Adding a number to its negation produces 0, no matter how large.
+    if (this.sign === -decimal.sign && this.layer === decimal.layer && this.mag === decimal.mag) {
+      return FC_NN(0, 0, 0);
+    }
+
+    let a;
+    let b;
+
+    //Special case: If one of the numbers is layer 2 or higher, just take the bigger number.
+    if (this.layer >= 2 || decimal.layer >= 2) {
+      return this.maxabs(decimal);
+    }
+
+    if (Decimal.cmpabs(this, decimal) > 0) {
+      a = this;
+      b = decimal;
+    } else {
+      a = decimal;
+      b = this;
+    }
+
+    if (a.layer === 0 && b.layer === 0) {
+      return D(a.sign * a.mag + b.sign * b.mag);
+    }
+
+    const layera = a.layer * Math.sign(a.mag);
+    const layerb = b.layer * Math.sign(b.mag);
+
+    //If one of the numbers is 2+ layers higher than the other, just take the bigger number.
+    if (layera - layerb >= 2) {
+      return a;
+    }
+
+    if (layera === 0 && layerb === -1) {
+      if (Math.abs(b.mag - Math.log10(a.mag)) > MAX_SIGNIFICANT_DIGITS) {
+        return a;
+      } else {
+        const magdiff = Math.pow(10, Math.log10(a.mag) - b.mag);
+        const mantissa = b.sign + a.sign * magdiff;
+        return FC(Math.sign(mantissa), 1, b.mag + Math.log10(Math.abs(mantissa)));
+      }
+    }
+
+    if (layera === 1 && layerb === 0) {
+      if (Math.abs(a.mag - Math.log10(b.mag)) > MAX_SIGNIFICANT_DIGITS) {
+        return a;
+      } else {
+        const magdiff = Math.pow(10, a.mag - Math.log10(b.mag));
+        const mantissa = b.sign + a.sign * magdiff;
+        return FC(Math.sign(mantissa), 1, Math.log10(b.mag) + Math.log10(Math.abs(mantissa)));
+      }
+    }
+
+    if (Math.abs(a.mag - b.mag) > MAX_SIGNIFICANT_DIGITS) {
+      return a;
+    } else {
+      const magdiff = Math.pow(10, a.mag - b.mag);
+      const mantissa = b.sign + a.sign * magdiff;
+      return FC(Math.sign(mantissa), 1, b.mag + Math.log10(Math.abs(mantissa)));
+    }
+
+    throw Error("Bad arguments to add: " + this + ", " + value);
+  }
+
+  public plus(value: DecimalSource): Decimal {
+    return this.add(value);
+  }
+
+  public sub(value: DecimalSource): Decimal {
+    return this.add(D(value).neg());
+  }
+
+  public subtract(value: DecimalSource): Decimal {
+    return this.sub(value);
+  }
+
+  public minus(value: DecimalSource): Decimal {
+    return this.sub(value);
+  }
+
+  public mul(value: DecimalSource): Decimal {
+    const decimal = D(value);
+
+    //inf/nan check
+    if (!Number.isFinite(this.layer)) {
+      return this;
+    }
+    if (!Number.isFinite(decimal.layer)) {
+      return decimal;
+    }
+
+    //Special case - if one of the numbers is 0, return 0.
+    if (this.sign === 0 || decimal.sign === 0) {
+      return FC_NN(0, 0, 0);
+    }
+
+    //Special case - Multiplying a number by its own reciprocal yields +/- 1, no matter how large.
+    if (this.layer === decimal.layer && this.mag === -decimal.mag) {
+      return FC_NN(this.sign * decimal.sign, 0, 1);
+    }
+
+    let a;
+    let b;
+
+    //Which number is bigger in terms of its multiplicative distance from 1?
+    if (
+      this.layer > decimal.layer ||
+      (this.layer == decimal.layer && Math.abs(this.mag) > Math.abs(decimal.mag))
+    ) {
+      a = this;
+      b = decimal;
+    } else {
+      a = decimal;
+      b = this;
+    }
+
+    if (a.layer === 0 && b.layer === 0) {
+      return D(a.sign * b.sign * a.mag * b.mag);
+    }
+
+    //Special case: If one of the numbers is layer 3 or higher or one of the numbers is 2+ layers bigger than the other, just take the bigger number.
+    if (a.layer >= 3 || a.layer - b.layer >= 2) {
+      return FC(a.sign * b.sign, a.layer, a.mag);
+    }
+
+    if (a.layer === 1 && b.layer === 0) {
+      return FC(a.sign * b.sign, 1, a.mag + Math.log10(b.mag));
+    }
+
+    if (a.layer === 1 && b.layer === 1) {
+      return FC(a.sign * b.sign, 1, a.mag + b.mag);
+    }
+
+    if (a.layer === 2 && b.layer === 1) {
+      const newmag = FC(Math.sign(a.mag), a.layer - 1, Math.abs(a.mag)).add(
+        FC(Math.sign(b.mag), b.layer - 1, Math.abs(b.mag))
+      );
+      return FC(a.sign * b.sign, newmag.layer + 1, newmag.sign * newmag.mag);
+    }
+
+    if (a.layer === 2 && b.layer === 2) {
+      const newmag = FC(Math.sign(a.mag), a.layer - 1, Math.abs(a.mag)).add(
+        FC(Math.sign(b.mag), b.layer - 1, Math.abs(b.mag))
+      );
+      return FC(a.sign * b.sign, newmag.layer + 1, newmag.sign * newmag.mag);
+    }
+
+    throw Error("Bad arguments to mul: " + this + ", " + value);
+  }
+
+  public multiply(value: DecimalSource): Decimal {
+    return this.mul(value);
+  }
+
+  public times(value: DecimalSource): Decimal {
+    return this.mul(value);
+  }
+
+  public div(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.mul(decimal.recip());
+  }
+
+  public divide(value: DecimalSource): Decimal {
+    return this.div(value);
+  }
+
+  public divideBy(value: DecimalSource): Decimal {
+    return this.div(value);
+  }
+
+  public dividedBy(value: DecimalSource): Decimal {
+    return this.div(value);
+  }
+
+  public recip(): Decimal {
+    if (this.mag === 0) {
+      return Decimal.dNaN;
+    } else if (this.layer === 0) {
+      return FC(this.sign, 0, 1 / this.mag);
+    } else {
+      return FC(this.sign, this.layer, -this.mag);
+    }
+  }
+
+  public reciprocal(): Decimal {
+    return this.recip();
+  }
+
+  public reciprocate(): Decimal {
+    return this.recip();
+  }
+
+  /**
+   * -1 for less than value, 0 for equals value, 1 for greater than value
+   */
+  public cmp(value: DecimalSource): CompareResult {
+    const decimal = D(value);
+    if (this.sign > decimal.sign) {
+      return 1;
+    }
+    if (this.sign < decimal.sign) {
+      return -1;
+    }
+    return (this.sign * this.cmpabs(value)) as CompareResult;
+  }
+
+  public cmpabs(value: DecimalSource): CompareResult {
+    const decimal = D(value);
+    const layera = this.mag > 0 ? this.layer : -this.layer;
+    const layerb = decimal.mag > 0 ? decimal.layer : -decimal.layer;
+    if (layera > layerb) {
+      return 1;
+    }
+    if (layera < layerb) {
+      return -1;
+    }
+    if (this.mag > decimal.mag) {
+      return 1;
+    }
+    if (this.mag < decimal.mag) {
+      return -1;
+    }
+    return 0;
+  }
+
+  public compare(value: DecimalSource): CompareResult {
+    return this.cmp(value);
+  }
+
+  public eq(value: DecimalSource): boolean {
+    const decimal = D(value);
+    return this.sign === decimal.sign && this.layer === decimal.layer && this.mag === decimal.mag;
+  }
+
+  public equals(value: DecimalSource): boolean {
+    return this.eq(value);
+  }
+
+  public neq(value: DecimalSource): boolean {
+    return !this.eq(value);
+  }
+
+  public notEquals(value: DecimalSource): boolean {
+    return this.neq(value);
+  }
+
+  public lt(value: DecimalSource): boolean {
+    const decimal = D(value); // FIXME: Remove?
+    return this.cmp(value) === -1;
+  }
+
+  public lte(value: DecimalSource): boolean {
+    return !this.gt(value);
+  }
+
+  public gt(value: DecimalSource): boolean {
+    const decimal = D(value); // FIXME: Remove?
+    return this.cmp(value) === 1;
+  }
+
+  public gte(value: DecimalSource): boolean {
+    return !this.lt(value);
+  }
+
+  public max(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.lt(decimal) ? decimal : this;
+  }
+
+  public min(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.gt(decimal) ? decimal : this;
+  }
+
+  public maxabs(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.cmpabs(decimal) < 0 ? decimal : this;
+  }
+
+  public minabs(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.cmpabs(decimal) > 0 ? decimal : this;
+  }
+
+  public clamp(min: DecimalSource, max: DecimalSource): Decimal {
+    return this.max(min).min(max);
+  }
+
+  public clampMin(min: DecimalSource): Decimal {
+    return this.max(min);
+  }
+
+  public clampMax(max: DecimalSource): Decimal {
+    return this.min(max);
+  }
+
+  public cmp_tolerance(value: DecimalSource, tolerance: number): CompareResult {
+    const decimal = D(value);
+    return this.eq_tolerance(decimal, tolerance) ? 0 : this.cmp(decimal);
+  }
+
+  public compare_tolerance(value: DecimalSource, tolerance: number): CompareResult {
+    return this.cmp_tolerance(value, tolerance);
+  }
+
+  /**
+   * Tolerance is a relative tolerance, multiplied by the greater of the magnitudes of the two arguments.
+   * For example, if you put in 1e-9, then any number closer to the
+   * larger number than (larger number)*1e-9 will be considered equal.
+   */
+  public eq_tolerance(value: DecimalSource, tolerance: number): boolean {
+    const decimal = D(value); // https://stackoverflow.com/a/33024979
+    if (tolerance == null) {
+      tolerance = 1e-7;
+    }
+    //Numbers that are too far away are never close.
+    if (this.sign !== decimal.sign) {
+      return false;
+    }
+    if (Math.abs(this.layer - decimal.layer) > 1) {
+      return false;
+    }
+    // return abs(a-b) <= tolerance * max(abs(a), abs(b))
+    let magA = this.mag;
+    let magB = decimal.mag;
+    if (this.layer > decimal.layer) {
+      magB = f_maglog10(magB);
+    }
+    if (this.layer < decimal.layer) {
+      magA = f_maglog10(magA);
+    }
+    return Math.abs(magA - magB) <= tolerance * Math.max(Math.abs(magA), Math.abs(magB));
+  }
+
+  public equals_tolerance(value: DecimalSource, tolerance: number): boolean {
+    return this.eq_tolerance(value, tolerance);
+  }
+
+  public neq_tolerance(value: DecimalSource, tolerance: number): boolean {
+    return !this.eq_tolerance(value, tolerance);
+  }
+
+  public notEquals_tolerance(value: DecimalSource, tolerance: number): boolean {
+    return this.neq_tolerance(value, tolerance);
+  }
+
+  public lt_tolerance(value: DecimalSource, tolerance: number): boolean {
+    const decimal = D(value);
+    return !this.eq_tolerance(decimal, tolerance) && this.lt(decimal);
+  }
+
+  public lte_tolerance(value: DecimalSource, tolerance: number): boolean {
+    const decimal = D(value);
+    return this.eq_tolerance(decimal, tolerance) || this.lt(decimal);
+  }
+
+  public gt_tolerance(value: DecimalSource, tolerance: number): boolean {
+    const decimal = D(value);
+    return !this.eq_tolerance(decimal, tolerance) && this.gt(decimal);
+  }
+
+  public gte_tolerance(value: DecimalSource, tolerance: number): boolean {
+    const decimal = D(value);
+    return this.eq_tolerance(decimal, tolerance) || this.gt(decimal);
+  }
+
+  public pLog10(): Decimal {
+    if (this.lt(Decimal.dZero)) {
+      return Decimal.dZero;
+    }
+    return this.log10();
+  }
+
+  public absLog10(): Decimal {
+    if (this.sign === 0) {
+      return Decimal.dNaN;
+    } else if (this.layer > 0) {
+      return FC(Math.sign(this.mag), this.layer - 1, Math.abs(this.mag));
+    } else {
+      return FC(1, 0, Math.log10(this.mag));
+    }
+  }
+
+  public log10(): Decimal {
+    if (this.sign <= 0) {
+      return Decimal.dNaN;
+    } else if (this.layer > 0) {
+      return FC(Math.sign(this.mag), this.layer - 1, Math.abs(this.mag));
+    } else {
+      return FC(this.sign, 0, Math.log10(this.mag));
+    }
+  }
+
+  public log(base: DecimalSource): Decimal {
+    base = D(base);
+    if (this.sign <= 0) {
+      return Decimal.dNaN;
+    }
+    if (base.sign <= 0) {
+      return Decimal.dNaN;
+    }
+    if (base.sign === 1 && base.layer === 0 && base.mag === 1) {
+      return Decimal.dNaN;
+    } else if (this.layer === 0 && base.layer === 0) {
+      return FC(this.sign, 0, Math.log(this.mag) / Math.log(base.mag));
+    }
+
+    return Decimal.div(this.log10(), base.log10());
+  }
+
+  public log2(): Decimal {
+    if (this.sign <= 0) {
+      return Decimal.dNaN;
+    } else if (this.layer === 0) {
+      return FC(this.sign, 0, Math.log2(this.mag));
+    } else if (this.layer === 1) {
+      return FC(Math.sign(this.mag), 0, Math.abs(this.mag) * 3.321928094887362); //log2(10)
+    } else if (this.layer === 2) {
+      return FC(Math.sign(this.mag), 1, Math.abs(this.mag) + 0.5213902276543247); //-log10(log10(2))
+    } else {
+      return FC(Math.sign(this.mag), this.layer - 1, Math.abs(this.mag));
+    }
+  }
+
+  public ln(): Decimal {
+    if (this.sign <= 0) {
+      return Decimal.dNaN;
+    } else if (this.layer === 0) {
+      return FC(this.sign, 0, Math.log(this.mag));
+    } else if (this.layer === 1) {
+      return FC(Math.sign(this.mag), 0, Math.abs(this.mag) * 2.302585092994046); //ln(10)
+    } else if (this.layer === 2) {
+      return FC(Math.sign(this.mag), 1, Math.abs(this.mag) + 0.36221568869946325); //log10(log10(e))
+    } else {
+      return FC(Math.sign(this.mag), this.layer - 1, Math.abs(this.mag));
+    }
+  }
+
+  public logarithm(base: DecimalSource): Decimal {
+    return this.log(base);
+  }
+
+  public pow(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    const a = this;
+    const b = decimal;
+
+    //special case: if a is 0, then return 0
+    if (a.sign === 0) {
+      return a;
+    }
+    //special case: if a is 1, then return 1
+    if (a.sign === 1 && a.layer === 0 && a.mag === 1) {
+      return a;
+    }
+    //special case: if b is 0, then return 1
+    if (b.sign === 0) {
+      return FC_NN(1, 0, 1);
+    }
+    //special case: if b is 1, then return a
+    if (b.sign === 1 && b.layer === 0 && b.mag === 1) {
+      return a;
+    }
+
+    const result = a.absLog10().mul(b).pow10();
+
+    if (this.sign === -1 && b.toNumber() % 2 === 1) {
+      return result.neg();
+    }
+
+    return result;
+  }
+
+  public pow10(): Decimal {
+    /*
+    There are four cases we need to consider:
+    1) positive sign, positive mag (e15, ee15): +1 layer (e.g. 10^15 becomes e15, 10^e15 becomes ee15)
+    2) negative sign, positive mag (-e15, -ee15): +1 layer but sign and mag sign are flipped (e.g. 10^-15 becomes e-15, 10^-e15 becomes ee-15)
+    3) positive sign, negative mag (e-15, ee-15): layer 0 case would have been handled in the Math.pow check, so just return 1
+    4) negative sign, negative mag (-e-15, -ee-15): layer 0 case would have been handled in the Math.pow check, so just return 1
+    */
+
+    if (!Number.isFinite(this.layer) || !Number.isFinite(this.mag)) {
+      return Decimal.dNaN;
+    }
+
+    let a = this;
+
+    //handle layer 0 case - if no precision is lost just use Math.pow, else promote one layer
+    if (a.layer === 0) {
+      const newmag = Math.pow(10, a.sign * a.mag);
+      if (Number.isFinite(newmag) && Math.abs(newmag) > 0.1) {
+        return FC(1, 0, newmag);
+      } else {
+        if (a.sign === 0) {
+          return Decimal.dOne;
+        } else {
+          a = FC_NN(a.sign, a.layer + 1, Math.log10(a.mag)) as this;
+        }
+      }
+    }
+
+    //handle all 4 layer 1+ cases individually
+    if (a.sign > 0 && a.mag > 0) {
+      return FC(a.sign, a.layer + 1, a.mag);
+    }
+    if (a.sign < 0 && a.mag > 0) {
+      return FC(-a.sign, a.layer + 1, -a.mag);
+    }
+    //both the negative mag cases are identical: one +/- rounding error
+    return Decimal.dOne;
+  }
+
+  public pow_base(value: DecimalSource): Decimal {
+    return D(value).pow(this);
+  }
+
+  public root(value: DecimalSource): Decimal {
+    const decimal = D(value);
+    return this.pow(decimal.recip());
+  }
+
+  public factorial(): Decimal {
+    if (this.mag < 0) {
+      return this.add(1).gamma();
+    } else if (this.layer === 0) {
+      return this.add(1).gamma();
+    } else if (this.layer === 1) {
+      return Decimal.exp(Decimal.mul(this, Decimal.ln(this).sub(1)));
+    } else {
+      return Decimal.exp(this);
+    }
+  }
+
+  //from HyperCalc source code
+  public gamma(): Decimal {
+    if (this.mag < 0) {
+      return this.recip();
+    } else if (this.layer === 0) {
+      if (this.lt(FC_NN(1, 0, 24))) {
+        return D(f_gamma(this.sign * this.mag));
+      }
+
+      const t = this.mag - 1;
+      let l = 0.9189385332046727; //0.5*Math.log(2*Math.PI)
+      l = l + (t + 0.5) * Math.log(t);
+      l = l - t;
+      const n2 = t * t;
+      let np = t;
+      let lm = 12 * np;
+      let adj = 1 / lm;
+      let l2 = l + adj;
+      if (l2 === l) {
+        return Decimal.exp(l);
+      }
+
+      l = l2;
+      np = np * n2;
+      lm = 360 * np;
+      adj = 1 / lm;
+      l2 = l - adj;
+      if (l2 === l) {
+        return Decimal.exp(l);
+      }
+
+      l = l2;
+      np = np * n2;
+      lm = 1260 * np;
+      let lt = 1 / lm;
+      l = l + lt;
+      np = np * n2;
+      lm = 1680 * np;
+      lt = 1 / lm;
+      l = l - lt;
+      return Decimal.exp(l);
+    } else if (this.layer === 1) {
+      return Decimal.exp(Decimal.mul(this, Decimal.ln(this).sub(1)));
+    } else {
+      return Decimal.exp(this);
+    }
+  }
+
+  public lngamma(): Decimal {
+    return this.gamma().ln();
+  }
+
+  public exp(): Decimal {
+    if (this.mag < 0) {
+      return Decimal.dOne;
+    }
+    if (this.layer === 0 && this.mag <= 709.7) {
+      return D(Math.exp(this.sign * this.mag));
+    } else if (this.layer === 0) {
+      return FC(1, 1, this.sign * Math.log10(Math.E) * this.mag);
+    } else if (this.layer === 1) {
+      return FC(1, 2, this.sign * (Math.log10(0.4342944819032518) + this.mag));
+    } else {
+      return FC(1, this.layer + 1, this.sign * this.mag);
+    }
+  }
+
+  public sqr(): Decimal {
+    return this.pow(2);
+  }
+
+  public sqrt(): Decimal {
+    if (this.layer === 0) {
+      return D(Math.sqrt(this.sign * this.mag));
+    } else if (this.layer === 1) {
+      return FC(1, 2, Math.log10(this.mag) - 0.3010299956639812);
+    } else {
+      const result = Decimal.div(FC_NN(this.sign, this.layer - 1, this.mag), FC_NN(1, 0, 2));
+      result.layer += 1;
+      result.normalize();
+      return result;
+    }
+  }
+
+  public cube(): Decimal {
+    return this.pow(3);
+  }
+
+  public cbrt(): Decimal {
+    return this.pow(1 / 3);
+  }
+
+  //Tetration/tetrate: The result of exponentiating 'this' to 'this' 'height' times in a row.  https://en.wikipedia.org/wiki/Tetration
+  //If payload != 1, then this is 'iterated exponentiation', the result of exping (payload) to base (this) (height) times. https://andydude.github.io/tetration/archives/tetration2/ident.html
+  //Works with negative and positive real heights.
+  public tetrate(height = 2, payload: DecimalSource = FC_NN(1, 0, 1)): Decimal {
+    if (height === Number.POSITIVE_INFINITY) {
+      //Formula for infinite height power tower.
+      const negln = Decimal.ln(this).neg();
+      return negln.lambertw().div(negln);
+    }
+
+    if (height < 0) {
+      return Decimal.iteratedlog(payload, this, -height);
+    }
+
+    payload = D(payload);
+    const oldheight = height;
+    height = Math.trunc(height);
+    const fracheight = oldheight - height;
+
+    if (fracheight !== 0) {
+      if (payload.eq(Decimal.dOne)) {
+        ++height;
+        payload = new Decimal(fracheight);
+      } else {
+        if (this.eq(10)) {
+          payload = payload.layeradd10(fracheight);
+        } else {
+          payload = payload.layeradd(fracheight, this);
+        }
+      }
+    }
+
+    for (let i = 0; i < height; ++i) {
+      payload = this.pow(payload);
+      //bail if we're NaN
+      if (!isFinite(payload.layer) || !isFinite(payload.mag)) {
+        return payload;
+      }
+      //shortcut
+      if (payload.layer - this.layer > 3) {
+        return FC_NN(payload.sign, payload.layer + (height - i - 1), payload.mag);
+      }
+      //give up after 100 iterations if nothing is happening
+      if (i > 100) {
+        return payload;
+      }
+    }
+    return payload;
+  }
+
+  //iteratedexp/iterated exponentiation: - all cases handled in tetrate, so just call it
+  public iteratedexp(height = 2, payload = FC_NN(1, 0, 1)): Decimal {
+    return this.tetrate(height, payload);
+  }
+
+  //iterated log/repeated log: The result of applying log(base) 'times' times in a row. Approximately equal to subtracting (times) from the number's slog representation. Equivalent to tetrating to a negative height.
+  //Works with negative and positive real heights.
+  public iteratedlog(base: DecimalSource = 10, times = 1): Decimal {
+    if (times < 0) {
+      return Decimal.tetrate(base, -times, this);
+    }
+
+    base = D(base);
+    let result = D(this);
+    const fulltimes = times;
+    times = Math.trunc(times);
+    const fraction = fulltimes - times;
+    if (result.layer - base.layer > 3) {
+      const layerloss = Math.min(times, result.layer - base.layer - 3);
+      times -= layerloss;
+      result.layer -= layerloss;
+    }
+
+    for (let i = 0; i < times; ++i) {
+      result = result.log(base);
+      //bail if we're NaN
+      if (!isFinite(result.layer) || !isFinite(result.mag)) {
+        return result;
+      }
+      //give up after 100 iterations if nothing is happening
+      if (i > 100) {
+        return result;
+      }
+    }
+
+    //handle fractional part
+    if (fraction > 0 && fraction < 1) {
+      if (base.eq(10)) {
+        result = result.layeradd10(-fraction);
+      } else {
+        result = result.layeradd(-fraction, base);
+      }
+    }
+
+    return result;
+  }
+
+  //Super-logarithm, one of tetration's inverses, tells you what size power tower you'd have to tetrate base to to get number. By definition, will never be higher than 1.8e308 in break_eternity.js, since a power tower 1.8e308 numbers tall is the largest representable number.
+  // https://en.wikipedia.org/wiki/Super-logarithm
+  public slog(base: DecimalSource = 10): Decimal {
+    if (this.mag < 0) {
+      return Decimal.dNegOne;
+    }
+
+    base = D(base);
+
+    let result = 0;
+    let copy = D(this);
+    if (copy.layer - base.layer > 3) {
+      const layerloss = copy.layer - base.layer - 3;
+      result += layerloss;
+      copy.layer -= layerloss;
+    }
+
+    for (let i = 0; i < 100; ++i) {
+      if (copy.lt(Decimal.dZero)) {
+        copy = Decimal.pow(base, copy);
+        result -= 1;
+      } else if (copy.lte(Decimal.dOne)) {
+        return D(result + copy.toNumber() - 1); //<-- THIS IS THE CRITICAL FUNCTION
+        //^ Also have to change tetrate payload handling and layeradd10 if this is changed!
+      } else {
+        result += 1;
+        copy = Decimal.log(copy, base);
+      }
+    }
+    return D(result);
+  }
+
+  //Approximations taken from the excellent paper https://web.archive.org/web/20090201164836/http://tetration.itgo.com/paper.html !
+  //Not using for now unless I can figure out how to use it in all the related functions.
+  /*var slog_criticalfunction_1 = function(x, z) {
+    z = z.toNumber();
+    return -1 + z;
+  }
+
+  var slog_criticalfunction_2 = function(x, z) {
+    z = z.toNumber();
+    var lnx = x.ln();
+    if (lnx.layer === 0)
+    {
+      lnx = lnx.toNumber();
+      return -1 + z*2*lnx/(1+lnx) - z*z*(1-lnx)/(1+lnx);
+    }
+    else
+    {
+      var term1 = lnx.mul(z*2).div(lnx.add(1));
+      var term2 = Decimal.sub(1, lnx).mul(z*z).div(lnx.add(1));
+      Decimal.dNegOne.add(Decimal.sub(term1, term2));
+    }
+  }
+
+  var slog_criticalfunction_3 = function(x, z) {
+    z = z.toNumber();
+    var lnx = x.ln();
+    var lnx2 = lnx.sqr();
+    var lnx3 = lnx.cube();
+    if (lnx.layer === 0 && lnx2.layer === 0 && lnx3.layer === 0)
+    {
+      lnx = lnx.toNumber();
+      lnx2 = lnx2.toNumber();
+      lnx3 = lnx3.toNumber();
+
+      var term1 = 6*z*(lnx+lnx3);
+      var term2 = 3*z*z*(3*lnx2-2*lnx3);
+      var term3 = 2*z*z*z*(1-lnx-2*lnx2+lnx3);
+      var top = term1+term2+term3;
+      var bottom = 2+4*lnx+5*lnx2+2*lnx3;
+
+      return -1 + top/bottom;
+    }
+    else
+    {
+      var term1 = (lnx.add(lnx3)).mul(6*z);
+      var term2 = (lnx2.mul(3).sub(lnx3.mul(2))).mul(3*z*z);
+      var term3 = (Decimal.dOne.sub(lnx).sub(lnx2.mul(2)).add(lnx3)).mul(2*z*z*z);
+      var top = term1.add(term2).add(term3);
+      var bottom = new Decimal(2).add(lnx.mul(4)).add(lnx2.mul(5)).add(lnx3.mul(2));
+
+      return Decimal.dNegOne.add(top.div(bottom));
+    }
+  }*/
+
+  //Function for adding/removing layers from a Decimal, even fractional layers (e.g. its slog10 representation).
+  //Everything continues to use the linear approximation ATM.
+  public layeradd10(diff: DecimalSource): Decimal {
+    diff = Decimal.fromValue_noAlloc(diff).toNumber();
+    const result = D(this);
+    if (diff >= 1) {
+      const layeradd = Math.trunc(diff);
+      diff -= layeradd;
+      result.layer += layeradd;
+    }
+    if (diff <= -1) {
+      const layeradd = Math.trunc(diff);
+      diff -= layeradd;
+      result.layer += layeradd;
+      if (result.layer < 0) {
+        for (let i = 0; i < 100; ++i) {
+          result.layer++;
+          result.mag = Math.log10(result.mag);
+          if (!isFinite(result.mag)) {
+            return result;
+          }
+          if (result.layer >= 0) {
+            break;
+          }
+        }
+      }
+    }
+
+    //layeradd10: like adding 'diff' to the number's slog(base) representation. Very similar to tetrate base 10 and iterated log base 10. Also equivalent to adding a fractional amount to the number's layer in its break_eternity.js representation.
+    if (diff > 0) {
+      let subtractlayerslater = 0;
+      //Ironically, this edge case would be unnecessary if we had 'negative layers'.
+      while (Number.isFinite(result.mag) && result.mag < 10) {
+        result.mag = Math.pow(10, result.mag);
+        ++subtractlayerslater;
+      }
+
+      //A^(10^B) === C, solve for B
+      //B === log10(logA(C))
+
+      if (result.mag > 1e10) {
+        result.mag = Math.log10(result.mag);
+        result.layer++;
+      }
+
+      //Note that every integer slog10 value, the formula changes, so if we're near such a number, we have to spend exactly enough layerdiff to hit it, and then use the new formula.
+      const diffToNextSlog = Math.log10(Math.log(1e10) / Math.log(result.mag));
+      if (diffToNextSlog < diff) {
+        result.mag = Math.log10(1e10);
+        result.layer++;
+        diff -= diffToNextSlog;
+      }
+
+      result.mag = Math.pow(result.mag, Math.pow(10, diff));
+
+      while (subtractlayerslater > 0) {
+        result.mag = Math.log10(result.mag);
+        --subtractlayerslater;
+      }
+    } else if (diff < 0) {
+      let subtractlayerslater = 0;
+
+      while (Number.isFinite(result.mag) && result.mag < 10) {
+        result.mag = Math.pow(10, result.mag);
+        ++subtractlayerslater;
+      }
+
+      if (result.mag > 1e10) {
+        result.mag = Math.log10(result.mag);
+        result.layer++;
+      }
+
+      const diffToNextSlog = Math.log10(1 / Math.log10(result.mag));
+      if (diffToNextSlog > diff) {
+        result.mag = 1e10;
+        result.layer--;
+        diff -= diffToNextSlog;
+      }
+
+      result.mag = Math.pow(result.mag, Math.pow(10, diff));
+
+      while (subtractlayerslater > 0) {
+        result.mag = Math.log10(result.mag);
+        --subtractlayerslater;
+      }
+    }
+
+    while (result.layer < 0) {
+      result.layer++;
+      result.mag = Math.log10(result.mag);
+    }
+    result.normalize();
+    return result;
+  }
+
+  //layeradd: like adding 'diff' to the number's slog(base) representation. Very similar to tetrate base 'base' and iterated log base 'base'.
+  public layeradd(diff: number, base: DecimalSource): Decimal {
+    const slogthis = this.slog(base).toNumber();
+    const slogdest = slogthis + diff;
+    if (slogdest >= 0) {
+      return Decimal.tetrate(base, slogdest);
+    } else if (!Number.isFinite(slogdest)) {
+      return Decimal.dNaN;
+    } else if (slogdest >= -1) {
+      return Decimal.log(Decimal.tetrate(base, slogdest + 1), base);
+    } else {
+      return Decimal.log(Decimal.log(Decimal.tetrate(base, slogdest + 2), base), base);
+    }
+  }
+
+  //The Lambert W function, also called the omega function or product logarithm, is the solution W(x) === x*e^x.
+  // https://en.wikipedia.org/wiki/Lambert_W_function
+  //Some special values, for testing: https://en.wikipedia.org/wiki/Lambert_W_function#Special_values
+  public lambertw(): Decimal {
+    if (this.lt(-0.3678794411710499)) {
+      throw Error("lambertw is unimplemented for results less than -1, sorry!");
+    } else if (this.mag < 0) {
+      return D(f_lambertw(this.toNumber()));
+    } else if (this.layer === 0) {
+      return D(f_lambertw(this.sign * this.mag));
+    } else if (this.layer === 1) {
+      return d_lambertw(this);
+    } else if (this.layer === 2) {
+      return d_lambertw(this);
+    }
+    if (this.layer >= 3) {
+      return FC_NN(this.sign, this.layer - 1, this.mag);
+    }
+
+    throw "Unhandled behavior in lambertw()";
+  }
+
+  //The super square-root function - what number, tetrated to height 2, equals this?
+  //Other sroots are possible to calculate probably through guess and check methods, this one is easy though.
+  // https://en.wikipedia.org/wiki/Tetration#Super-root
+  public ssqrt(): Decimal {
+    if (this.sign == 1 && this.layer >= 3) {
+      return FC_NN(this.sign, this.layer - 1, this.mag);
+    }
+    const lnx = this.ln();
+    return lnx.div(lnx.lambertw());
+  }
+  /*
+
+Unit tests for tetrate/iteratedexp/iteratedlog/layeradd10/layeradd/slog:
+
+for (var i = 0; i < 1000; ++i)
+{
+  var first = Math.random()*100;
+  var both = Math.random()*100;
+  var expected = first+both+1;
+  var result = new Decimal(10).layeradd10(first).layeradd10(both).slog();
+  if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result))
+  {
+      console.log(first + ", " + both);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var first = Math.random()*100;
+  var both = Math.random()*100;
+  first += both;
+  var expected = first-both+1;
+  var result = new Decimal(10).layeradd10(first).layeradd10(-both).slog();
+  if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result))
+  {
+      console.log(first + ", " + both);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var first = Math.random()*100;
+  var both = Math.random()*100;
+  var base = Math.random()*8+2;
+  var expected = first+both+1;
+  var result = new Decimal(base).layeradd(first, base).layeradd(both, base).slog(base);
+  if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result))
+  {
+      console.log(first + ", " + both);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var first = Math.random()*100;
+  var both = Math.random()*100;
+  var base = Math.random()*8+2;
+  first += both;
+  var expected = first-both+1;
+  var result = new Decimal(base).layeradd(first, base).layeradd(-both, base).slog(base);
+  if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result))
+  {
+      console.log(first + ", " + both);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+var first = Math.round((Math.random()*30))/10;
+var both = Math.round((Math.random()*30))/10;
+var tetrateonly = Decimal.tetrate(10, first);
+var tetrateandlog = Decimal.tetrate(10, first+both).iteratedlog(10, both);
+if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog))
+{
+  console.log(first + ", " + both);
+}
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+var first = Math.round((Math.random()*30))/10;
+var both = Math.round((Math.random()*30))/10;
+var base = Math.random()*8+2;
+var tetrateonly = Decimal.tetrate(base, first);
+var tetrateandlog = Decimal.tetrate(base, first+both).iteratedlog(base, both);
+if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog))
+{
+  console.log(first + ", " + both);
+}
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+var first = Math.round((Math.random()*30))/10;
+var both = Math.round((Math.random()*30))/10;
+var base = Math.random()*8+2;
+var tetrateonly = Decimal.tetrate(base, first, base);
+var tetrateandlog = Decimal.tetrate(base, first+both, base).iteratedlog(base, both);
+if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog))
+{
+  console.log(first + ", " + both);
+}
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var xex = new Decimal(-0.3678794411710499+Math.random()*100);
+  var x = Decimal.lambertw(xex);
+  if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x))))
+  {
+      console.log(xex);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var xex = new Decimal(-0.3678794411710499+Math.exp(Math.random()*100));
+  var x = Decimal.lambertw(xex);
+  if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x))))
+  {
+      console.log(xex);
+  }
+}
+
+for (var i = 0; i < 1000; ++i)
+{
+  var a = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1);
+  var b = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1);
+  if (Math.random() > 0.5) { a = a.recip(); }
+  if (Math.random() > 0.5) { b = b.recip(); }
+  var c = a.add(b).toNumber();
+  if (Number.isFinite(c) && !Decimal.eq_tolerance(c, a.toNumber()+b.toNumber()))
+  {
+      console.log(a + ", " + b);
+  }
+}
+
+for (var i = 0; i < 100; ++i)
+{
+  var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4));
+  var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4));
+  if (Math.random() > 0.5) { a = a.recip(); }
+  if (Math.random() > 0.5) { b = b.recip(); }
+  var c = a.mul(b).toNumber();
+  if (Number.isFinite(c) && Number.isFinite(a.toNumber()) && Number.isFinite(b.toNumber()) && a.toNumber() != 0 && b.toNumber() != 0 && c != 0 && !Decimal.eq_tolerance(c, a.toNumber()*b.toNumber()))
+  {
+      console.log("Test 1: " + a + ", " + b);
+  }
+  else if (!Decimal.mul(a.recip(), b.recip()).eq_tolerance(Decimal.mul(a, b).recip()))
+  {
+      console.log("Test 3: " + a + ", " + b);
+  }
+}
+
+for (var i = 0; i < 10; ++i)
+{
+  var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4));
+  var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4));
+  if (Math.random() > 0.5 && a.sign !== 0) { a = a.recip(); }
+  if (Math.random() > 0.5 && b.sign !== 0) { b = b.recip(); }
+  var c = a.pow(b);
+  var d = a.root(b.recip());
+  var e = a.pow(b.recip());
+  var f = a.root(b);
+
+  if (!c.eq_tolerance(d) && a.sign !== 0 && b.sign !== 0)
+  {
+    console.log("Test 1: " + a + ", " + b);
+  }
+  if (!e.eq_tolerance(f) && a.sign !== 0 && b.sign !== 0)
+  {
+    console.log("Test 2: " + a + ", " + b);
+  }
+}
+
+for (var i = 0; i < 10; ++i)
+{
+  var a = Math.round(Math.random()*18-9);
+  var b = Math.round(Math.random()*100-50);
+  var c = Math.round(Math.random()*18-9);
+  var d = Math.round(Math.random()*100-50);
+  console.log("Decimal.pow(Decimal.fromMantissaExponent(" + a + ", " + b + "), Decimal.fromMantissaExponent(" + c + ", " + d + ")).toString()");
+}
+
+*/
+
+  //Pentation/pentate: The result of tetrating 'height' times in a row. An absurdly strong operator - Decimal.pentate(2, 4.28) and Decimal.pentate(10, 2.37) are already too huge for break_eternity.js!
+  // https://en.wikipedia.org/wiki/Pentation
+  public pentate(height = 2, payload: DecimalSource = FC_NN(1, 0, 1)): Decimal {
+    payload = D(payload);
+    const oldheight = height;
+    height = Math.trunc(height);
+    const fracheight = oldheight - height;
+
+    //I have no idea if this is a meaningful approximation for pentation to continuous heights, but it is monotonic and continuous.
+    if (fracheight !== 0) {
+      if (payload.eq(Decimal.dOne)) {
+        ++height;
+        payload = new Decimal(fracheight);
+      } else {
+        if (this.eq(10)) {
+          payload = payload.layeradd10(fracheight);
+        } else {
+          payload = payload.layeradd(fracheight, this);
+        }
+      }
+    }
+
+    for (let i = 0; i < height; ++i) {
+      payload = this.tetrate(payload.toNumber());
+      //bail if we're NaN
+      if (!isFinite(payload.layer) || !isFinite(payload.mag)) {
+        return payload;
+      }
+      //give up after 10 iterations if nothing is happening
+      if (i > 10) {
+        return payload;
+      }
+    }
+
+    return payload;
+  }
+
+  // trig functions!
+  public sin(): this | Decimal {
+    if (this.mag < 0) {
+      return this;
+    }
+    if (this.layer === 0) {
+      return D(Math.sin(this.sign * this.mag));
+    }
+    return FC_NN(0, 0, 0);
+  }
+
+  public cos(): Decimal {
+    if (this.mag < 0) {
+      return Decimal.dOne;
+    }
+    if (this.layer === 0) {
+      return D(Math.cos(this.sign * this.mag));
+    }
+    return FC_NN(0, 0, 0);
+  }
+
+  public tan(): this | Decimal {
+    if (this.mag < 0) {
+      return this;
+    }
+    if (this.layer === 0) {
+      return D(Math.tan(this.sign * this.mag));
+    }
+    return FC_NN(0, 0, 0);
+  }
+
+  public asin(): this | Decimal {
+    if (this.mag < 0) {
+      return this;
+    }
+    if (this.layer === 0) {
+      return D(Math.asin(this.sign * this.mag));
+    }
+    return FC_NN(Number.NaN, Number.NaN, Number.NaN);
+  }
+
+  public acos(): Decimal {
+    if (this.mag < 0) {
+      return D(Math.acos(this.toNumber()));
+    }
+    if (this.layer === 0) {
+      return D(Math.acos(this.sign * this.mag));
+    }
+    return FC_NN(Number.NaN, Number.NaN, Number.NaN);
+  }
+
+  public atan(): this | Decimal {
+    if (this.mag < 0) {
+      return this;
+    }
+    if (this.layer === 0) {
+      return D(Math.atan(this.sign * this.mag));
+    }
+    return D(Math.atan(this.sign * 1.8e308));
+  }
+
+  public sinh(): Decimal {
+    return this.exp().sub(this.negate().exp()).div(2);
+  }
+
+  public cosh(): Decimal {
+    return this.exp().add(this.negate().exp()).div(2);
+  }
+
+  public tanh(): Decimal {
+    return this.sinh().div(this.cosh());
+  }
+
+  public asinh(): Decimal {
+    return Decimal.ln(this.add(this.sqr().add(1).sqrt()));
+  }
+
+  public acosh(): Decimal {
+    return Decimal.ln(this.add(this.sqr().sub(1).sqrt()));
+  }
+
+  public atanh(): Decimal {
+    if (this.abs().gte(1)) {
+      return FC_NN(Number.NaN, Number.NaN, Number.NaN);
+    }
+
+    return Decimal.ln(this.add(1).div(D(1).sub(this))).div(2);
+  }
+
+  /**
+   * Joke function from Realm Grinder
+   */
+  public ascensionPenalty(ascensions: DecimalSource): Decimal {
+    if (ascensions === 0) {
+      return this;
+    }
+
+    return this.root(Decimal.pow(10, ascensions));
+  }
+
+  /**
+   * Joke function from Cookie Clicker. It's 'egg'
+   */
+  public egg(): Decimal {
+    return this.add(9);
+  }
+
+  public lessThanOrEqualTo(other: DecimalSource): boolean {
+    return this.cmp(other) < 1;
+  }
+
+  public lessThan(other: DecimalSource): boolean {
+    return this.cmp(other) < 0;
+  }
+
+  public greaterThanOrEqualTo(other: DecimalSource): boolean {
+    return this.cmp(other) > -1;
+  }
+
+  public greaterThan(other: DecimalSource): boolean {
+    return this.cmp(other) > 0;
+  }
+
+  // return Decimal;
+}
+
+// return Decimal;
diff --git a/src/main.js b/src/main.js
deleted file mode 100644
index 4b86f58..0000000
--- a/src/main.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { createApp } from 'vue';
-import App from './App';
-import { load } from './util/save';
-import { setVue } from './util/vue';
-import gameLoop from './game/gameLoop';
-import { registerComponents } from './components/index';
-import modInfo from './data/modInfo.json';
-
-requestAnimationFrame(async () => {
-	await load();
-
-	// Create Vue
-	const vue = window.vue = createApp({
-		...App
-	});
-	setVue(vue);
-	registerComponents(vue);
-	vue.mount('#app');
-	document.title = modInfo.title;
-
-	gameLoop();
-});
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..4f0f07d
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,22 @@
+import { createApp } from "vue";
+import App from "./App.vue";
+import { load } from "./util/save";
+import { setVue } from "./util/vue";
+import gameLoop from "./game/gameLoop";
+import { registerComponents } from "./components/index";
+import modInfo from "./data/modInfo.json";
+
+requestAnimationFrame(async () => {
+    await load();
+
+    // Create Vue
+    const vue = (window.vue = createApp({
+        ...App
+    }));
+    setVue(vue);
+    registerComponents(vue);
+    vue.mount("#app");
+    document.title = modInfo.title;
+
+    gameLoop();
+});
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
new file mode 100644
index 0000000..dbd3fb9
--- /dev/null
+++ b/src/shims-vue.d.ts
@@ -0,0 +1,6 @@
+/* eslint-disable */
+declare module '*.vue' {
+  import type { defineComponent } from 'vue';
+  const component: ReturnType<typeof defineComponent>;
+  export default component;
+}
diff --git a/src/typings/branches.d.ts b/src/typings/branches.d.ts
new file mode 100644
index 0000000..93b263b
--- /dev/null
+++ b/src/typings/branches.d.ts
@@ -0,0 +1,28 @@
+import { ComponentPublicInstance } from "vue";
+
+export interface BranchLink {
+    start: string;
+    end: string;
+    options: string | BranchOptions;
+}
+
+export interface BranchNode {
+    x?: number;
+    y?: number;
+    component: ComponentPublicInstance;
+    element: HTMLElement;
+}
+
+export interface BranchOptions {
+    target?: string;
+    featureType?: string;
+    stroke?: string;
+    "stroke-width"?: string;
+    startOffset?: Position;
+    endOffset?: Position;
+}
+
+export interface Position {
+    x: number;
+    y: number;
+}
diff --git a/src/typings/cacheableFunction.d.ts b/src/typings/cacheableFunction.d.ts
new file mode 100644
index 0000000..2e7efdd
--- /dev/null
+++ b/src/typings/cacheableFunction.d.ts
@@ -0,0 +1,3 @@
+export interface CacheableFunction extends Function {
+    forceCached?: boolean;
+}
diff --git a/src/typings/component.d.ts b/src/typings/component.d.ts
new file mode 100644
index 0000000..5b3c011
--- /dev/null
+++ b/src/typings/component.d.ts
@@ -0,0 +1,3 @@
+import { ComponentOptions } from "vue";
+
+export type CoercableComponent = string | ComponentOptions;
diff --git a/src/typings/computable.d.ts b/src/typings/computable.d.ts
new file mode 100644
index 0000000..a622ee9
--- /dev/null
+++ b/src/typings/computable.d.ts
@@ -0,0 +1,5 @@
+export type Computable<T> = {
+    [K in keyof T]:
+        | ((this: T) => T[K])
+        | (NonNullable<T[K]> extends (..._: infer A) => infer R ? (this: T, ..._: A) => R : T[K]);
+};
diff --git a/src/typings/features/achievement.d.ts b/src/typings/features/achievement.d.ts
new file mode 100644
index 0000000..7633bbb
--- /dev/null
+++ b/src/typings/features/achievement.d.ts
@@ -0,0 +1,16 @@
+import { CoercableComponent } from "@/component";
+import { State } from "../state";
+import { Feature } from "./feature";
+
+export interface Achievement extends Feature {
+    earned: boolean;
+    onComplete?: () => void;
+    effect?: State;
+    display?: CoercableComponent;
+    name?: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+    image?: string;
+    doneTooltip?: CoercableComponent;
+    goalTooltip?: CoercableComponent;
+    tooltip?: CoercableComponent;
+}
diff --git a/src/typings/features/bar.d.ts b/src/typings/features/bar.d.ts
new file mode 100644
index 0000000..b6b80ed
--- /dev/null
+++ b/src/typings/features/bar.d.ts
@@ -0,0 +1,15 @@
+import { CoercableComponent } from "@/component";
+import Decimal from "@/util/bignum";
+import { Feature } from "./feature";
+
+export interface Bar extends Feature {
+    width: number;
+    height: number;
+    style: Partial<CSSStyleDeclaration>;
+    borderStyle: Partial<CSSStyleDeclaration>;
+    baseStyle: Partial<CSSStyleDeclaration>;
+    textStyle: Partial<CSSStyleDeclaration>;
+    fillStyle: Partial<CSSStyleDeclaration>;
+    progress: number | Decimal;
+    display: CoercableComponent;
+}
diff --git a/src/typings/features/buyable.d.ts b/src/typings/features/buyable.d.ts
new file mode 100644
index 0000000..165bf89
--- /dev/null
+++ b/src/typings/features/buyable.d.ts
@@ -0,0 +1,20 @@
+import { CoercableComponent } from "@/typings/component";
+import { State } from "@/typings/state";
+import Decimal, { DecimalSource } from "@/util/bignum";
+import { Feature } from "./feature";
+
+export interface Buyable extends Feature {
+    amount: Decimal;
+    amountSet?: (amount: Decimal) => void;
+    canBuy: boolean;
+    canAfford: boolean;
+    effect?: State;
+    purchaseLimit: DecimalSource;
+    sellOne?: () => void;
+    sellAll?: () => void;
+    cost?: DecimalSource;
+    buy: () => void;
+    title?: CoercableComponent;
+    display: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+}
diff --git a/src/typings/features/challenge.d.ts b/src/typings/features/challenge.d.ts
new file mode 100644
index 0000000..e083dce
--- /dev/null
+++ b/src/typings/features/challenge.d.ts
@@ -0,0 +1,34 @@
+import { CoercableComponent } from "@/typings/component";
+import { State } from "@/typings/state";
+import { DecimalSource } from "@/util/bignum";
+import { Feature } from "./feature";
+
+export interface Challenge extends Feature {
+    shown: boolean;
+    completed: boolean;
+    completions: DecimalSource;
+    maxed: boolean;
+    active: boolean;
+    effect?: State;
+    canStart: boolean;
+    canComplete: boolean;
+    completionLimit: DecimalSource;
+    mark: boolean | string;
+    goal: DecimalSource;
+    currencyInternalName?: string;
+    currencyDisplayName?: string;
+    currencyLocation?: { [key: string]: DecimalSource };
+    currencyLayer?: string;
+    titleDisplay?: CoercableComponent;
+    name?: CoercableComponent;
+    fullDisplay?: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+    challengeDescription: CoercableComponent;
+    goalDescription?: CoercableComponent;
+    rewardDescription: CoercableComponent;
+    rewardDisplay?: CoercableComponent;
+    toggle: () => void;
+    onComplete?: () => void;
+    onExit?: () => void;
+    onEnter?: () => void;
+}
diff --git a/src/typings/features/clickable.d.ts b/src/typings/features/clickable.d.ts
new file mode 100644
index 0000000..051649f
--- /dev/null
+++ b/src/typings/features/clickable.d.ts
@@ -0,0 +1,15 @@
+import { CoercableComponent } from "@/typings/component";
+import { State } from "@/typings/state";
+import { Feature } from "./feature";
+
+export interface Clickable extends Feature {
+    state: State;
+    stateSet: (state: State) => void;
+    effect?: State;
+    canClick: boolean;
+    click?: () => void;
+    hold?: () => void;
+    style?: Partial<CSSStyleDeclaration>;
+    title?: CoercableComponent;
+    display: CoercableComponent;
+}
diff --git a/src/typings/features/feature.d.ts b/src/typings/features/feature.d.ts
new file mode 100644
index 0000000..4fa74ea
--- /dev/null
+++ b/src/typings/features/feature.d.ts
@@ -0,0 +1,34 @@
+import { Computable } from "@/typings/computable";
+
+export type RawFeature<T extends Feature> = Partial<Computable<T>>;
+
+export interface Feature {
+    id: string;
+    layer: string;
+    unlocked: boolean;
+    [key: string]: unknown;
+}
+
+export interface RawFeatures<T extends Features<S>, S extends Feature>
+    extends Partial<Omit<Computable<T>, "data">>,
+        ThisType<T> {
+    layer?: string;
+    data: Record<string | number, RawFeature<S>>;
+}
+
+export interface Features<T extends Feature> {
+    layer: string;
+    data: Record<string | number, T>;
+    [key: string]: unknown;
+}
+
+export interface GridFeatures<T extends Feature> extends Features<T> {
+    rows: number;
+    cols: number;
+}
+
+export interface RawGridFeatures<T extends GridFeatures<S>, S extends Feature>
+    extends RawFeatures<T, S> {
+    rows?: number;
+    cols?: number;
+}
diff --git a/src/typings/features/grid.d.ts b/src/typings/features/grid.d.ts
new file mode 100644
index 0000000..61c4021
--- /dev/null
+++ b/src/typings/features/grid.d.ts
@@ -0,0 +1,33 @@
+import { State } from "@/typings/state";
+import { Feature } from "./feature";
+
+export interface Grid extends Feature {
+    maxRows: number;
+    rows: number;
+    cols: number;
+    getData?: (cell: string | number) => State;
+    setData?: (cell: string | number, data: State) => void;
+    getUnlocked: boolean | ((cell: string | number) => boolean);
+    getCanClick: boolean | ((cell: string | number) => boolean);
+    getStartData: State | ((cell: string | number) => State);
+    getStyle?:
+        | Partial<CSSStyleDeclaration>
+        | ((cell: string | number) => Partial<CSSStyleDeclaration> | undefined);
+    click?: (cell: string | number) => void;
+    hold?: (cell: string | number) => void;
+    getTitle?: string | ((cell: string | number) => string);
+    getDisplay: string | ((cell: string | number) => string);
+}
+
+export interface GridCell extends Feature {
+    data: State;
+    dataSet: (data: State) => void;
+    effect?: State;
+    unlocked: boolean;
+    canClick: boolean;
+    style?: Partial<CSSStyleDeclaration>;
+    click?: () => void;
+    hold?: () => void;
+    title?: string;
+    display: string;
+}
diff --git a/src/typings/features/hotkey.d.ts b/src/typings/features/hotkey.d.ts
new file mode 100644
index 0000000..bec6a5c
--- /dev/null
+++ b/src/typings/features/hotkey.d.ts
@@ -0,0 +1,8 @@
+import { Feature } from "./feature";
+
+export interface Hotkey extends Feature {
+    unlocked: boolean;
+    press: () => void;
+    description: string;
+    key: string;
+}
diff --git a/src/typings/features/infobox.d.ts b/src/typings/features/infobox.d.ts
new file mode 100644
index 0000000..9eb08fd
--- /dev/null
+++ b/src/typings/features/infobox.d.ts
@@ -0,0 +1,11 @@
+import { CoercableComponent } from "@/component";
+import { Feature } from "./feature";
+
+export interface Infobox extends Feature {
+    borderColor?: string;
+    style?: Partial<CSSStyleDeclaration>;
+    titleStyle?: Partial<CSSStyleDeclaration>;
+    bodyStyle?: Partial<CSSStyleDeclaration>;
+    title?: CoercableComponent;
+    body: CoercableComponent;
+}
diff --git a/src/typings/features/milestone.d.ts b/src/typings/features/milestone.d.ts
new file mode 100644
index 0000000..5659415
--- /dev/null
+++ b/src/typings/features/milestone.d.ts
@@ -0,0 +1,12 @@
+import { Feature } from "./feature";
+
+export interface Milestone extends Feature {
+    earned: boolean;
+    shown: boolean;
+    done: boolean;
+    style?: Partial<CSSStyleDeclaration>;
+    requirementDisplay?: CoercableComponent;
+    effectDisplay?: CoercableComponent;
+    optionsDisplay?: CoercableComponent;
+    onComplete?: () => void;
+}
diff --git a/src/typings/features/subtab.d.ts b/src/typings/features/subtab.d.ts
new file mode 100644
index 0000000..0937f8e
--- /dev/null
+++ b/src/typings/features/subtab.d.ts
@@ -0,0 +1,35 @@
+import { CoercableComponent } from "@/typings/component";
+import { Feature, RawFeature } from "./feature";
+
+export interface Subtab extends Feature {
+    notify?: boolean;
+    prestigeNotify?: boolean;
+    glowColor?: string;
+    active: boolean;
+    unlocked?: boolean;
+    embedLayer?: boolean;
+    display?: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+    buttonStyle?: Partial<CSSStyleDeclaration>;
+}
+
+export interface Microtab extends Feature {
+    family: string;
+    notify?: boolean;
+    prestigeNotify?: boolean;
+    glowColor?: string;
+    active: boolean;
+    embedLayer?: string;
+    display?: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+}
+
+export type RawMicrotabFamily = Omit<RawFeature<MicrotabFamily>, "data"> & {
+    data: Record<string, RawFeature<Microtab>>;
+};
+
+export interface MicrotabFamily extends Feature {
+    activeMicrotab: Microtab | undefined;
+    family: string;
+    data: Record<string, Microtab>;
+}
diff --git a/src/typings/features/upgrade.d.ts b/src/typings/features/upgrade.d.ts
new file mode 100644
index 0000000..efcfb3f
--- /dev/null
+++ b/src/typings/features/upgrade.d.ts
@@ -0,0 +1,23 @@
+import { CoercableComponent } from "@/component";
+import { State } from "@/typings/state";
+import { DecimalSource } from "@/util/bignum";
+import { Feature } from "./feature";
+
+export interface Upgrade extends Feature {
+    bought: boolean;
+    canAfford: boolean;
+    pay: () => void;
+    buy: () => void;
+    cost: DecimalSource;
+    currencyInternalName?: string | number;
+    currencyLocation?: { [key: string]: DecimalSource };
+    currencyLayer?: string;
+    onPurchase?: () => void;
+    title?: CoercableComponent;
+    description: CoercableComponent;
+    effect?: State;
+    effectDisplay?: CoercableComponent;
+    currencyDisplayName?: string;
+    style?: Partial<CSSStyleDeclaration>;
+    fullDisplay?: CoercableComponent;
+}
diff --git a/src/typings/global.d.ts b/src/typings/global.d.ts
new file mode 100644
index 0000000..6bec3c7
--- /dev/null
+++ b/src/typings/global.d.ts
@@ -0,0 +1,27 @@
+import Decimal, { DecimalSource } from "@/util/bignum";
+import { App } from "vue";
+import { PlayerData } from "./player";
+
+declare global {
+    interface Window {
+        vue: App;
+        save: () => void;
+        hardReset: () => void;
+        layers: Dictionary<typeof Proxy>;
+        player: PlayerData;
+        Decimal: typeof Decimal;
+        exponentialFormat: (
+            num: DecimalSource,
+            precision: number,
+            mantissa: boolean = true
+        ) => string;
+        commaFormat: (num: DecimalSource, precision: number) => string;
+        regularFormat: (num: DecimalSource, precision: number) => string;
+        format: (num: DecimalSource, precision?: number, small?: boolean) => string;
+        formatWhole: (num: DecimalSource) => string;
+        formatTime: (s: number) => string;
+        toPlaces: (x: DecimalSource, precision: number, maxAccepted: DecimalSource) => string;
+        formatSmall: (x: DecimalSource, precision?: number) => string;
+        invertOOM: (x: DecimalSource) => Decimal;
+    }
+}
diff --git a/src/typings/layer.d.ts b/src/typings/layer.d.ts
new file mode 100644
index 0000000..a03bd14
--- /dev/null
+++ b/src/typings/layer.d.ts
@@ -0,0 +1,146 @@
+import { LayerType } from "@/game/layers";
+import Decimal, { DecimalSource } from "@/util/bignum";
+import { CoercableComponent } from "./component";
+import { Achievement } from "./features/achievement";
+import { Bar } from "./features/bar";
+import { Buyable } from "./features/buyable";
+import { Challenge } from "./features/challenge";
+import { Clickable } from "./features/clickable";
+import {
+    Feature,
+    Features,
+    GridFeatures,
+    RawFeature,
+    RawFeatures,
+    RawGridFeatures
+} from "./features/feature";
+import { Grid } from "./features/grid";
+import { Hotkey } from "./features/hotkey";
+import { Infobox } from "./features/infobox";
+import { Milestone } from "./features/milestone";
+import { MicrotabFamily, RawMicrotabFamily, Subtab } from "./features/subtab";
+import { Upgrade } from "./features/upgrade";
+import { State } from "./state";
+
+export interface RawLayer extends RawFeature<Layer> {
+    id: string;
+    componentStyles?: RawComponentStyles;
+    achievements?: RawGridFeatures<NonNullable<Layer["achievements"]>, Achievement>;
+    bars?: RawFeatures<NonNullable<Layer["bars"]>, Bar>;
+    buyables?: RawGridFeatures<NonNullable<Layer["buyables"]>, Buyable>;
+    challenges?: RawGridFeatures<NonNullable<Layer["challenges"]>, Challenge>;
+    clickables?: RawGridFeatures<NonNullable<Layer["clickables"]>, Clickable>;
+    grids?: RawFeatures<NonNullable<Layer["grids"]>, Grid>;
+    hotkeys?: RawFeature<Hotkey>[];
+    infoboxes?: RawFeatures<NonNullable<Layer["infoboxes"]>, Infoboxe>;
+    milestones?: RawFeatures<NonNullable<Layer["milestones"]>, Milestone>;
+    subtabs?: Record<string, RawFeature<Subtab>>;
+    microtabs?: Record<string, RawMicrotabFamily>;
+    upgrades?: RawGridFeatures<NonNullable<Layer["upgrades"]>, Upgrade>;
+    startData?: () => Record<string, any>;
+}
+
+export interface Layer extends Feature {
+    id: string;
+    name?: string;
+    type: LayerType;
+    row?: number | string;
+    position?: number;
+    deactivated?: boolean;
+    baseResource?: string;
+    baseAmount?: DecimalSource;
+    requires?: DecimalSource;
+    base?: DecimalSource;
+    exponent?: DecimalSource;
+    effect?: State;
+    effectDisplay?: CoercableComponent;
+    resetDescription?: string;
+    component?: CoercableComponent;
+    midsection?: CoercableComponent;
+    style?: Partial<CSSStyleDeclaration>;
+    nodeStyle?: Partial<CSSStyleDeclaration>;
+    display?: CoercableComponent;
+    shown: boolean;
+    layerShown: boolean | "ghost";
+    color: string;
+    glowColor: string;
+    minWidth: number;
+    displayRow: number | string;
+    symbol: string;
+    canClick?: boolean;
+    trueGlowColor: string;
+    resetGain: Decimal;
+    gainMult?: DecimalSource;
+    directMult?: DecimalSource;
+    gainExp?: DecimalSource;
+    softcap?: DecimalSource;
+    softcapPower?: DecimalSource;
+    passiveGeneration?: DecimalSource | boolean;
+    autoReset?: boolean;
+    resetsNothing?: boolean;
+    autoUpgrade?: boolean;
+    resource: string;
+    showNextAt?: boolean;
+    nextAt: Decimal;
+    nextAtMax: Decimal;
+    canReset: boolean;
+    prestigeButtonDisplay?: CoercableComponent;
+    notify: boolean;
+    tooltip?: CoercableComponent;
+    tooltipLocked?: CoercableComponent;
+    resetNotify: boolean;
+    componentStyles?: ComponentStyles;
+    increaseUnlockOrder?: Array<string>;
+    achievements?: GridFeatures<Achievement>;
+    bars?: Features<Bar>;
+    buyables?: GridFeatures<Buyable> & {
+        respec?: () => void;
+        reset: () => void;
+        respecButtonDisplay?: CoercableComponent;
+        respecWarningDisplay?: CoercableComponent;
+        showRespecButton?: boolean;
+    };
+    challenges?: GridFeatures<Challenge>;
+    activeChallenge?: Challenge | undefined;
+    clickables?: GridFeatures<Clickable> & {
+        masterButtonClick?: () => void;
+        masterButtonDisplay?: CoercableComponent;
+        showMasterButton?: boolean;
+    };
+    grids?: Features<Grid>;
+    hotkeys?: Hotkey[];
+    infoboxes?: Features<Infobox>;
+    milestones?: Features<Milestone>;
+    subtabs?: Record<string, Subtab>;
+    activeSubtab?: Subtab | undefined;
+    microtabs?: Record<string, MicrotabFamily>;
+    upgrades?: GridFeatures<Upgrade>;
+    startData?: () => Record<string, any>;
+    click?: () => void;
+    automate?: () => void;
+    reset: (force?: boolean) => void;
+    onReset: (resettingLayer: string) => void;
+    onPrestige?: (resetGain: Decimal) => void;
+    hardReset: (keep?: Array<string>) => void;
+    update?: (diff: DecimalSource) => void;
+}
+
+export type RawComponentStyles = Partial<Computed<ComponentStyles>>;
+export interface ComponentStyles {
+    achievement?: Partial<CSSStyleDeclaration>;
+    bar?: Partial<CSSStyleDeclaration>;
+    buyable?: Partial<CSSStyleDeclaration>;
+    clickable?: Partial<CSSStyleDeclaration>;
+    challenge?: Partial<CSSStyleDeclaration>;
+    "grid-cell"?: Partial<CSSStyleDeclaration>;
+    infobox?: Partial<CSSStyleDeclaration>;
+    "infobox-title"?: Partial<CSSStyleDeclaration>;
+    "infobox-body"?: Partial<CSSStyleDeclaration>;
+    "main-display"?: Partial<CSSStyleDeclaration>;
+    "master-button"?: Partial<CSSStyleDeclaration>;
+    milestone?: Partial<CSSStyleDeclaration>;
+    "prestige-button"?: Partial<CSSStyleDeclaration>;
+    "respec-button"?: Partial<CSSStyleDeclaration>;
+    upgrade?: Partial<CSSStyleDeclaration>;
+    "tab-button"?: Partial<CSSStyleDeclaration>;
+}
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
new file mode 100644
index 0000000..f45c2cc
--- /dev/null
+++ b/src/typings/player.d.ts
@@ -0,0 +1,66 @@
+import { Themes } from "@/data/themes";
+import { DecimalSource } from "@/lib/break_eternity";
+import Decimal from "@/util/bignum";
+import { MilestoneDisplay } from "./features/milestone";
+import { State } from "./state";
+
+export interface ModSaveData {
+    active?: string;
+    saves?: string[];
+}
+
+export interface PlayerData {
+    id: string;
+    devSpeed?: DecimalSource;
+    points: Decimal;
+    oomps: Decimal;
+    oompsMag: number;
+    name: string;
+    tabs: Array<string>;
+    time: number;
+    autosave: boolean;
+    offlineProd: boolean;
+    offlineTime: Decimal | null;
+    timePlayed: Decimal;
+    keepGoing: boolean;
+    lastTenTicks: Array<number>;
+    showTPS: boolean;
+    msDisplay: MilestoneDisplay;
+    hideChallenges: boolean;
+    theme: Themes;
+    subtabs: {
+        [index: string]: {
+            mainTabs?: string;
+            [index: string]: string;
+        };
+    };
+    minimized: Record<string, boolean>;
+    modID: string;
+    modVersion: string;
+    hasNaN: boolean;
+    NaNPath?: Array<string>;
+    NaNReceiver?: Record<string, unknown> | null;
+    importing: ImportingStatus;
+    saveToImport: string;
+    saveToExport: string;
+    layers: Record<string, LayerSaveData>;
+    [index: string]: unknown;
+}
+
+export interface LayerSaveData {
+    points: Decimal;
+    unlocked: boolean;
+    unlockOrder?: number;
+    forceTooltip?: boolean;
+    resetTime: Decimal;
+    upgrades: Array<string | number>;
+    achievements: Array<string | number>;
+    milestones: Array<string | number>;
+    infoboxes: Record<string | number, boolean>;
+    buyables: Record<string | number, Decimal>;
+    clickables: Record<string | number, State>;
+    challenges: Record<string | number, Decimal>;
+    grids: Record<string | number, Record<string, number, State>>;
+    confirmRespecBuyables: boolean;
+    [index: string]: unknown;
+}
diff --git a/src/typings/state.d.ts b/src/typings/state.d.ts
new file mode 100644
index 0000000..30465fb
--- /dev/null
+++ b/src/typings/state.d.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type State = string | number | boolean | object;
diff --git a/src/typings/theme.d.ts b/src/typings/theme.d.ts
new file mode 100644
index 0000000..d05ddbb
--- /dev/null
+++ b/src/typings/theme.d.ts
@@ -0,0 +1,7 @@
+export interface Theme {
+    variables: {
+        [index: string]: string;
+    };
+    stackedInfoboxes: boolean;
+    floatingTabs: boolean;
+}
diff --git a/src/util/bignum.js b/src/util/bignum.ts
similarity index 64%
rename from src/util/bignum.js
rename to src/util/bignum.ts
index 1adfabe..e4ab5ff 100644
--- a/src/util/bignum.js
+++ b/src/util/bignum.ts
@@ -1,19 +1,22 @@
 // Import Decimal and numberUtils from a different file to globally change which big num library gets used
 // This way switching out big number libraries just needs to happen here, not every file that needs big numbers
-import Decimal, * as numberUtils from '../util/break_eternity';
+import { DecimalSource as RawDecimalSource } from "@/lib/break_eternity";
+import Decimal, * as numberUtils from "@/util/break_eternity";
 
 export const {
-	exponentialFormat,
-	commaFormat,
-	regularFormat,
-	format,
-	formatWhole,
-	formatTime,
-	toPlaces,
-	formatSmall,
-	invertOOM
+    exponentialFormat,
+    commaFormat,
+    regularFormat,
+    format,
+    formatWhole,
+    formatTime,
+    toPlaces,
+    formatSmall,
+    invertOOM
 } = numberUtils;
 
+export type DecimalSource = RawDecimalSource;
+
 window.Decimal = Decimal;
 window.exponentialFormat = exponentialFormat;
 window.commaFormat = commaFormat;
diff --git a/src/util/break_eternity.js b/src/util/break_eternity.js
deleted file mode 100644
index d71bcd0..0000000
--- a/src/util/break_eternity.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import Decimal from '../lib/break_eternity';
-import modInfo from '../data/modInfo';
-
-export default Decimal;
-
-const decimalOne = new Decimal(1);
-
-export function exponentialFormat(num, precision, mantissa = true) {
-	let e = num.log10().floor();
-	let m = num.div(Decimal.pow(10, e));
-	if(m.toStringWithDecimalPlaces(precision) === 10) {
-		m = decimalOne;
-		e = e.add(1);
-	}
-	e = (e.gte(1e9) ? format(e, Math.max(Math.max(precision, 3), modInfo.defaultDecimalsShown)) : (e.gte(10000) ? commaFormat(e, 0) : e.toStringWithDecimalPlaces(0)))
-	if (mantissa) {
-		return m.toStringWithDecimalPlaces(precision)+"e"+e;
-	} else {
-		return "e"+e;
-	}
-}
-
-export function commaFormat(num, precision) {
-	if (num === null || num === undefined) {
-		return "NaN";
-	}
-	if (num.mag < 0.001) {
-		return (0).toFixed(precision);
-	}
-	let init = num.toStringWithDecimalPlaces(precision)
-	let portions = init.split(".")
-	portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
-	if (portions.length == 1) return portions[0]
-	return portions[0] + "." + portions[1]
-}
-
-export function regularFormat(num, precision) {
-	if (num === null || num === undefined) {
-		return "NaN";
-	}
-	if (num.mag < 0.0001) {
-		return (0).toFixed(precision);
-	}
-	if (num.mag < 0.1 && precision !== 0) {
-		precision = Math.max(Math.max(precision, 4), modInfo.defaultDecimalsShown);
-	}
-	return num.toStringWithDecimalPlaces(precision);
-}
-
-export function format(decimal, precision = null, small) {
-	if (precision == null) precision = modInfo.defaultDecimalsShown;
-	small = small || modInfo.allowSmall;
-	decimal = new Decimal(decimal);
-	if (isNaN(decimal.sign)||isNaN(decimal.layer)||isNaN(decimal.mag)) {
-		//player.hasNaN = true;
-		return "NaN";
-	}
-	if (decimal.sign<0) {
-		return "-"+format(decimal.neg(), precision);
-	}
-	if (decimal.mag === Number.POSITIVE_INFINITY) {
-		return "Infinity";
-	}
-	if (decimal.gte("eeee1000")) {
-		const slog = decimal.slog();
-		if (slog.gte(1e6)) {
-			return "F" + format(slog.floor());
-		} else {
-			return Decimal.pow(10, slog.sub(slog.floor())).toStringWithDecimalPlaces(3) + "F" + commaFormat(slog.floor(), 0);
-		}
-	} else if (decimal.gte("1e100000")) {
-		return exponentialFormat(decimal, 0, false);
-	} else if (decimal.gte("1e1000")) {
-		return exponentialFormat(decimal, 0);
-	} else if (decimal.gte(1e9)) {
-		return exponentialFormat(decimal, precision);
-	} else if (decimal.gte(1e3)) {
-		return commaFormat(decimal, 0);
-	} else if (decimal.gte(0.001) || !small) {
-		return regularFormat(decimal, precision);
-	} else if (decimal.eq(0)) {
-		return (0).toFixed(precision);
-	}
-
-	decimal = invertOOM(decimal);
-	if (decimal.lt("1e1000")){
-		const val = exponentialFormat(decimal, precision);
-		return val.replace(/([^(?:e|F)]*)$/, '-$1');
-	} else {
-		return format(decimal, precision) + "⁻¹";
-	}
-}
-
-export function formatWhole(decimal) {
-	decimal = new Decimal(decimal);
-	if (decimal.sign<0) {
-		return "-"+formatWhole(decimal.neg());
-	}
-	if (decimal.gte(1e9)) {
-		return format(decimal);
-	}
-	if (decimal.lte(0.98) && !decimal.eq(0)) {
-		return format(decimal);
-	}
-	return format(decimal, 0);
-}
-
-export function formatTime(s) {
-	if (s<60) {
-		return format(s)+"s";
-	} else if (s<3600) {
-		return formatWhole(Math.floor(s/60))+"m "+format(s%60)+"s";
-	} else if (s<86400) {
-		return formatWhole(Math.floor(s/3600))+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
-	} else if (s<31536000) {
-		return formatWhole(Math.floor(s/84600)%365)+"d " + formatWhole(Math.floor(s/3600)%24)+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
-	} else {
-		return formatWhole(Math.floor(s/31536000))+"y "+formatWhole(Math.floor(s/84600)%365)+"d " + formatWhole(Math.floor(s/3600)%24)+"h "+formatWhole(Math.floor(s/60)%60)+"m "+format(s%60)+"s";
-	}
-}
-
-export function toPlaces(x, precision, maxAccepted) {
-	x = new Decimal(x);
-	let result = x.toStringWithDecimalPlaces(precision);
-	if (new Decimal(result).gte(maxAccepted)) {
-		result = new Decimal(maxAccepted - Math.pow(0.1, precision)).toStringWithDecimalPlaces(precision);
-	}
-	return result;
-}
-
-// Will also display very small numbers
-export function formatSmall(x, precision = null) {
-	return format(x, precision, true);
-}
-
-export function invertOOM(x){
-	let e = x.log10().ceil();
-	let m = x.div(Decimal.pow(10, e));
-	e = e.neg();
-	x = new Decimal(10).pow(e).times(m);
-
-	return x;
-}
diff --git a/src/util/break_eternity.ts b/src/util/break_eternity.ts
new file mode 100644
index 0000000..9a92169
--- /dev/null
+++ b/src/util/break_eternity.ts
@@ -0,0 +1,186 @@
+import Decimal, { DecimalSource } from "@/lib/break_eternity";
+import modInfo from "@/data/modInfo.json";
+
+export default Decimal;
+
+const decimalOne = new Decimal(1);
+
+export function exponentialFormat(num: DecimalSource, precision: number, mantissa = true): string {
+    let e = Decimal.log10(num).floor();
+    let m = Decimal.div(num, Decimal.pow(10, e));
+    if (m.toStringWithDecimalPlaces(precision) === "10") {
+        m = decimalOne;
+        e = e.add(1);
+    }
+    const eString = e.gte(1e9)
+        ? format(e, Math.max(Math.max(precision, 3), modInfo.defaultDecimalsShown))
+        : e.gte(10000)
+        ? commaFormat(e, 0)
+        : e.toStringWithDecimalPlaces(0);
+    if (mantissa) {
+        return m.toStringWithDecimalPlaces(precision) + "e" + eString;
+    } else {
+        return "e" + eString;
+    }
+}
+
+export function commaFormat(num: DecimalSource, precision: number): string {
+    if (num === null || num === undefined) {
+        return "NaN";
+    }
+    num = new Decimal(num);
+    if (num.mag < 0.001) {
+        return (0).toFixed(precision);
+    }
+    const init = num.toStringWithDecimalPlaces(precision);
+    const portions = init.split(".");
+    portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
+    if (portions.length == 1) return portions[0];
+    return portions[0] + "." + portions[1];
+}
+
+export function regularFormat(num: DecimalSource, precision: number): string {
+    if (num === null || num === undefined) {
+        return "NaN";
+    }
+    num = new Decimal(num);
+    if (num.mag < 0.0001) {
+        return (0).toFixed(precision);
+    }
+    if (num.mag < 0.1 && precision !== 0) {
+        precision = Math.max(Math.max(precision, 4), modInfo.defaultDecimalsShown);
+    }
+    return num.toStringWithDecimalPlaces(precision);
+}
+
+export function format(num: DecimalSource, precision?: number, small?: boolean): string {
+    if (precision == null) precision = modInfo.defaultDecimalsShown;
+    small = small || modInfo.allowSmall;
+    num = new Decimal(num);
+    if (isNaN(num.sign) || isNaN(num.layer) || isNaN(num.mag)) {
+        return "NaN";
+    }
+    if (num.sign < 0) {
+        return "-" + format(num.neg(), precision);
+    }
+    if (num.mag === Number.POSITIVE_INFINITY) {
+        return "Infinity";
+    }
+    if (num.gte("eeee1000")) {
+        const slog = num.slog();
+        if (slog.gte(1e6)) {
+            return "F" + format(slog.floor());
+        } else {
+            return (
+                Decimal.pow(10, slog.sub(slog.floor())).toStringWithDecimalPlaces(3) +
+                "F" +
+                commaFormat(slog.floor(), 0)
+            );
+        }
+    } else if (num.gte("1e100000")) {
+        return exponentialFormat(num, 0, false);
+    } else if (num.gte("1e1000")) {
+        return exponentialFormat(num, 0);
+    } else if (num.gte(1e9)) {
+        return exponentialFormat(num, precision);
+    } else if (num.gte(1e3)) {
+        return commaFormat(num, 0);
+    } else if (num.gte(0.001) || !small) {
+        return regularFormat(num, precision);
+    } else if (num.eq(0)) {
+        return (0).toFixed(precision);
+    }
+
+    num = invertOOM(num);
+    if (num.lt("1e1000")) {
+        const val = exponentialFormat(num, precision);
+        return val.replace(/([^(?:e|F)]*)$/, "-$1");
+    } else {
+        return format(num, precision) + "⁻¹";
+    }
+}
+
+export function formatWhole(num: DecimalSource): string {
+    num = new Decimal(num);
+    if (num.sign < 0) {
+        return "-" + formatWhole(num.neg());
+    }
+    if (num.gte(1e9)) {
+        return format(num);
+    }
+    if (num.lte(0.98) && !num.eq(0)) {
+        return format(num);
+    }
+    return format(num, 0);
+}
+
+export function formatTime(s: DecimalSource): string {
+    if (Decimal.gt(s, 2 ^ 51)) {
+        // integer precision limit
+        return format(Decimal.div(s, 31536000)) + "y";
+    }
+    s = new Decimal(s).toNumber();
+    if (s < 60) {
+        return format(s) + "s";
+    } else if (s < 3600) {
+        return formatWhole(Math.floor(s / 60)) + "m " + format(s % 60) + "s";
+    } else if (s < 86400) {
+        return (
+            formatWhole(Math.floor(s / 3600)) +
+            "h " +
+            formatWhole(Math.floor(s / 60) % 60) +
+            "m " +
+            format(s % 60) +
+            "s"
+        );
+    } else if (s < 31536000) {
+        return (
+            formatWhole(Math.floor(s / 84600) % 365) +
+            "d " +
+            formatWhole(Math.floor(s / 3600) % 24) +
+            "h " +
+            formatWhole(Math.floor(s / 60) % 60) +
+            "m " +
+            format(s % 60) +
+            "s"
+        );
+    } else {
+        return (
+            formatWhole(Math.floor(s / 31536000)) +
+            "y " +
+            formatWhole(Math.floor(s / 84600) % 365) +
+            "d " +
+            formatWhole(Math.floor(s / 3600) % 24) +
+            "h " +
+            formatWhole(Math.floor(s / 60) % 60) +
+            "m " +
+            format(s % 60) +
+            "s"
+        );
+    }
+}
+
+export function toPlaces(x: DecimalSource, precision: number, maxAccepted: DecimalSource): string {
+    x = new Decimal(x);
+    let result = x.toStringWithDecimalPlaces(precision);
+    if (new Decimal(result).gte(maxAccepted)) {
+        result = Decimal.sub(maxAccepted, Math.pow(0.1, precision)).toStringWithDecimalPlaces(
+            precision
+        );
+    }
+    return result;
+}
+
+// Will also display very small numbers
+export function formatSmall(x: DecimalSource, precision?: number): string {
+    return format(x, precision, true);
+}
+
+export function invertOOM(x: DecimalSource): Decimal {
+    let e = Decimal.log10(x).ceil();
+    const m = Decimal.div(x, Decimal.pow(10, e));
+    e = e.neg();
+    x = new Decimal(10).pow(e).times(m);
+
+    return x;
+}
diff --git a/src/util/common.js b/src/util/common.js
deleted file mode 100644
index d36f05e..0000000
--- a/src/util/common.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Decimal from './bignum';
-
-// Reference: https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
-export function camelToTitle(camel) {
-	let title = camel.replace(/([A-Z])/g, " $1");
-	title = title.charAt(0).toUpperCase() + title.slice(1);
-	return title;
-}
-
-export function isPlainObject(object) {
-	return Object.prototype.toString.call(object) === '[object Object]';
-}
-
-export function isFunction(func) {
-	return typeof func === 'function';
-}
-
-export function softcap(value, cap, power = 0.5) {
-	if (value.lte(cap)) {
-		return value;
-	} else {
-		return value.pow(power).times(cap.pow(Decimal.sub(1, power)));
-	}
-}
diff --git a/src/util/common.ts b/src/util/common.ts
new file mode 100644
index 0000000..638a8ce
--- /dev/null
+++ b/src/util/common.ts
@@ -0,0 +1,30 @@
+import { DecimalSource } from "@/util/bignum";
+import Decimal from "./bignum";
+
+// Reference:
+// https://stackoverflow.com/questions/7225407/convert-camelcasetext-to-sentence-case-text
+export function camelToTitle(camel: string): string {
+    let title = camel.replace(/([A-Z])/g, " $1");
+    title = title.charAt(0).toUpperCase() + title.slice(1);
+    return title;
+}
+
+export function isPlainObject(object: any): boolean {
+    return Object.prototype.toString.call(object) === "[object Object]";
+}
+
+export function isFunction(func: any): boolean {
+    return typeof func === "function";
+}
+
+export function softcap(
+    value: DecimalSource,
+    cap: DecimalSource,
+    power: DecimalSource = 0.5
+): Decimal {
+    if (Decimal.lte(value, cap)) {
+        return new Decimal(value);
+    } else {
+        return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power)));
+    }
+}
diff --git a/src/util/features.js b/src/util/features.js
deleted file mode 100644
index ef99368..0000000
--- a/src/util/features.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { layers } from '../game/layers';
-
-export function hasUpgrade(layer, id) {
-	return layers[layer]?.upgrades?.[id]?.bought;
-}
-
-export function hasMilestone(layer, id) {
-	return layers[layer]?.milestones?.[id]?.earned;
-}
-
-export function hasAchievement(layer, id) {
-	return layers[layer]?.achievements?.[id]?.earned;
-}
-
-export function hasChallenge(layer, id) {
-	return layers[layer]?.challenges?.[id]?.completed;
-}
-
-export function maxedChallenge(layer, id) {
-	return layers[layer]?.challenges?.[id]?.maxed;
-}
-
-export function challengeCompletions(layer, id) {
-	return layers[layer]?.challenges?.[id]?.completions;
-}
-
-export function inChallenge(layer, id) {
-	return layers[layer]?.challenges?.[id]?.active;
-}
-
-export function getBuyableAmount(layer, id) {
-	return layers[layer]?.buyables?.[id]?.amount;
-}
-
-export function setBuyableAmount(layer, id, amt) {
-	layers[layer].buyables[id].amount = amt;
-}
-
-export function getClickableState(layer, id) {
-	return layers[layer]?.clickables?.[id]?.state;
-}
-
-export function setClickableState(layer, id, state) {
-	layers[layer].clickables[id].state = state;
-}
-
-export function getGridData(layer, id, cell) {
-	return layers[layer]?.grids?.[id]?.[cell];
-}
-
-export function setGridData(layer, id, cell, data) {
-	layers[layer].grids[id][cell] = data;
-}
-
-export function upgradeEffect(layer, id) {
-	return layers[layer]?.upgrades?.[id]?.effect;
-}
-
-export function challengeEffect(layer, id) {
-	return layers[layer]?.challenges?.[id]?.rewardEffect;
-}
-
-export function buyableEffect(layer, id) {
-	return layers[layer]?.buyables?.[id]?.effect;
-}
-
-export function clickableEffect(layer, id) {
-	return layers[layer]?.clickables?.[id]?.effect;
-}
-
-export function achievementEffect(layer, id) {
-	return layers[layer]?.achievements?.[id]?.effect;
-}
-
-export function gridEffect(layer, id, cell) {
-	return layers[layer]?.grids?.[id]?.[cell]?.effect;
-}
diff --git a/src/util/features.ts b/src/util/features.ts
new file mode 100644
index 0000000..8e31c71
--- /dev/null
+++ b/src/util/features.ts
@@ -0,0 +1,89 @@
+import { layers } from "@/game/layers";
+import { GridCell } from "@/typings/features/grid";
+import { State } from "@/typings/state";
+import Decimal, { DecimalSource } from "@/util/bignum";
+
+export function hasUpgrade(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].upgrades?.data[id].bought;
+}
+
+export function hasMilestone(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].milestones?.data[id].earned;
+}
+
+export function hasAchievement(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].achievements?.data[id].earned;
+}
+
+export function hasChallenge(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].challenges?.data[id].completed;
+}
+
+export function maxedChallenge(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].challenges?.data[id].maxed;
+}
+
+export function challengeCompletions(
+    layer: string,
+    id: string | number
+): DecimalSource | undefined {
+    return layers[layer].challenges?.data[id].completions;
+}
+
+export function inChallenge(layer: string, id: string | number): boolean | undefined {
+    return layers[layer].challenges?.data[id].active;
+}
+
+export function getBuyableAmount(layer: string, id: string | number): Decimal | undefined {
+    return layers[layer].buyables?.data[id].amount;
+}
+
+export function setBuyableAmount(layer: string, id: string | number, amt: Decimal): void {
+    if (layers[layer].buyables?.data[id]) {
+        layers[layer].buyables!.data[id].amount = amt;
+    }
+}
+
+export function getClickableState(layer: string, id: string | number): State | undefined {
+    return layers[layer].clickables?.data[id].state;
+}
+
+export function setClickableState(layer: string, id: string | number, state: State): void {
+    if (layers[layer].clickables?.data[id]) {
+        layers[layer].clickables!.data[id].state = state;
+    }
+}
+
+export function getGridData(layer: string, id: string | number, cell: string): State | undefined {
+    return (layers[layer].grids?.data[id][cell] as GridCell).effect;
+}
+
+export function setGridData(layer: string, id: string | number, cell: string, data: State): void {
+    if (layers[layer].grids?.data[id][cell]) {
+        layers[layer].grids!.data[id][cell] = data;
+    }
+}
+
+export function upgradeEffect(layer: string, id: string | number): State | undefined {
+    return layers[layer].upgrades?.data[id].effect;
+}
+
+export function challengeEffect(layer: string, id: string | number): State | undefined {
+    return layers[layer].challenges?.data[id].effect;
+}
+
+export function buyableEffect(layer: string, id: string | number): State | undefined {
+    return layers[layer].buyables?.data[id].effect;
+}
+
+export function clickableEffect(layer: string, id: string | number): State | undefined {
+    return layers[layer].clickables?.data[id].effect;
+}
+
+export function achievementEffect(layer: string, id: string | number): State | undefined {
+    return layers[layer].achievements?.data[id].effect;
+}
+
+export function gridEffect(layer: string, id: string, cell: string | number): State | undefined {
+    return (layers[layer].grids?.data[id][cell] as GridCell).effect;
+}
diff --git a/src/util/layers.js b/src/util/layers.js
deleted file mode 100644
index 5d9c0cb..0000000
--- a/src/util/layers.js
+++ /dev/null
@@ -1,313 +0,0 @@
-import Decimal from './bignum';
-import { isPlainObject } from './common';
-import { layers, hotkeys } from '../game/layers';
-import player from '../game/player';
-
-export function resetLayer(layer, force = false) {
-	layers[layer].reset(force);
-}
-
-export function hardReset(layer, keep = []) {
-	layers[layer].hardReset(keep);
-}
-
-export function cache(func) {
-	func.forceCached = true;
-	return func;
-}
-
-export function noCache(func) {
-	func.forceCached = false;
-	return func;
-}
-
-export function getStartingBuyables(layer) {
-	return layer.buyables && Object.keys(layer.buyables).reduce((acc, curr) => {
-		if (isPlainObject(layer.buyables[curr])) {
-			acc[curr] = new Decimal(0);
-		}
-		return acc;
-	}, {});
-}
-
-export function getStartingClickables(layer) {
-	return layer.clickables && Object.keys(layer.clickables).reduce((acc, curr) => {
-		if (isPlainObject(layer.clickables[curr])) {
-			acc[curr] = "";
-		}
-		return acc;
-	}, {});
-}
-
-export function getStartingChallenges(layer) {
-	return layer.challenges && Object.keys(layer.challenges).reduce((acc, curr) => {
-		if (isPlainObject(layer.challenges[curr])) {
-			acc[curr] = new Decimal(0);
-		}
-		return acc;
-	}, {});
-}
-
-export function resetLayerData(layer, keep = []) {
-	keep.push('unlocked', 'forceTooltip', 'noRespecConfirm');
-	const keptData = keep.reduce((acc, curr) => {
-		acc[curr] = player[layer][curr];
-		return acc;
-	}, {});
-
-	player.upgrades = [];
-	player.achievements = [];
-	player.milestones = [];
-	player.infoboxes = {};
-
-	player[layer].buyables = getStartingBuyables(layers[layer]);
-	player[layer].clickables = getStartingClickables(layers[layer]);
-	player[layer].challenges = getStartingChallenges(layers[layer]);
-
-	Object.assign(player[layer], layers[layer].startData?.());
-
-	for (let item in keptData) {
-		player[layer][item] = keptData[item];
-	}
-}
-
-export function resetRow(row, ignore) {
-	Object.values(layers).filter(layer => layer.row === row && layer.layer !== ignore).forEach(layer => layer.hardReset());
-}
-
-export const defaultLayerProperties = {
-	type: "none",
-	shown: true,
-	layerShown: true,
-	glowColor: "red",
-	minWidth: 640,
-	displayRow() {
-		return this.row;
-	},
-	symbol() {
-		return this.id;
-	},
-	unlocked() {
-		if (player[this.id].unlocked) {
-			return true;
-		}
-		if (this.type !== "none" && this.canReset && this.layerShown) {
-			return true;
-		}
-		return false;
-	},
-	trueGlowColor() {
-		if (this.subtabs) {
-			for (let subtab of Object.values(this.subtabs)) {
-				if (subtab.notify) {
-					return subtab.glowColor || "red";
-				}
-			}
-		}
-		if (this.microtabs) {
-			for (let microtab of Object.values(this.microtabs)) {
-				if (microtab.notify) {
-					return microtab.glowColor || "red";
-				}
-			}
-		}
-		return this.glowColor || "red";
-	},
-	resetGain() {
-		if (this.type === "none" || this.type === "custom") {
-			return new Decimal(0);
-		}
-		if (this.gainExp?.eq(0)) {
-			return new Decimal(0);
-		}
-		if (this.baseAmount.lt(this.requires)) {
-			return new Decimal(0);
-		}
-		if (this.type === "static") {
-			if (!this.canBuyMax) {
-				return new Decimal(1);
-			}
-			let gain = this.baseAmount.div(this.requires).div(this.gainMult || 1).max(1).log(this.base)
-				.times(this.gainExp || 1).pow(Decimal.pow(this.exponent || 1, -1));
-			gain = gain.times(this.directMult || 1);
-			return gain.floor().sub(player[this.layer].points).add(1).max(1);
-		}
-		if (this.type === "normal") {
-			let gain = this.baseAmount.div(this.requires).pow(this.exponent || 1).times(this.gainMult || 1)
-				.pow(this.gainExp || 1);
-			if (this.softcap && gain.gte(this.softcap)) {
-				gain = gain.pow(this.softcapPower).times(this.softcap.pow(Decimal.sub(1, this.softcapPower)));
-			}
-			gain = gain.times(this.directMult || 1);
-			return gain.floor().max(0);
-		}
-		// Unknown prestige type
-		return new Decimal(0);
-	},
-	nextAt() {
-		if (this.type === "none" || this.type === "custom") {
-			return new Decimal(Infinity);
-		}
-		if (this.gainMult?.lte(0) || this.gainExp?.lte(0)) {
-			return new Decimal(Infinity);
-		}
-		if (this.type === "static") {
-			const amount = player[this.layer].points.div(this.directMult || 1);
-			const extraCost = Decimal.pow(this.base, amount.pow(this.exponent || 1).div(this.gainExp || 1))
-				.times(this.gainMult || 1);
-			let cost = extraCost.times(this.requires).max(this.requires);
-			if (this.roundUpCost) {
-				cost = cost.ceil();
-			}
-			return cost;
-		}
-		if (this.type === "normal") {
-			let next = this.resetGain.add(1).div(this.directMult || 1);
-			if (this.softcap && next.gte(this.softcap)) {
-				next = next.div(this.softcap.pow(Decimal.sub(1, this.softcapPower)))
-					.pow(Decimal.div(1, this.softcapPower));
-			}
-			next = next.root(this.gainExp || 1).div(this.gainMult || 1).root(this.exponent || 1)
-				.times(this.requires).max(this.requires);
-			if (this.roundUpCost) {
-				next = next.ceil();
-			}
-			return next;
-		}
-		// Unknown prestige type
-		return new Decimal(0);
-	},
-	nextAtMax() {
-		if (!this.canBuyMax || this.type !== "static") {
-			return this.nextAt;
-		}
-		const amount = player[this.layer].points.plus(this.resetGain).div(this.directMult || 1);
-		const extraCost = Decimal.pow(this.base, amount.pow(this.exponent || 1).div(this.gainExp || 1))
-			.times(this.gainMult || 1);
-		let cost = extraCost.times(this.requires).max(this.requires);
-		if (this.roundUpCost) {
-			cost = cost.ceil();
-		}
-		return cost;
-	},
-	canReset() {
-		if (this.type === "normal") {
-			return this.baseAmount.gte(this.requires);
-		}
-		if (this.type === "static") {
-			return this.baseAmount.gte(this.nextAt);
-		}
-		return false;
-	},
-	notify() {
-		if (this.upgrades) {
-			if (Object.values(this.upgrades).some(upgrade => upgrade.canAfford && !upgrade.bought && upgrade.unlocked)) {
-				return true;
-			}
-		}
-		if (this.activeChallenge?.canComplete) {
-			return true;
-		}
-		if (this.subtabs) {
-			if (Object.values(this.subtabs).some(subtab => subtab.notify)) {
-				return true;
-			}
-		}
-		if (this.microtabs) {
-			if (Object.values(this.microtabs).some(subtab => subtab.notify)) {
-				return true;
-			}
-		}
-
-		return false;
-	},
-	resetNotify() {
-		if (this.subtabs) {
-			if (Object.values(this.subtabs).some(subtab => subtab.prestigeNotify)) {
-				return true;
-			}
-		}
-		if (this.microtabs) {
-			if (Object.values(this.microtabs).some(microtab => microtab.prestigeNotify)) {
-				return true;
-			}
-		}
-		if (this.autoPrestige || this.passiveGeneration) {
-			return false;
-		}
-		if (this.type === "static") {
-			return this.canReset;
-		}
-		if (this.type === "normal") {
-			return this.canReset && this.resetGain.gte(player[this.layer].points.div(10));
-		}
-		return false;
-	},
-	reset(force = false) {
-		if (this.type === 'none') {
-			return;
-		}
-		if (!force) {
-			if (!this.canReset) {
-				return;
-			}
-			this.onPrestige?.(this.resetGain);
-			if (player[this.layer].points != undefined) {
-				player[this.layer].points = player[this.layer].points.add(this.resetGain);
-			}
-			if (!player[this.layer].unlocked) {
-				player[this.layer].unlocked = true;
-				if (this.increaseUnlockOrder) {
-					for (let layer in this.increaseUnlockOrder) {
-						player[layer].unlockOrder = (player[layer].unlockOrder || 0) + 1;
-					}
-				}
-			}
-		}
-
-		if (this.resetsNothing) {
-			return;
-		}
-
-		Object.values(layers).forEach(layer => {
-			if (this.row >= layer.row && (!force || this !== layer)) {
-				this.activeChallenge?.toggle();
-			}
-		});
-
-		player.points = new Decimal(0);
-
-		for (let row = this.row - 1; row >= 0; row--) {
-			resetRow(row, this.layer);
-		}
-		resetRow('side', this.layer);
-
-		if (player[this.layer].resetTime != undefined) {
-			player[this.layer].resetTime = 0;
-		}
-	},
-	hardReset(keep = []) {
-		if (!isNaN(this.row)) {
-			resetLayerData(this.layer, keep);
-		}
-	}
-};
-
-document.onkeydown = function(e) {
-	if (player.hasWon && !player.keepGoing) {
-		return;
-	}
-	let key = e.key;
-	if (e.shiftKey) {
-		key = "shift+" + key;
-	}
-	if (e.ctrlKey) {
-		key = "ctrl+" + key;
-	}
-	if (hotkeys[key]) {
-		e.preventDefault();
-		if (hotkeys[key].unlocked) {
-			hotkeys[key].press?.();
-		}
-	}
-}
diff --git a/src/util/layers.ts b/src/util/layers.ts
new file mode 100644
index 0000000..5bbb2e2
--- /dev/null
+++ b/src/util/layers.ts
@@ -0,0 +1,390 @@
+import { hotkeys, layers } from "@/game/layers";
+import player from "@/game/player";
+import { CacheableFunction } from "@/typings/cacheableFunction";
+import { Buyable } from "@/typings/features/buyable";
+import { Challenge } from "@/typings/features/challenge";
+import { Clickable } from "@/typings/features/clickable";
+import { RawFeature } from "@/typings/features/feature";
+import { MicrotabFamily, Subtab } from "@/typings/features/subtab";
+import { Layer, RawLayer } from "@/typings/layer";
+import { State } from "@/typings/state";
+import Decimal from "./bignum";
+
+export function resetLayer(layer: string, force = false): void {
+    layers[layer].reset(force);
+}
+
+export function hardReset(layer: string, keep: Array<string> = []): void {
+    layers[layer].hardReset(keep);
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export function cache<T extends CacheableFunction | Function>(func: T): T & CacheableFunction {
+    return Object.assign(func, { forceCached: true });
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export function noCache<T extends CacheableFunction | Function>(func: T): T & CacheableFunction {
+    return Object.assign(func, { forceCached: false });
+}
+
+export function getStartingBuyables(
+    buyables?: Record<string, Buyable> | Record<string, RawFeature<Buyable>> | undefined
+): Record<string, Decimal> {
+    return buyables
+        ? Object.keys(buyables).reduce((acc: Record<string, Decimal>, curr: string): Record<
+              string,
+              Decimal
+          > => {
+              acc[curr] = new Decimal(0);
+              return acc;
+          }, {})
+        : {};
+}
+
+export function getStartingClickables(
+    clickables?: Record<string, Clickable> | Record<string, RawFeature<Clickable>> | undefined
+): Record<string, State> {
+    return clickables
+        ? Object.keys(clickables).reduce((acc: Record<string, State>, curr: string): Record<
+              string,
+              State
+          > => {
+              acc[curr] = "";
+              return acc;
+          }, {})
+        : {};
+}
+
+export function getStartingChallenges(
+    challenges?: Record<string, Challenge> | Record<string, RawFeature<Challenge>> | undefined
+): Record<string, Decimal> {
+    return challenges
+        ? Object.keys(challenges).reduce((acc: Record<string, Decimal>, curr: string): Record<
+              string,
+              Decimal
+          > => {
+              acc[curr] = new Decimal(0);
+              return acc;
+          }, {})
+        : {};
+}
+
+export function resetLayerData(layer: string, keep: Array<string> = []): void {
+    keep.push("unlocked", "forceTooltip", "noRespecConfirm");
+    const keptData = keep.reduce((acc: Record<string, any>, curr: string): Record<string, any> => {
+        acc[curr] = player.layers[layer][curr];
+        return acc;
+    }, {});
+
+    player.upgrades = [];
+    player.achievements = [];
+    player.milestones = [];
+    player.infoboxes = {};
+
+    player.layers[layer].buyables = getStartingBuyables(layers[layer].buyables?.data);
+    player.layers[layer].clickables = getStartingClickables(layers[layer].clickables?.data);
+    player.layers[layer].challenges = getStartingChallenges(layers[layer].challenges?.data);
+
+    Object.assign(player.layers[layer], layers[layer].startData?.());
+
+    for (const item in keptData) {
+        player.layers[layer][item] = keptData[item];
+    }
+}
+
+export function resetRow(row: string | number | undefined, ignore?: string): void {
+    Object.values(layers)
+        .filter(layer => layer.row === row && layer.layer !== ignore)
+        .forEach(layer => layer.hardReset());
+}
+
+export const defaultLayerProperties = {
+    type: "none",
+    shown: true,
+    layerShown: true,
+    glowColor: "red",
+    minWidth: 640,
+    displayRow() {
+        return this.row;
+    },
+    symbol() {
+        return this.id;
+    },
+    unlocked() {
+        if (player.layers[this.id].unlocked) {
+            return true;
+        }
+        if (this.type !== "none" && this.canReset && this.layerShown) {
+            return true;
+        }
+        return false;
+    },
+    trueGlowColor() {
+        if (this.subtabs) {
+            for (const subtab of Object.values<Subtab>(this.subtabs)) {
+                if (subtab.notify) {
+                    return subtab.glowColor || "red";
+                }
+            }
+        }
+        if (this.microtabs) {
+            for (const microtabFamily of Object.values<MicrotabFamily>(this.microtabs)) {
+                for (const microtab of Object.values(microtabFamily.data)) {
+                    if (microtab.notify) {
+                        return microtab.glowColor || "red";
+                    }
+                }
+            }
+        }
+        return this.glowColor || "red";
+    },
+    resetGain() {
+        if (this.type === "none" || this.type === "custom") {
+            return new Decimal(0);
+        }
+        if (this.gainExp && Decimal.eq(this.gainExp, 0)) {
+            return new Decimal(0);
+        }
+        if (Decimal.lt(this.baseAmount!, this.requires!)) {
+            return new Decimal(0);
+        }
+        if (this.type === "static") {
+            if (!this.canBuyMax) {
+                return new Decimal(1);
+            }
+            let gain = Decimal.div(this.baseAmount!, this.requires!)
+                .div(this.gainMult || 1)
+                .max(1)
+                .log(this.base!)
+                .times(this.gainExp || 1)
+                .pow(Decimal.pow(this.exponent || 1, -1));
+            gain = gain.times(this.directMult || 1);
+            return gain
+                .floor()
+                .sub(player.layers[this.layer].points)
+                .add(1)
+                .max(1);
+        }
+        if (this.type === "normal") {
+            let gain = Decimal.div(this.baseAmount!, this.requires!)
+                .pow(this.exponent || 1)
+                .times(this.gainMult || 1)
+                .pow(this.gainExp || 1);
+            if (this.softcap && this.softcapPower && gain.gte(this.softcap)) {
+                gain = gain
+                    .pow(this.softcapPower)
+                    .times(Decimal.pow(this.softcap, Decimal.sub(1, this.softcapPower)));
+            }
+            gain = gain.times(this.directMult || 1);
+            return gain.floor().max(0);
+        }
+        // Unknown prestige type
+        return new Decimal(0);
+    },
+    nextAt() {
+        if (this.type === "none" || this.type === "custom") {
+            return new Decimal(Infinity);
+        }
+        if (
+            (this.gainMult && Decimal.lte(this.gainMult, 0)) ||
+            (this.gainExp && Decimal.lte(this.gainExp, 0))
+        ) {
+            return new Decimal(Infinity);
+        }
+        if (this.type === "static") {
+            const amount = player.layers[this.layer].points.div(this.directMult || 1);
+            const extraCost = Decimal.pow(
+                this.base!,
+                amount.pow(this.exponent || 1).div(this.gainExp || 1)
+            ).times(this.gainMult || 1);
+            let cost = extraCost.times(this.requires!).max(this.requires!);
+            if (this.roundUpCost) {
+                cost = cost.ceil();
+            }
+            return cost;
+        }
+        if (this.type === "normal") {
+            let next = this.resetGain.add(1).div(this.directMult || 1);
+            if (this.softcap && this.softcapPower && next.gte(this.softcap)) {
+                next = next
+                    .div(Decimal.pow(this.softcap, Decimal.sub(1, this.softcapPower)))
+                    .pow(Decimal.div(1, this.softcapPower));
+            }
+            next = next
+                .root(this.gainExp || 1)
+                .div(this.gainMult || 1)
+                .root(this.exponent || 1)
+                .times(this.requires!)
+                .max(this.requires!);
+            if (this.roundUpCost) {
+                next = next.ceil();
+            }
+            return next;
+        }
+        // Unknown prestige type
+        return new Decimal(0);
+    },
+    nextAtMax() {
+        if (!this.canBuyMax || this.type !== "static") {
+            return this.nextAt;
+        }
+        const amount = player.layers[this.layer].points
+            .plus(this.resetGain)
+            .div(this.directMult || 1);
+        const extraCost = Decimal.pow(
+            this.base!,
+            amount.pow(this.exponent || 1).div(this.gainExp || 1)
+        ).times(this.gainMult || 1);
+        let cost = extraCost.times(this.requires!).max(this.requires!);
+        if (this.roundUpCost) {
+            cost = cost.ceil();
+        }
+        return cost;
+    },
+    canReset() {
+        if (this.type === "normal") {
+            return Decimal.gte(this.baseAmount!, this.requires!);
+        }
+        if (this.type === "static") {
+            return Decimal.gte(this.baseAmount!, this.nextAt);
+        }
+        return false;
+    },
+    notify() {
+        if (this.upgrades) {
+            if (
+                Object.values(this.upgrades.data).some(
+                    upgrade => upgrade.canAfford && !upgrade.bought && upgrade.unlocked
+                )
+            ) {
+                return true;
+            }
+        }
+        if (this.activeChallenge?.canComplete) {
+            return true;
+        }
+        if (this.subtabs) {
+            if (Object.values(this.subtabs).some(subtab => subtab.notify)) {
+                return true;
+            }
+        }
+        if (this.microtabs) {
+            for (const microtabFamily of Object.values(this.microtabs)) {
+                if (Object.values(microtabFamily.data).some(subtab => subtab.notify)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    },
+    resetNotify() {
+        if (this.subtabs) {
+            if (Object.values(this.subtabs).some(subtab => subtab.prestigeNotify)) {
+                return true;
+            }
+        }
+        if (this.microtabs) {
+            for (const microtabFamily of Object.values(this.microtabs)) {
+                if (Object.values(microtabFamily.data).some(subtab => subtab.prestigeNotify)) {
+                    return true;
+                }
+            }
+        }
+        if (this.autoPrestige || this.passiveGeneration) {
+            return false;
+        }
+        if (this.type === "static") {
+            return this.canReset;
+        }
+        if (this.type === "normal") {
+            return this.canReset && this.resetGain.gte(player.layers[this.layer].points.div(10));
+        }
+        return false;
+    },
+    reset(force = false) {
+        if (this.type === "none") {
+            return;
+        }
+        if (!force) {
+            if (!this.canReset) {
+                return;
+            }
+            this.onPrestige?.(this.resetGain);
+            if (player.layers[this.layer].points != undefined) {
+                player.layers[this.layer].points = player.layers[this.layer].points.add(
+                    this.resetGain
+                );
+            }
+            if (!player.layers[this.layer].unlocked) {
+                player.layers[this.layer].unlocked = true;
+                if (this.increaseUnlockOrder) {
+                    for (const layer in this.increaseUnlockOrder) {
+                        player.layers[layer].unlockOrder =
+                            (player.layers[layer].unlockOrder || 0) + 1;
+                    }
+                }
+            }
+        }
+
+        if (this.resetsNothing) {
+            return;
+        }
+
+        Object.values(layers)
+            .filter(layer => typeof layer.row === "number")
+            .forEach(layer => {
+                if ((this.row as number) >= (layer.row as number) && (!force || this !== layer)) {
+                    this.activeChallenge?.toggle();
+                }
+            });
+
+        player.points = new Decimal(0);
+
+        Object.values(layers)
+            .sort((a, b) => {
+                if (typeof a.row !== "number" || typeof b.row !== "number") {
+                    return 0;
+                }
+                return a.row - b.row;
+            })
+            .forEach(layer => layer.onReset(this.layer));
+
+        if (player.layers[this.layer].resetTime != undefined) {
+            player.layers[this.layer].resetTime = new Decimal(0);
+        }
+    },
+    onReset(resettingLayer: string) {
+        if (
+            typeof layers[resettingLayer].row === "number" &&
+            typeof this.row === "number" &&
+            (layers[resettingLayer].row as number) > this.row
+        ) {
+            this.hardReset();
+        }
+    },
+    hardReset(keep = []) {
+        if (!isNaN(Number(this.row))) {
+            resetLayerData(this.layer, keep);
+        }
+    }
+} as Omit<RawLayer, "id"> & Partial<Pick<RawLayer, "id">> & ThisType<Layer>;
+
+document.onkeydown = function(e) {
+    if (player.hasWon && !player.keepGoing) {
+        return;
+    }
+    let key = e.key;
+    if (e.shiftKey) {
+        key = "shift+" + key;
+    }
+    if (e.ctrlKey) {
+        key = "ctrl+" + key;
+    }
+    const hotkey = hotkeys.find(hotkey => hotkey.key === key);
+    if (hotkey && hotkey.unlocked) {
+        e.preventDefault();
+        hotkey.press?.();
+    }
+};
diff --git a/src/util/proxies.js b/src/util/proxies.js
deleted file mode 100644
index 6638cf5..0000000
--- a/src/util/proxies.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import { isFunction, isPlainObject } from './common';
-import Decimal from './bignum';
-import { isRef, computed } from 'vue';
-
-export function createProxy(object) {
-	if (object.isProxy) {
-		console.warn("Creating a proxy out of a proxy! This may cause unintentional function calls and stack overflows.");
-	}
-	const objectProxy = new Proxy(object, mainHandler);
-	travel(createProxy, object, objectProxy);
-	return objectProxy;
-}
-
-// TODO cache grid values? Currently they'll be calculated every render they're visible
-export function createGridProxy(object) {
-	if (object.isProxy) {
-		console.warn("Creating a proxy out of a proxy! This may cause unintentional function calls and stack overflows.");
-	}
-	const objectProxy = new Proxy(object, gridHandler);
-	travel(createGridProxy, object, objectProxy);
-	return objectProxy;
-}
-
-function travel(callback, object, objectProxy) {
-	for (let key in object) {
-		if (object[key] == undefined || object[key].isProxy) {
-			continue;
-		}
-		if (isFunction(object[key])) {
-			if ((object[key].length !== 0 && object[key].forceCached !== true) || object[key].forceCached === false) {
-				continue;
-			}
-			object[key] = computed(object[key].bind(objectProxy));
-		} else if ((isPlainObject(object[key]) || Array.isArray(object[key])) && !(object[key] instanceof Decimal)) {
-			object[key] = callback(object[key]);
-		}
-	}
-}
-
-const mainHandler = {
-	get(target, key, receiver) {
-		if (key === 'isProxy') {
-			return true;
-		}
-
-		if (target[key] == undefined) {
-			return;
-		}
-
-		if (isRef(target[key])) {
-			return target[key].value;
-		} else if (target[key].isProxy || target[key] instanceof Decimal) {
-			return target[key];
-		} else if ((isPlainObject(target[key]) || Array.isArray(target[key])) && key.slice(0, 2) !== '__') {
-			console.warn("Creating proxy outside `createProxy`. This may cause issues when calling proxied functions.",
-				target, key);
-			target[key] = new Proxy(target[key], mainHandler);
-			return target[key];
-		} else if (isFunction(target[key])) {
-			return target[key].bind(receiver);
-		}
-		return target[key];
-	},
-	set(target, key, value, receiver) {
-		if (`${key}Set` in target && isFunction(target[`${key}Set`]) && target[`${key}Set`].length < 2) {
-			target[`${key}Set`].call(receiver, value);
-			return true;
-		} else {
-			console.warn(`No setter for "${key}".`, target);
-		}
-	}
-};
-
-const gridHandler = {
-	get(target, key, receiver) {
-		if (key === 'isProxy') {
-			return true;
-		}
-
-		if (isRef(target[key])) {
-			return target[key].value;
-		} else if (target[key] && (target[key].isProxy || target[key] instanceof Decimal)) {
-			return target[key];
-		} else if (isPlainObject(target[key]) || Array.isArray(target[key])) {
-			console.warn("Creating proxy outside `createProxy`. This may cause issues when calling proxied functions.",
-				target, key);
-			target[key] = new Proxy(target[key], mainHandler);
-			return target[key];
-		} else if (isFunction(target[key])) {
-			return target[key].bind(receiver);
-		}
-		if (typeof key !== 'symbol' && !isNaN(key)) {
-			target[key] = new Proxy(target, getCellHandler(key));
-		}
-		return target[key];
-	},
-	set(target, key, value, receiver) {
-		if (`${key}Set` in target && isFunction(target[`${key}Set`]) && target[`${key}Set`].length < 2) {
-			target[`${key}Set`].call(receiver, value);
-			return true;
-		} else {
-			console.warn(`No setter for "${key}".`, target);
-		}
-	}
-};
-
-function getCellHandler(id) {
-	return {
-		get(target, key, receiver) {
-			if (key === 'isProxy') {
-				return true;
-			}
-
-			let prop = target[key];
-
-			if (isFunction(prop) && prop.forceCached === false) {
-				return () => prop.call(receiver, id, target.getData(id));
-			}
-			if (prop != undefined || key.slice == undefined) {
-				return prop;
-			}
-
-			key = key.slice(0, 1).toUpperCase() + key.slice(1);
-			prop = target[`get${key}`];
-			if (isFunction(prop)) {
-				return prop.call(receiver, id, target.getData(id));
-			} else if (prop != undefined) {
-				return prop;
-			}
-
-			prop = target[`on${key}`];
-			if (isFunction(prop)) {
-				return () => prop.call(receiver, id, target.getData(id));
-			} else if (prop != undefined) {
-				return prop;
-			}
-
-			return target[key];
-		},
-		set(target, key, value, receiver) {
-			if (`${key}Set` in target && isFunction(target[`${key}Set`]) && target[`${key}Set`].length < 3) {
-				target[`${key}Set`].call(receiver, id, value);
-				return true;
-			} else {
-				console.warn(`No setter for "${key}".`, target);
-			}
-		}
-	};
-}
diff --git a/src/util/proxies.ts b/src/util/proxies.ts
new file mode 100644
index 0000000..57fa533
--- /dev/null
+++ b/src/util/proxies.ts
@@ -0,0 +1,187 @@
+import { computed, isRef } from "vue";
+import Decimal from "./bignum";
+import { isFunction, isPlainObject } from "./common";
+
+export function createLayerProxy(object: Record<string, any>): Record<string, any> {
+    if (object.isProxy) {
+        console.warn(
+            "Creating a proxy out of a proxy! This may cause unintentional function calls and stack overflows."
+        );
+    }
+    const objectProxy = new Proxy(object, layerHandler);
+    travel(createLayerProxy, object, objectProxy);
+    return objectProxy;
+}
+
+// TODO cache grid values? Currently they'll be calculated every render they're visible
+export function createGridProxy(object: Record<string, any>): Record<string, any> {
+    if (object.isProxy) {
+        console.warn(
+            "Creating a proxy out of a proxy! This may cause unintentional function calls and stack overflows."
+        );
+    }
+    const objectProxy = new Proxy(object, gridHandler);
+    travel(createGridProxy, object, objectProxy);
+    return objectProxy;
+}
+
+function travel(
+    callback: (object: Record<string, any>) => void,
+    object: Record<string, any>,
+    objectProxy: Record<string, any>
+) {
+    for (const key in object) {
+        if (object[key] == undefined || object[key].isProxy) {
+            continue;
+        }
+        if (isFunction(object[key])) {
+            if (
+                (object[key].length !== 0 && object[key].forceCached !== true) ||
+                object[key].forceCached === false
+            ) {
+                continue;
+            }
+            object[key] = computed(object[key].bind(objectProxy));
+        } else if (
+            (isPlainObject(object[key]) || Array.isArray(object[key])) &&
+            !(object[key] instanceof Decimal)
+        ) {
+            object[key] = callback(object[key]);
+        }
+    }
+}
+
+const layerHandler: ProxyHandler<Record<string, any>> = {
+    get(target: Record<string, any>, key: string, receiver: typeof Proxy): any {
+        if (key === "isProxy") {
+            return true;
+        }
+
+        if (target[key] == undefined) {
+            return;
+        }
+
+        if (isRef(target[key])) {
+            return target[key].value;
+        } else if (target[key].isProxy || target[key] instanceof Decimal) {
+            return target[key];
+        } else if (
+            (isPlainObject(target[key]) || Array.isArray(target[key])) &&
+            key.slice(0, 2) !== "__"
+        ) {
+            console.warn(
+                "Creating proxy outside `createProxy`. This may cause issues when calling proxied functions.",
+                target,
+                key
+            );
+            target[key] = new Proxy(target[key], layerHandler);
+            return target[key];
+        } else if (isFunction(target[key])) {
+            return target[key].bind(receiver);
+        }
+        return target[key];
+    },
+    set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean {
+        if (
+            `${key}Set` in target &&
+            isFunction(target[`${key}Set`]) &&
+            target[`${key}Set`].length < 2
+        ) {
+            target[`${key}Set`].call(receiver, value);
+            return true;
+        } else {
+            console.warn(`No setter for "${key}".`, target);
+            return false;
+        }
+    }
+};
+
+const gridHandler: ProxyHandler<Record<string, any>> = {
+    get(target: Record<string, any>, key: string, receiver: typeof Proxy): any {
+        if (key === "isProxy") {
+            return true;
+        }
+
+        if (isRef(target[key])) {
+            return target[key].value;
+        } else if (target[key] && (target[key].isProxy || target[key] instanceof Decimal)) {
+            return target[key];
+        } else if (isPlainObject(target[key]) || Array.isArray(target[key])) {
+            console.warn(
+                "Creating proxy outside `createProxy`. This may cause issues when calling proxied functions.",
+                target,
+                key
+            );
+            target[key] = new Proxy(target[key], layerHandler);
+            return target[key];
+        } else if (isFunction(target[key])) {
+            return target[key].bind(receiver);
+        }
+        if (typeof key !== "symbol" && !isNaN(Number(key))) {
+            target[key] = new Proxy(target, getCellHandler(key));
+        }
+        return target[key];
+    },
+    set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean {
+        if (
+            `${key}Set` in target &&
+            isFunction(target[`${key}Set`]) &&
+            target[`${key}Set`].length < 2
+        ) {
+            target[`${key}Set`].call(receiver, value);
+            return true;
+        } else {
+            console.warn(`No setter for "${key}".`, target);
+            return false;
+        }
+    }
+};
+
+function getCellHandler(id: string) {
+    return {
+        get(target: Record<string, any>, key: string, receiver: typeof Proxy): any {
+            if (key === "isProxy") {
+                return true;
+            }
+
+            let prop = target[key];
+
+            if (isFunction(prop) && prop.forceCached === false) {
+                return () => prop.call(receiver, id, target.getData(id));
+            }
+            if (prop != undefined || key.slice == undefined) {
+                return prop;
+            }
+
+            key = key.slice(0, 1).toUpperCase() + key.slice(1);
+            prop = target[`get${key}`];
+            if (isFunction(prop)) {
+                return prop.call(receiver, id, target.getData(id));
+            } else if (prop != undefined) {
+                return prop;
+            }
+
+            prop = target[`on${key}`];
+            if (isFunction(prop)) {
+                return () => prop.call(receiver, id, target.getData(id));
+            } else if (prop != undefined) {
+                return prop;
+            }
+
+            return target[key];
+        },
+        set(target: Record<string, any>, key: string, value: any, receiver: typeof Proxy): boolean {
+            if (
+                `${key}Set` in target &&
+                isFunction(target[`${key}Set`]) &&
+                target[`${key}Set`].length < 3
+            ) {
+                target[`${key}Set`].call(receiver, id, value);
+                return true;
+            } else {
+                console.warn(`No setter for "${key}".`, target);
+                return false;
+            }
+        }
+    };
+}
diff --git a/src/util/save.js b/src/util/save.js
deleted file mode 100644
index 3cc865a..0000000
--- a/src/util/save.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import modInfo from '../data/modInfo';
-import { getStartingData, getInitialLayers, fixOldSave } from '../data/mod';
-import player from '../game/player';
-import Decimal from './bignum';
-
-export const NOT_IMPORTING = false;
-export const IMPORTING = true;
-export const IMPORTING_FAILED = "FAILED";
-export const IMPORTING_WRONG_ID = "WRONG_ID";
-export const IMPORTING_FORCE = "FORCE";
-
-export function getInitialStore(playerData = {}) {
-	return applyPlayerData({
-		id: `${modInfo.id}-0`,
-		name: "Default Save",
-		tabs: modInfo.initialTabs.slice(),
-		time: Date.now(),
-		autosave: true,
-		offlineProd: true,
-		timePlayed: new Decimal(0),
-		keepGoing: false,
-		lastTenTicks: [],
-		showTPS: true,
-		msDisplay: "all",
-		hideChallenges: false,
-		theme: "paper",
-		subtabs: {},
-		minimized: {},
-		modID: modInfo.id,
-		modVersion: modInfo.versionNumber,
-		...getStartingData(),
-
-		// Values that don't get loaded/saved
-		hasNaN: false,
-		NaNPath: [],
-		NaNReceiver: null,
-		importing: NOT_IMPORTING,
-		saveToImport: "",
-		saveToExport: ""
-	}, playerData);
-}
-
-export function save() {
-	/* eslint-disable-next-line no-unused-vars */
-	let { hasNaN, NaNPath, NaNReceiver, importing, saveToImport, saveToExport, ...playerData } = player.__state;
-	player.saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(playerData))));
-
-	localStorage.setItem(player.id, player.saveToExport);
-}
-
-export async function load() {
-	try {
-		let modData = localStorage.getItem(modInfo.id);
-		if (modData == null) {
-			await loadSave(newSave());
-			return;
-		}
-		modData = JSON.parse(decodeURIComponent(escape(atob(modData))));
-		if (modData?.active == null) {
-			await loadSave(newSave());
-			return;
-		}
-		const save = localStorage.getItem(modData.active);
-		const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
-		if (playerData.modID !== modInfo.id) {
-			await loadSave(newSave());
-			return;
-		}
-		playerData.id = modData.active;
-		await loadSave(playerData);
-	} catch (e) {
-		await loadSave(newSave());
-	}
-}
-
-export async function newSave() {
-	const id = getUniqueID();
-	const playerData = getInitialStore({ id });
-	localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
-
-	if (!localStorage.getItem(modInfo.id)) {
-		const modData = { active: id, saves: [ id ] };
-		localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-	} else {
-		const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)))));
-		modData.saves.push(id);
-		localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-	}
-
-	return playerData;
-}
-
-export function getUniqueID() {
-	let id, i = 0;
-	do {
-		id = `${modInfo.id}-${i++}`;
-	} while (localStorage.getItem(id));
-	return id;
-}
-
-export async function loadSave(playerData) {
-	const { layers, removeLayer, addLayer } = await import('../game/layers');
-
-	for (let layer in layers) {
-		removeLayer(layer);
-	}
-	getInitialLayers(playerData).forEach(layer => addLayer(layer, playerData));
-
-	playerData = getInitialStore(playerData);
-	if (playerData.offlineProd) {
-		if (playerData.offTime === undefined)
-			playerData.offTime = { remain: 0 };
-		playerData.offTime.remain += (Date.now() - playerData.time) / 1000;
-	}
-	playerData.time = Date.now();
-	if (playerData.modVersion !== modInfo.versionNumber) {
-		fixOldSave(playerData.modVersion, playerData);
-	}
-
-	Object.assign(player, playerData);
-	for (let prop in player) {
-		if (!(prop in playerData) && !(prop in layers) && prop !== '__state' && prop !== '__path') {
-			delete player[prop];
-		}
-	}
-}
-
-export function applyPlayerData(target, source, destructive = false) {
-	for (let prop in source) {
-		if (target[prop] == null) {
-			target[prop] = source[prop];
-		} else if (target[prop] instanceof Decimal) {
-			target[prop] = new Decimal(source[prop]);
-		} else if (Array.isArray(target[prop]) || typeof target[prop] === 'object') {
-			target[prop] = applyPlayerData(target[prop], source[prop], destructive);
-		} else {
-			target[prop] = source[prop];
-		}
-	}
-	if (destructive) {
-		for (let prop in target) {
-			if (!(prop in source)) {
-				delete target[prop];
-			}
-		}
-	}
-	return target;
-}
-
-setInterval(() => {
-	if (player.autosave) {
-		save();
-	}
-}, 1000);
-window.onbeforeunload = () => {
-	if (player.autosave) {
-		save();
-	}
-};
-window.save = save;
diff --git a/src/util/save.ts b/src/util/save.ts
new file mode 100644
index 0000000..ccf6a53
--- /dev/null
+++ b/src/util/save.ts
@@ -0,0 +1,191 @@
+import { fixOldSave, getInitialLayers, getStartingData } from "@/data/mod";
+import modInfo from "@/data/modInfo.json";
+import { Themes } from "@/data/themes";
+import { ImportingStatus, MilestoneDisplay } from "@/game/enums";
+import player from "@/game/player";
+import { ModSaveData, PlayerData } from "@/typings/player";
+import Decimal from "./bignum";
+
+export function getInitialStore(playerData: Partial<PlayerData> = {}): PlayerData {
+    return applyPlayerData(
+        {
+            id: `${modInfo.id}-0`,
+            points: new Decimal(0),
+            oomps: new Decimal(0),
+            oompsMag: 0,
+            name: "Default Save",
+            tabs: modInfo.initialTabs.slice(),
+            time: Date.now(),
+            autosave: true,
+            offlineProd: true,
+            timePlayed: new Decimal(0),
+            keepGoing: false,
+            lastTenTicks: [],
+            showTPS: true,
+            msDisplay: MilestoneDisplay.All,
+            hideChallenges: false,
+            theme: Themes.Paper,
+            subtabs: {},
+            minimized: {},
+            modID: modInfo.id,
+            modVersion: modInfo.versionNumber,
+            layers: {},
+            ...getStartingData(),
+
+            // Values that don't get loaded/saved
+            hasNaN: false,
+            NaNPath: [],
+            NaNReceiver: null,
+            importing: ImportingStatus.NotImporting,
+            saveToImport: "",
+            saveToExport: ""
+        },
+        playerData
+    ) as PlayerData;
+}
+
+export function save(): void {
+    /* eslint-disable @typescript-eslint/no-unused-vars */
+    const {
+        hasNaN,
+        NaNPath,
+        NaNReceiver,
+        importing,
+        saveToImport,
+        saveToExport,
+        ...playerData
+    } = player.__state as PlayerData;
+    /* eslint-enable @typescript-eslint/no-unused-vars */
+    player.saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(playerData))));
+
+    localStorage.setItem(player.id, player.saveToExport);
+}
+
+export async function load(): Promise<void> {
+    try {
+        let modData: string | ModSaveData | null = localStorage.getItem(modInfo.id);
+        if (modData == null) {
+            await loadSave(newSave());
+            return;
+        }
+        modData = JSON.parse(decodeURIComponent(escape(atob(modData)))) as ModSaveData;
+        if (modData?.active == null) {
+            await loadSave(newSave());
+            return;
+        }
+        const save = localStorage.getItem(modData.active);
+        if (save == null) {
+            await loadSave(newSave());
+            return;
+        }
+        const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
+        if (playerData.modID !== modInfo.id) {
+            await loadSave(newSave());
+            return;
+        }
+        playerData.id = modData.active;
+        await loadSave(playerData);
+    } catch (e) {
+        await loadSave(newSave());
+    }
+}
+
+export function newSave(): PlayerData {
+    const id = getUniqueID();
+    const playerData = getInitialStore({ id });
+    localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(playerData)))));
+
+    const rawModData = localStorage.getItem(modInfo.id);
+    if (rawModData == null) {
+        const modData = { active: id, saves: [id] };
+        localStorage.setItem(
+            modInfo.id,
+            btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+        );
+    } else {
+        const modData = JSON.parse(decodeURIComponent(escape(atob(rawModData))));
+        modData.saves.push(id);
+        localStorage.setItem(
+            modInfo.id,
+            btoa(unescape(encodeURIComponent(JSON.stringify(modData))))
+        );
+    }
+
+    return playerData;
+}
+
+export function getUniqueID(): string {
+    let id,
+        i = 0;
+    do {
+        id = `${modInfo.id}-${i++}`;
+    } while (localStorage.getItem(id));
+    return id;
+}
+
+export async function loadSave(playerData: Partial<PlayerData>): Promise<void> {
+    const { layers, removeLayer, addLayer } = await import("../game/layers");
+
+    for (const layer in layers) {
+        removeLayer(layer);
+    }
+    getInitialLayers(playerData).forEach(layer => addLayer(layer, playerData));
+
+    playerData = getInitialStore(playerData);
+    if (playerData.offlineProd && playerData.time) {
+        if (playerData.offlineTime == undefined) playerData.offlineTime = new Decimal(0);
+        playerData.offlineTime = playerData.offlineTime.add((Date.now() - playerData.time) / 1000);
+    }
+    playerData.time = Date.now();
+    if (playerData.modVersion !== modInfo.versionNumber) {
+        fixOldSave(playerData.modVersion, playerData);
+    }
+
+    Object.assign(player, playerData);
+    for (const prop in player) {
+        if (!(prop in playerData) && !(prop in layers) && prop !== "__state" && prop !== "__path") {
+            delete player.layers[prop];
+        }
+    }
+}
+
+export function applyPlayerData<T extends Record<string, any>>(
+    target: T,
+    source: T,
+    destructive = false
+): T {
+    for (const prop in source) {
+        if (target[prop] == null) {
+            target[prop] = source[prop];
+        } else if (target[prop as string] instanceof Decimal) {
+            target[prop as keyof T] = new Decimal(source[prop]) as any;
+        } else if (Array.isArray(target[prop]) || typeof target[prop] === "object") {
+            target[prop] = applyPlayerData(target[prop], source[prop], destructive);
+        } else {
+            target[prop] = source[prop];
+        }
+    }
+    if (destructive) {
+        for (const prop in target) {
+            if (!(prop in source)) {
+                delete target[prop];
+            }
+        }
+    }
+    return target;
+}
+
+setInterval(() => {
+    if (player.autosave) {
+        save();
+    }
+}, 1000);
+window.onbeforeunload = () => {
+    if (player.autosave) {
+        save();
+    }
+};
+window.save = save;
+window.hardReset = () => {
+    loadSave(newSave());
+};
diff --git a/src/util/vue.js b/src/util/vue.js
deleted file mode 100644
index 4b0dbb4..0000000
--- a/src/util/vue.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import player from '../game/player';
-import { layers } from '../game/layers';
-import { hasWon, pointGain } from '../data/mod';
-import { hasUpgrade, hasMilestone, hasAchievement, hasChallenge, maxedChallenge, challengeCompletions, inChallenge, getBuyableAmount, setBuyableAmount, getClickableState, setClickableState, getGridData, setGridData, upgradeEffect, challengeEffect, buyableEffect, clickableEffect, achievementEffect, gridEffect } from './features';
-import Decimal, * as numberUtils from './bignum';
-
-let vue;
-export function setVue(vm) {
-	vue = vm;
-}
-
-// Pass in various data that the template could potentially use
-const data = function() {
-	return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
-}
-export function coerceComponent(component, defaultWrapper = 'span') {
-	if (typeof component === 'number') {
-		component = "" + component;
-	}
-	if (typeof component === 'string') {
-		component = component.trim();
-		if (!(component in vue._context.components)) {
-			if (component.charAt(0) !== '<') {
-				component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
-			}
-
-			return { template: component, data, inject: [ 'tab' ], methods: { hasUpgrade, hasMilestone, hasAchievement, hasChallenge, maxedChallenge, challengeCompletions, inChallenge, getBuyableAmount, setBuyableAmount, getClickableState, setClickableState, getGridData, setGridData, upgradeEffect, challengeEffect, buyableEffect, clickableEffect, achievementEffect, gridEffect } };
-		}
-	}
-	return component;
-}
-
-export function getFiltered(objects, filter = null) {
-	if (filter) {
-		filter = filter.map(v => v.toString());
-		return Object.keys(objects)
-			.filter(key => filter.includes(key))
-			.reduce((acc, curr) => {
-				acc[curr] = objects[curr];
-				return acc;
-			}, {});
-	}
-	return objects;
-}
-
-export function mapState(properties = []) {
-	return properties.reduce((acc, curr) => {
-		acc[curr] = () => player[curr];
-		return acc;
-	}, {});
-}
-
-export const UP = 'UP';
-export const DOWN = 'DOWN';
-export const LEFT = 'LEFT';
-export const RIGHT = 'RIGHT';
-export const DEFAULT = 'DEFAULT';
diff --git a/src/util/vue.ts b/src/util/vue.ts
new file mode 100644
index 0000000..28f12ad
--- /dev/null
+++ b/src/util/vue.ts
@@ -0,0 +1,114 @@
+import { hasWon, pointGain } from "@/data/mod";
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { App, Component, ComponentOptions, defineComponent, inject } from "vue";
+import Decimal, * as numberUtils from "./bignum";
+import {
+    achievementEffect,
+    buyableEffect,
+    challengeCompletions,
+    challengeEffect,
+    clickableEffect,
+    getBuyableAmount,
+    getClickableState,
+    getGridData,
+    gridEffect,
+    hasAchievement,
+    hasChallenge,
+    hasMilestone,
+    hasUpgrade,
+    inChallenge,
+    maxedChallenge,
+    setBuyableAmount,
+    setClickableState,
+    setGridData,
+    upgradeEffect
+} from "./features";
+
+let vue: App;
+export function setVue(vm: App): void {
+    vue = vm;
+}
+
+// Pass in various data that the template could potentially use
+const data = function(): Record<string, unknown> {
+    return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
+};
+export function coerceComponent(
+    component: string | ComponentOptions,
+    defaultWrapper = "span"
+): Component | string {
+    if (typeof component === "string") {
+        component = component.trim();
+        if (!(component in vue._context.components)) {
+            if (component.charAt(0) !== "<") {
+                component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
+            }
+
+            return defineComponent({
+                template: component,
+                data,
+                inject: ["tab"],
+                methods: {
+                    hasUpgrade,
+                    hasMilestone,
+                    hasAchievement,
+                    hasChallenge,
+                    maxedChallenge,
+                    challengeCompletions,
+                    inChallenge,
+                    getBuyableAmount,
+                    setBuyableAmount,
+                    getClickableState,
+                    setClickableState,
+                    getGridData,
+                    setGridData,
+                    upgradeEffect,
+                    challengeEffect,
+                    buyableEffect,
+                    clickableEffect,
+                    achievementEffect,
+                    gridEffect
+                }
+            });
+        }
+    }
+    return component;
+}
+
+export function getFiltered<T>(
+    objects: Record<string, T>,
+    filter?: Array<string>
+): Record<string, T> {
+    if (filter != null) {
+        filter = filter.map(v => v.toString());
+        return Object.keys(objects)
+            .filter(key => filter!.includes(key))
+            .reduce((acc: Record<string, T>, curr: string) => {
+                acc[curr] = objects[curr];
+                return acc;
+            }, {});
+    }
+    return objects;
+}
+
+export function mapState(properties: Array<string> = []): Record<string, unknown> {
+    return properties.reduce((acc: Record<string, unknown>, curr: string): Record<
+        string,
+        unknown
+    > => {
+        acc[curr] = () => player[curr];
+        return acc;
+    }, {});
+}
+
+export const InjectLayerMixin = {
+    props: {
+        layer: {
+            type: String,
+            default(): string {
+                return (inject("tab") as { layer: string }).layer;
+            }
+        }
+    }
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..3c80063
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,43 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": true,
+    "checkJs": false,
+    "jsx": "preserve",
+    "importHelpers": true,
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "experimentalDecorators": true,
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "types": [
+      "webpack-env"
+    ],
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "tests/**/*.ts",
+    "tests/**/*.tsx"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}
diff --git a/vue.config.js b/vue.config.js
index 7d30f61..b707485 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -1,4 +1,11 @@
 module.exports = {
-    publicPath: process.env.NODE_ENV === 'production' ? '/The-Modding-Tree-X' : '/',
-    runtimeCompiler: true
+    publicPath: process.env.NODE_ENV === "production" ? "/The-Modding-Tree-X" : "/",
+    runtimeCompiler: true,
+    chainWebpack(config) {
+        config.resolve.alias.delete("@");
+        config.resolve
+            .plugin("tsconfig-paths")
+            // eslint-disable-next-line @typescript-eslint/no-var-requires
+            .use(require("tsconfig-paths-webpack-plugin"));
+    }
 };