From 8fff8f283545148a8fc93e8f4c67ee1032a2dd22 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 17 Aug 2021 08:06:30 -0500
Subject: [PATCH 01/49] setup basic mod info

---
 saves/safff.txt                  |    1 -
 src/components/system/Tabs.vue   |    8 +-
 src/data/layers/aca/a.ts         |  103 --
 src/data/layers/aca/c.ts         |  573 --------
 src/data/layers/aca/f.ts         |  151 --
 src/data/layers/demo-infinity.ts |  354 -----
 src/data/layers/demo.ts          | 2301 ------------------------------
 src/data/layers/main.ts          |   11 +
 src/data/mod.ts                  |  156 +-
 src/data/modInfo.json            |    8 +-
 10 files changed, 25 insertions(+), 3641 deletions(-)
 delete mode 100644 saves/safff.txt
 delete mode 100644 src/data/layers/aca/a.ts
 delete mode 100644 src/data/layers/aca/c.ts
 delete mode 100644 src/data/layers/aca/f.ts
 delete mode 100644 src/data/layers/demo-infinity.ts
 delete mode 100644 src/data/layers/demo.ts
 create mode 100644 src/data/layers/main.ts

diff --git a/saves/safff.txt b/saves/safff.txt
deleted file mode 100644
index 7a1451d..0000000
--- a/saves/safff.txt
+++ /dev/null
@@ -1 +0,0 @@
-eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9
\ No newline at end of file
diff --git a/src/components/system/Tabs.vue b/src/components/system/Tabs.vue
index 94fc880..ec69f0e 100644
--- a/src/components/system/Tabs.vue
+++ b/src/components/system/Tabs.vue
@@ -14,7 +14,7 @@
                     :layer="tab"
                     :index="index"
                     v-else-if="tab in components"
-                    :minimizable="true"
+                    :minimizable="minimizable[tab]"
                     :tab="() => $refs[`tab-${index}`]"
                 />
                 <component :is="tab" :index="index" v-else />
@@ -47,6 +47,12 @@ export default defineComponent({
                 },
                 {}
             );
+        },
+        minimizable() {
+            return Object.keys(layers).reduce((acc: Record<string, boolean>, curr) => {
+                acc[curr] = layers[curr].minimizable !== false;
+                return acc;
+            }, {});
         }
     }
 });
diff --git a/src/data/layers/aca/a.ts b/src/data/layers/aca/a.ts
deleted file mode 100644
index a18fdab..0000000
--- a/src/data/layers/aca/a.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/* 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.ts b/src/data/layers/aca/c.ts
deleted file mode 100644
index b291172..0000000
--- a/src/data/layers/aca/c.ts
+++ /dev/null
@@ -1,573 +0,0 @@
-/* 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.ts b/src/data/layers/aca/f.ts
deleted file mode 100644
index 95c2bdd..0000000
--- a/src/data/layers/aca/f.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/* 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.ts b/src/data/layers/demo-infinity.ts
deleted file mode 100644
index 6e62446..0000000
--- a/src/data/layers/demo-infinity.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-/* 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.ts b/src/data/layers/demo.ts
deleted file mode 100644
index 329ac6f..0000000
--- a/src/data/layers/demo.ts
+++ /dev/null
@@ -1,2301 +0,0 @@
-/* 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/layers/main.ts b/src/data/layers/main.ts
new file mode 100644
index 0000000..0206502
--- /dev/null
+++ b/src/data/layers/main.ts
@@ -0,0 +1,11 @@
+import { RawLayer } from "@/typings/layer";
+
+export default {
+    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>TODO: Board</div>
+		`,
+    minimizable: false
+} as RawLayer;
diff --git a/src/data/mod.ts b/src/data/mod.ts
index 6dfbc4e..1c3ef96 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -1,97 +1,13 @@
-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;
+import main from "./layers/main";
 
 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];
+): Array<RawLayer> => [main];
 
 export function getStartingData(): Record<string, unknown> {
     return {
@@ -104,73 +20,7 @@ export const hasWon = computed(() => {
 });
 
 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;
+    return new Decimal(0);
 });
 
 /* eslint-disable @typescript-eslint/no-unused-vars */
diff --git a/src/data/modInfo.json b/src/data/modInfo.json
index 8a6d164..42c9ed0 100644
--- a/src/data/modInfo.json
+++ b/src/data/modInfo.json
@@ -1,6 +1,6 @@
 {
-	"title": "The Modding Tree X",
-	"id": "tmt-x",
+	"title": "Side Project",
+	"id": "side-project",
 	"author": "thepaperpilot",
 	"discordName": "The Paper Pilot Community",
 	"discordLink": "https://discord.gg/WzejVAx",
@@ -14,8 +14,8 @@
 	"useHeader": true,
 	"banner": null,
 	"logo": null,
-	"initialTabs": [ "main", "c" ],
+	"initialTabs": [ "main" ],
 
 	"maxTickLength": 3600,
-	"offlineLimit": 1
+	"offlineLimit": 0
 }

From 78ca3713a19bfb600f1e43b05ecbb5bd03cdd219 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 17 Aug 2021 18:39:11 -0500
Subject: [PATCH 02/49] Added type predicate to setDefault

---
 src/game/layers.ts | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/game/layers.ts b/src/game/layers.ts
index 2ce627a..c4c9783 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -96,7 +96,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             RawGridFeatures<GridFeatures<Upgrade>, Upgrade>,
             GridFeatures<Upgrade>,
             Upgrade
-        >(layer.id, layer.upgrades!);
+        >(layer.id, layer.upgrades);
         setRowCol(layer.upgrades);
         for (const id in layer.upgrades.data) {
             layer.upgrades.data[id].bought = function() {
@@ -193,7 +193,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             RawGridFeatures<GridFeatures<Achievement>, Achievement>,
             GridFeatures<Achievement>,
             Achievement
-        >(layer.id, layer.achievements!);
+        >(layer.id, layer.achievements);
         setRowCol(layer.achievements);
         for (const id in layer.achievements.data) {
             layer.achievements.data[id].earned = function() {
@@ -461,8 +461,8 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             );
         };
         setDefault(player, "subtabs", {});
-        setDefault(player.subtabs!, layer.id, {});
-        setDefault(player.subtabs![layer.id], "mainTabs", Object.keys(layer.subtabs)[0]);
+        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;
@@ -471,7 +471,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
     }
     if (layer.microtabs) {
         setDefault(player, "subtabs", {});
-        setDefault(player.subtabs!, layer.id, {});
+        setDefault(player.subtabs, layer.id, {});
         for (const family in layer.microtabs) {
             if (Object.keys(layer.microtabs[family]).length === 0) {
                 console.warn(
@@ -497,7 +497,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
                 return firstUnlocked != undefined ? this[firstUnlocked] : undefined;
             };
             setDefault(
-                player.subtabs![layer.id],
+                player.subtabs[layer.id],
                 family,
                 Object.keys(layer.microtabs[family]).find(tab => tab !== "activeMicrotab")!
             );
@@ -594,7 +594,12 @@ function setupFeatures<T extends RawFeatures<R, S>, R extends Features<S>, S ext
     }
 }
 
-function setDefault<T, K extends keyof T>(object: T, key: K, value: T[K], forceCached?: boolean) {
+function setDefault<T, K extends keyof T>(
+    object: T,
+    key: K,
+    value: T[K],
+    forceCached?: boolean
+): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
     if (object[key] == undefined && value != undefined) {
         object[key] = value;
     }

From 6e1536930e11020281d34310b6fca726cf87968f Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 17 Aug 2021 22:30:49 -0500
Subject: [PATCH 03/49] Made setupFeatures use more accurate types

---
 src/game/layers.ts     | 41 ++++++++++++++++++++---------------------
 src/typings/layer.d.ts |  4 ++--
 2 files changed, 22 insertions(+), 23 deletions(-)

diff --git a/src/game/layers.ts b/src/game/layers.ts
index c4c9783..ccf4292 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -92,11 +92,10 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.upgrades) {
-        setupFeatures<
-            RawGridFeatures<GridFeatures<Upgrade>, Upgrade>,
-            GridFeatures<Upgrade>,
-            Upgrade
-        >(layer.id, layer.upgrades);
+        setupFeatures<NonNullable<RawLayer["upgrades"]>, NonNullable<Layer["upgrades"]>, Upgrade>(
+            layer.id,
+            layer.upgrades
+        );
         setRowCol(layer.upgrades);
         for (const id in layer.upgrades.data) {
             layer.upgrades.data[id].bought = function() {
@@ -190,8 +189,8 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
     }
     if (layer.achievements) {
         setupFeatures<
-            RawGridFeatures<GridFeatures<Achievement>, Achievement>,
-            GridFeatures<Achievement>,
+            NonNullable<RawLayer["achievements"]>,
+            NonNullable<Layer["achievements"]>,
             Achievement
         >(layer.id, layer.achievements);
         setRowCol(layer.achievements);
@@ -209,8 +208,8 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
     }
     if (layer.challenges) {
         setupFeatures<
-            RawGridFeatures<GridFeatures<Challenge>, Challenge>,
-            GridFeatures<Challenge>,
+            NonNullable<RawLayer["challenges"]>,
+            NonNullable<Layer["challenges"]>,
             Challenge
         >(layer.id, layer.challenges);
         setRowCol(layer.challenges);
@@ -301,11 +300,10 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.buyables) {
-        setupFeatures<
-            RawGridFeatures<GridFeatures<Buyable>, Buyable>,
-            GridFeatures<Buyable>,
-            Buyable
-        >(layer.id, layer.buyables);
+        setupFeatures<NonNullable<RawLayer["buyables"]>, NonNullable<Layer["buyables"]>, Buyable>(
+            layer.id,
+            layer.buyables
+        );
         setRowCol(layer.buyables);
         setDefault(layer.buyables, "respec", undefined, false);
         setDefault(
@@ -353,8 +351,8 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
     }
     if (layer.clickables) {
         setupFeatures<
-            RawGridFeatures<GridFeatures<Clickable>, Clickable>,
-            GridFeatures<Clickable>,
+            NonNullable<RawLayer["clickables"]>,
+            NonNullable<Layer["clickables"]>,
             Clickable
         >(layer.id, layer.clickables);
         setRowCol(layer.clickables);
@@ -375,10 +373,11 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.milestones) {
-        setupFeatures<RawFeatures<Features<Milestone>, Milestone>, Features<Milestone>, Milestone>(
-            layer.id,
-            layer.milestones
-        );
+        setupFeatures<
+            NonNullable<RawLayer["milestones"]>,
+            NonNullable<Layer["milestones"]>,
+            Milestone
+        >(layer.id, layer.milestones);
         for (const id in layer.milestones.data) {
             layer.milestones.data[id].earned = function() {
                 return (
@@ -416,7 +415,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.grids) {
-        setupFeatures<RawFeatures<Features<Grid>, Grid>, Features<Grid>, Grid>(
+        setupFeatures<NonNullable<RawLayer["grids"]>, NonNullable<Layer["grids"]>, Grid>(
             layer.id,
             layer.grids
         );
diff --git a/src/typings/layer.d.ts b/src/typings/layer.d.ts
index a03bd14..9059174 100644
--- a/src/typings/layer.d.ts
+++ b/src/typings/layer.d.ts
@@ -37,7 +37,7 @@ export interface RawLayer extends RawFeature<Layer> {
     subtabs?: Record<string, RawFeature<Subtab>>;
     microtabs?: Record<string, RawMicrotabFamily>;
     upgrades?: RawGridFeatures<NonNullable<Layer["upgrades"]>, Upgrade>;
-    startData?: () => Record<string, any>;
+    startData?: () => Record<string, State>;
 }
 
 export interface Layer extends Feature {
@@ -115,7 +115,7 @@ export interface Layer extends Feature {
     activeSubtab?: Subtab | undefined;
     microtabs?: Record<string, MicrotabFamily>;
     upgrades?: GridFeatures<Upgrade>;
-    startData?: () => Record<string, any>;
+    startData?: () => Record<string, State>;
     click?: () => void;
     automate?: () => void;
     reset: (force?: boolean) => void;

From c8651adb6b5d29af7e8ef9f1d58822b5fd143862 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Wed, 18 Aug 2021 00:18:23 -0500
Subject: [PATCH 04/49] cleaned up setupFeatures

---
 src/components/tree/Tree.vue      | 40 ++++++++----------
 src/game/layers.ts                | 67 +++++++++++--------------------
 src/typings/features/feature.d.ts |  6 +--
 3 files changed, 43 insertions(+), 70 deletions(-)

diff --git a/src/components/tree/Tree.vue b/src/components/tree/Tree.vue
index 3a522ef..770135f 100644
--- a/src/components/tree/Tree.vue
+++ b/src/components/tree/Tree.vue
@@ -44,7 +44,7 @@ export default defineComponent({
         };
     },
     props: {
-        nodes: Object as PropType<Record<string, Array<string | number>>>,
+        nodes: Object as PropType<Record<string, Array<string>>>,
         append: Boolean
     },
     inject: ["tab"],
@@ -55,31 +55,25 @@ export default defineComponent({
             }
             return layers[this.modal].name || this.modal;
         },
-        rows(): Record<string | number, Array<string | number>> {
+        rows(): Record<string, Array<string>> {
             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;
-                },
-                {}
-            );
+            const rows = Object.keys(layers).reduce((acc: Record<string, Array<string>>, 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, Array<string>>, curr) => {
+                acc[curr] = rows[curr].filter(layer => layer);
+                return acc;
+            }, {});
         }
     },
     methods: {
diff --git a/src/game/layers.ts b/src/game/layers.ts
index ccf4292..41c688e 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -58,7 +58,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
     layer = clone(layer);
 
     setDefault(player, "layers", {});
-    player.layers![layer.id] = applyPlayerData(
+    player.layers[layer.id] = applyPlayerData(
         {
             points: new Decimal(0),
             unlocked: false,
@@ -74,7 +74,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             confirmRespecBuyables: false,
             ...(layer.startData?.() || {})
         },
-        player.layers![layer.id]
+        player.layers[layer.id]
     );
 
     // Set default property values
@@ -92,10 +92,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.upgrades) {
-        setupFeatures<NonNullable<RawLayer["upgrades"]>, NonNullable<Layer["upgrades"]>, Upgrade>(
-            layer.id,
-            layer.upgrades
-        );
+        setupFeatures<NonNullable<RawLayer["upgrades"]>, Upgrade>(layer.id, layer.upgrades);
         setRowCol(layer.upgrades);
         for (const id in layer.upgrades.data) {
             layer.upgrades.data[id].bought = function() {
@@ -188,11 +185,10 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.achievements) {
-        setupFeatures<
-            NonNullable<RawLayer["achievements"]>,
-            NonNullable<Layer["achievements"]>,
-            Achievement
-        >(layer.id, layer.achievements);
+        setupFeatures<NonNullable<RawLayer["achievements"]>, Achievement>(
+            layer.id,
+            layer.achievements
+        );
         setRowCol(layer.achievements);
         for (const id in layer.achievements.data) {
             layer.achievements.data[id].earned = function() {
@@ -207,11 +203,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.challenges) {
-        setupFeatures<
-            NonNullable<RawLayer["challenges"]>,
-            NonNullable<Layer["challenges"]>,
-            Challenge
-        >(layer.id, layer.challenges);
+        setupFeatures<NonNullable<RawLayer["challenges"]>, Challenge>(layer.id, layer.challenges);
         setRowCol(layer.challenges);
         layer.activeChallenge = function() {
             return Object.values(this.challenges!.data).find(
@@ -300,10 +292,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.buyables) {
-        setupFeatures<NonNullable<RawLayer["buyables"]>, NonNullable<Layer["buyables"]>, Buyable>(
-            layer.id,
-            layer.buyables
-        );
+        setupFeatures<NonNullable<RawLayer["buyables"]>, Buyable>(layer.id, layer.buyables);
         setRowCol(layer.buyables);
         setDefault(layer.buyables, "respec", undefined, false);
         setDefault(
@@ -350,11 +339,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.clickables) {
-        setupFeatures<
-            NonNullable<RawLayer["clickables"]>,
-            NonNullable<Layer["clickables"]>,
-            Clickable
-        >(layer.id, layer.clickables);
+        setupFeatures<NonNullable<RawLayer["clickables"]>, Clickable>(layer.id, layer.clickables);
         setRowCol(layer.clickables);
         setDefault(layer.clickables, "masterButtonClick", undefined, false);
         if (layer.clickables.masterButtonDisplay != undefined) {
@@ -373,11 +358,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.milestones) {
-        setupFeatures<
-            NonNullable<RawLayer["milestones"]>,
-            NonNullable<Layer["milestones"]>,
-            Milestone
-        >(layer.id, layer.milestones);
+        setupFeatures<NonNullable<RawLayer["milestones"]>, Milestone>(layer.id, layer.milestones);
         for (const id in layer.milestones.data) {
             layer.milestones.data[id].earned = function() {
                 return (
@@ -415,12 +396,9 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         }
     }
     if (layer.grids) {
-        setupFeatures<NonNullable<RawLayer["grids"]>, NonNullable<Layer["grids"]>, Grid>(
-            layer.id,
-            layer.grids
-        );
+        setupFeatures<NonNullable<RawLayer["grids"]>, Grid>(layer.id, layer.grids);
         for (const id in layer.grids.data) {
-            setDefault(player.layers![layer.id].grids, id, {});
+            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];
@@ -578,17 +556,18 @@ function setRowCol<T extends GridFeatures<S>, S extends Feature>(features: RawGr
     features.cols = maxCol;
 }
 
-function setupFeatures<T extends RawFeatures<R, S>, R extends Features<S>, S extends Feature>(
-    layer: string,
-    features: T
-) {
+function setupFeatures<
+    T extends RawFeatures<R, S, unknown>,
+    S extends Feature,
+    R extends Features<S> = Features<S>
+>(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;
+        (feature as S).id = id;
+        (feature as S).layer = layer;
+        if ((feature as S).unlocked == undefined) {
+            (feature as S).unlocked = true;
         }
     }
 }
@@ -599,7 +578,7 @@ function setDefault<T, K extends keyof T>(
     value: T[K],
     forceCached?: boolean
 ): asserts object is Exclude<T, K> & Required<Pick<T, K>> {
-    if (object[key] == undefined && value != undefined) {
+    if (object[key] === undefined && value != undefined) {
         object[key] = value;
     }
     if (object[key] != undefined && isFunction(object[key]) && forceCached != undefined) {
diff --git a/src/typings/features/feature.d.ts b/src/typings/features/feature.d.ts
index 4fa74ea..d1eb6a8 100644
--- a/src/typings/features/feature.d.ts
+++ b/src/typings/features/feature.d.ts
@@ -9,16 +9,16 @@ export interface Feature {
     [key: string]: unknown;
 }
 
-export interface RawFeatures<T extends Features<S>, S extends Feature>
+export interface RawFeatures<T extends Features<S>, S extends Feature, R = RawFeature<S>>
     extends Partial<Omit<Computable<T>, "data">>,
         ThisType<T> {
     layer?: string;
-    data: Record<string | number, RawFeature<S>>;
+    data: Record<string, R>;
 }
 
 export interface Features<T extends Feature> {
     layer: string;
-    data: Record<string | number, T>;
+    data: Record<string, T>;
     [key: string]: unknown;
 }
 

From d47ce3525d9895c52966a66c995cb1bb5e9bd1b7 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 19 Aug 2021 00:25:49 -0500
Subject: [PATCH 05/49] Implemented most of the basic features for new Board
 component

---
 package-lock.json                  |  83 ++++++++++++
 package.json                       |   1 +
 src/components/board/Board.vue     |  80 ++++++++++++
 src/components/board/BoardNode.vue | 203 +++++++++++++++++++++++++++++
 src/components/index.ts            |   2 +
 src/game/enums.ts                  |   5 +
 src/game/layers.ts                 |  29 +++++
 src/typings/features/board.d.ts    |  45 +++++++
 src/typings/layer.d.ts             |   4 +
 src/typings/player.d.ts            |  18 +--
 src/typings/theme.d.ts             |  14 +-
 src/util/layers.ts                 |  15 +++
 src/util/proxies.ts                |   1 -
 13 files changed, 490 insertions(+), 10 deletions(-)
 create mode 100644 src/components/board/Board.vue
 create mode 100644 src/components/board/BoardNode.vue
 create mode 100644 src/typings/features/board.d.ts

diff --git a/package-lock.json b/package-lock.json
index 82536ac..b496392 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
         "vue": "^3.2.2",
         "vue-class-component": "^8.0.0-rc.1",
         "vue-next-select": "^2.9.0",
+        "vue-panzoom": "^1.1.6",
         "vue-sortable": "github:Netbel/vue-sortable#master-fix",
         "vue-textarea-autosize": "^1.1.1",
         "vue-transition-expand": "^0.1.0"
@@ -14925,6 +14926,14 @@
       "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
       "dev": true
     },
+    "node_modules/amator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
+      "integrity": "sha1-CMa2C8k67Cthu/wMTWd9MDI8wPE=",
+      "dependencies": {
+        "bezier-easing": "^2.0.3"
+      }
+    },
     "node_modules/ansi-colors": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@@ -15495,6 +15504,11 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "node_modules/bezier-easing": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+      "integrity": "sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY="
+    },
     "node_modules/bfj": {
       "version": "6.1.2",
       "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz",
@@ -22071,6 +22085,11 @@
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
       "dev": true
     },
+    "node_modules/ngraph.events": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz",
+      "integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw=="
+    },
     "node_modules/nice-try": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -22736,6 +22755,16 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
       "dev": true
     },
+    "node_modules/panzoom": {
+      "version": "9.4.2",
+      "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.2.tgz",
+      "integrity": "sha512-sQLr0t6EmNFXpZHag0HQVtOKqF9xjF7iZdgWg3Ss1o7uh2QZLvcrz7S0Cl8M0d2TkPZ69JfPJdknXN3I0e/2aQ==",
+      "dependencies": {
+        "amator": "^1.1.0",
+        "ngraph.events": "^1.2.1",
+        "wheel": "^1.0.0"
+      }
+    },
     "node_modules/parallel-transform": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
@@ -27245,6 +27274,14 @@
         "vue": "^3.0.0"
       }
     },
+    "node_modules/vue-panzoom": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/vue-panzoom/-/vue-panzoom-1.1.6.tgz",
+      "integrity": "sha512-yEE60C/gnc5NGL6YBD++CErD820va7fkBJE5dCWZZzXX2aMGklj/UKmtqu1u5xDkuOIjnGUr412LNHwOOE711w==",
+      "dependencies": {
+        "panzoom": "^9.4.1"
+      }
+    },
     "node_modules/vue-sortable": {
       "version": "0.1.3",
       "resolved": "git+ssh://git@github.com/Netbel/vue-sortable.git#f4d4870ace71ea59bd79252eb2ec1cf6bfb02fe7",
@@ -28083,6 +28120,11 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/wheel": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
+      "integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA=="
+    },
     "node_modules/which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -31140,6 +31182,14 @@
       "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
       "dev": true
     },
+    "amator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
+      "integrity": "sha1-CMa2C8k67Cthu/wMTWd9MDI8wPE=",
+      "requires": {
+        "bezier-easing": "^2.0.3"
+      }
+    },
     "ansi-colors": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
@@ -31576,6 +31626,11 @@
         "tweetnacl": "^0.14.3"
       }
     },
+    "bezier-easing": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+      "integrity": "sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY="
+    },
     "bfj": {
       "version": "6.1.2",
       "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz",
@@ -36803,6 +36858,11 @@
       "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
       "dev": true
     },
+    "ngraph.events": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz",
+      "integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw=="
+    },
     "nice-try": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -37323,6 +37383,16 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
       "dev": true
     },
+    "panzoom": {
+      "version": "9.4.2",
+      "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.2.tgz",
+      "integrity": "sha512-sQLr0t6EmNFXpZHag0HQVtOKqF9xjF7iZdgWg3Ss1o7uh2QZLvcrz7S0Cl8M0d2TkPZ69JfPJdknXN3I0e/2aQ==",
+      "requires": {
+        "amator": "^1.1.0",
+        "ngraph.events": "^1.2.1",
+        "wheel": "^1.0.0"
+      }
+    },
     "parallel-transform": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz",
@@ -41002,6 +41072,14 @@
       "integrity": "sha512-GjX4pHqZXXitquDeSAtLaf85jXdMUOKyCNzo+EF3xRr4DebGwbST4CtmRvL0TX3EhwLHQjUlAc3JcJX+azpLHg==",
       "requires": {}
     },
+    "vue-panzoom": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/vue-panzoom/-/vue-panzoom-1.1.6.tgz",
+      "integrity": "sha512-yEE60C/gnc5NGL6YBD++CErD820va7fkBJE5dCWZZzXX2aMGklj/UKmtqu1u5xDkuOIjnGUr412LNHwOOE711w==",
+      "requires": {
+        "panzoom": "^9.4.1"
+      }
+    },
     "vue-sortable": {
       "version": "git+ssh://git@github.com/Netbel/vue-sortable.git#f4d4870ace71ea59bd79252eb2ec1cf6bfb02fe7",
       "from": "vue-sortable@github:Netbel/vue-sortable#master-fix",
@@ -41687,6 +41765,11 @@
       "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
       "dev": true
     },
+    "wheel": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
+      "integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA=="
+    },
     "which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/package.json b/package.json
index e65641f..d09e8ea 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "vue": "^3.2.2",
     "vue-class-component": "^8.0.0-rc.1",
     "vue-next-select": "^2.9.0",
+    "vue-panzoom": "^1.1.6",
     "vue-sortable": "github:Netbel/vue-sortable#master-fix",
     "vue-textarea-autosize": "^1.1.1",
     "vue-transition-expand": "^0.1.0"
diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
new file mode 100644
index 0000000..f490b60
--- /dev/null
+++ b/src/components/board/Board.vue
@@ -0,0 +1,80 @@
+<template>
+    <panZoom
+        :style="style"
+        selector="#g1"
+        @init="onInit"
+        :options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10 }"
+        ref="stage"
+    >
+        <svg class="stage" width="100%" height="100%">
+            <g id="g1">
+                <BoardNode
+                    v-for="(node, nodeIndex) in nodes"
+                    :key="nodeIndex"
+                    :index="nodeIndex"
+                    :node="node"
+                    :nodeType="board.types[node.type]"
+                />
+            </g>
+        </svg>
+    </panZoom>
+</template>
+
+<script lang="ts">
+import { layers } from "@/game/layers";
+import player from "@/game/player";
+import { Board } from "@/typings/features/board";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: "Board",
+    mixins: [InjectLayerMixin],
+    props: {
+        id: {
+            type: [Number, String],
+            required: true
+        }
+    },
+    provide() {
+        return {
+            getZoomLevel: () => (this.$refs.stage as any).$panZoomInstance.getTransform().scale
+        };
+    },
+    computed: {
+        board(): Board {
+            return layers[this.layer].boards!.data[this.id];
+        },
+        style(): Array<Partial<CSSStyleDeclaration> | undefined> {
+            return [
+                {
+                    width: this.board.width,
+                    height: this.board.height
+                },
+                layers[this.layer].componentStyles?.board,
+                this.board.style
+            ];
+        },
+        nodes() {
+            return player.layers[this.layer].boards[this.id];
+        }
+    },
+    methods: {
+        onInit: function(panzoomInstance) {
+            panzoomInstance.setTransformOrigin(null);
+        }
+    }
+});
+</script>
+
+<style>
+.vue-pan-zoom-scene {
+    width: 100%;
+    height: 100%;
+    cursor: move;
+}
+
+#g1 {
+    transition-duration: 0s;
+}
+</style>
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
new file mode 100644
index 0000000..bec3081
--- /dev/null
+++ b/src/components/board/BoardNode.vue
@@ -0,0 +1,203 @@
+<template>
+    <g
+        class="boardnode"
+        :style="{ opacity: dragging ? 0.5 : 1 }"
+        :transform="`translate(${position.x},${position.y})`"
+        @mousedown="mouseDown"
+    >
+        <circle :r="size + 8" :fill="backgroundColor" stroke="#0F03" :stroke-width="2" />
+
+        <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
+
+        <circle
+            v-if="progressDisplay === ProgressDisplay.Fill"
+            :r="size * progress"
+            :fill="progressColor"
+        />
+        <circle
+            v-else
+            :r="size + 4.5"
+            class="progressRing"
+            fill="transparent"
+            :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
+            :stroke-width="5"
+            :stroke-dashoffset="(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI"
+            :stroke="progressColor"
+        />
+
+        <text :fill="titleColor" class="node-title">{{ title }}</text>
+    </g>
+</template>
+
+<script lang="ts">
+import themes from "@/data/themes";
+import { ProgressDisplay } from "@/game/enums";
+import player from "@/game/player";
+import { BoardNode, NodeType } from "@/typings/features/board";
+import { InjectLayerMixin } from "@/util/vue";
+import { defineComponent, PropType } from "vue";
+
+// TODO will blindly use any T given (can't restrict it to S[R] because I can't figure out how
+//  to make it support narrowing the return type)
+function getTypeProperty<T, S extends NodeType, R extends keyof S>(
+    nodeType: S,
+    node: BoardNode,
+    property: R
+): S[R] extends Pick<
+    S,
+    {
+        [K in keyof S]-?: undefined extends S[K] ? never : K;
+    }[keyof S]
+>
+    ? T
+    : T | undefined {
+    return typeof nodeType[property] === "function"
+        ? (nodeType[property] as (node: BoardNode) => T)(node)
+        : (nodeType[property] as T);
+}
+
+export default defineComponent({
+    name: "BoardNode",
+    mixins: [InjectLayerMixin],
+    inject: ["getZoomLevel"],
+    data() {
+        return {
+            ProgressDisplay,
+            lastMousePosition: { x: 0, y: 0 },
+            dragged: { x: 0, y: 0 },
+            dragging: false
+        };
+    },
+    props: {
+        index: {
+            type: Number,
+            required: true
+        },
+        node: {
+            type: Object as PropType<BoardNode>,
+            required: true
+        },
+        nodeType: {
+            type: Object as PropType<NodeType>,
+            required: true
+        }
+    },
+    computed: {
+        draggable(): boolean {
+            return getTypeProperty(this.nodeType, this.node, "draggable");
+        },
+        position(): { x: number; y: number } {
+            return this.draggable && this.dragging
+                ? {
+                      x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
+                      y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
+                  }
+                : this.node.position;
+        },
+        size(): number {
+            return getTypeProperty(this.nodeType, this.node, "size");
+        },
+        title(): string {
+            return getTypeProperty(this.nodeType, this.node, "title");
+        },
+        progress(): number {
+            return getTypeProperty(this.nodeType, this.node, "progress") || 0;
+        },
+        backgroundColor(): string {
+            return themes[player.theme].variables["--background"];
+        },
+        outlineColor(): string {
+            return (
+                getTypeProperty(this.nodeType, this.node, "outlineColor") ||
+                themes[player.theme].variables["--separator"]
+            );
+        },
+        fillColor(): string {
+            return (
+                getTypeProperty(this.nodeType, this.node, "fillColor") ||
+                themes[player.theme].variables["--secondary-background"]
+            );
+        },
+        progressColor(): string {
+            return getTypeProperty(this.nodeType, this.node, "progressColor") || "none";
+        },
+        titleColor(): string {
+            return (
+                getTypeProperty(this.nodeType, this.node, "titleColor") ||
+                themes[player.theme].variables["--color"]
+            );
+        },
+        progressDisplay(): ProgressDisplay {
+            return (
+                getTypeProperty(this.nodeType, this.node, "progressDisplay") ||
+                ProgressDisplay.Outline
+            );
+        }
+    },
+    methods: {
+        mouseDown(e: MouseEvent) {
+            if (this.draggable) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                this.lastMousePosition = {
+                    x: e.clientX,
+                    y: e.clientY
+                };
+                this.dragged = { x: 0, y: 0 };
+
+                this.dragging = true;
+                document.onmouseup = this.mouseUp;
+                document.onmousemove = this.mouseMove;
+            }
+        },
+        mouseMove(e: MouseEvent) {
+            if (this.draggable && this.dragging) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                const zoom = (this.getZoomLevel as () => number)();
+                console.log(zoom);
+                this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
+                this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
+                this.lastMousePosition = {
+                    x: e.clientX,
+                    y: e.clientY
+                };
+            }
+        },
+        mouseUp(e: MouseEvent) {
+            if (this.draggable && this.dragging) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                let node = player.layers[this.nodeType.layer].boards[this.nodeType.id][this.index];
+                node.position.x += Math.round(this.dragged.x / 25) * 25;
+                node.position.y += Math.round(this.dragged.y / 25) * 25;
+
+                this.dragging = false;
+                document.onmouseup = null;
+                document.onmousemove = null;
+            }
+        }
+    }
+});
+</script>
+
+<style scoped>
+.boardnode {
+    cursor: pointer;
+    transition-duration: 0s;
+}
+
+.node-title {
+    text-anchor: middle;
+    dominant-baseline: middle;
+    font-family: monospace;
+    font-size: 200%;
+}
+
+.progressRing {
+    transform: rotate(-90deg);
+}
+</style>
diff --git a/src/components/index.ts b/src/components/index.ts
index 9619597..408338f 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -6,6 +6,7 @@ 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 panZoom from "vue-panzoom";
 import { App } from "vue";
 
 export function registerComponents(vue: App): void {
@@ -23,4 +24,5 @@ export function registerComponents(vue: App): void {
     vue.use(VueTextareaAutosize);
     vue.use(Sortable);
     vue.component("vue-select", VueNextSelect);
+    vue.use(panZoom);
 }
diff --git a/src/game/enums.ts b/src/game/enums.ts
index f175079..a9047d3 100644
--- a/src/game/enums.ts
+++ b/src/game/enums.ts
@@ -28,3 +28,8 @@ export enum ImportingStatus {
     WrongID = "WRONG_ID",
     Force = "FORCE"
 }
+
+export enum ProgressDisplay {
+    Outline = "Outline",
+    Fill = "Fill"
+}
diff --git a/src/game/layers.ts b/src/game/layers.ts
index 41c688e..d20d094 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -1,5 +1,6 @@
 import { CacheableFunction } from "@/typings/cacheableFunction";
 import { Achievement } from "@/typings/features/achievement";
+import { Board } from "@/typings/features/board";
 import { Buyable } from "@/typings/features/buyable";
 import { Challenge } from "@/typings/features/challenge";
 import { Clickable } from "@/typings/features/clickable";
@@ -23,6 +24,7 @@ import Decimal, { DecimalSource } from "@/util/bignum";
 import { isFunction } from "@/util/common";
 import {
     defaultLayerProperties,
+    getStartingBoards,
     getStartingBuyables,
     getStartingChallenges,
     getStartingClickables,
@@ -32,6 +34,7 @@ import { createGridProxy, createLayerProxy } from "@/util/proxies";
 import { applyPlayerData } from "@/util/save";
 import clone from "lodash.clonedeep";
 import { isRef } from "vue";
+import { ProgressDisplay } from "./enums";
 import { default as playerProxy } from "./player";
 
 export const layers: Record<string, Readonly<Layer>> = {};
@@ -70,6 +73,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             buyables: getStartingBuyables(layer.buyables?.data),
             clickables: getStartingClickables(layer.clickables?.data),
             challenges: getStartingChallenges(layer.challenges?.data),
+            boards: getStartingBoards(layer.boards?.data),
             grids: {},
             confirmRespecBuyables: false,
             ...(layer.startData?.() || {})
@@ -423,6 +427,31 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             layer.grids.data[id] = createGridProxy(layer.grids.data[id]) as Grid;
         }
     }
+    if (layer.boards) {
+        setupFeatures<NonNullable<RawLayer["boards"]>, Board>(layer.id, layer.boards);
+        for (const id in layer.boards.data) {
+            setDefault(layer.boards.data[id], "width", "100%");
+            setDefault(layer.boards.data[id], "height", "400px");
+            for (const nodeType in layer.boards.data[id].types) {
+                layer.boards.data[id].types[nodeType].layer = layer.id;
+                layer.boards.data[id].types[nodeType].id = id;
+                layer.boards.data[id].types[nodeType].type = nodeType;
+                setDefault(layer.boards.data[id].types[nodeType], "size", 50);
+                setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
+                setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
+                setDefault(
+                    layer.boards.data[id].types[nodeType],
+                    "progressDisplay",
+                    ProgressDisplay.Fill
+                );
+                setDefault(layer.boards.data[id].types[nodeType], "nodes", function() {
+                    return playerProxy.layers[this.layer].boards[this.id].filter(
+                        node => node.type === this.type
+                    );
+                });
+            }
+        }
+    }
     if (layer.subtabs) {
         layer.activeSubtab = function() {
             if (
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
new file mode 100644
index 0000000..520411e
--- /dev/null
+++ b/src/typings/features/board.d.ts
@@ -0,0 +1,45 @@
+import { State } from "../state";
+import { Feature, RawFeature } from "./feature";
+
+export interface BoardNode {
+    position: {
+        x: number;
+        y: number;
+    };
+    type: string;
+    data?: State;
+}
+
+export interface CardOption {
+    text: string;
+    selected: (node: BoardNode) => void;
+}
+
+export interface Board extends Feature {
+    startNodes: () => BoardNode[];
+    style?: Partial<CSSStyleDeclaration>;
+    height: string;
+    width: string;
+    types: Record<string, NodeType>;
+}
+
+export type RawBoard = Omit<RawFeature<Board>, "types"> & {
+    startNodes: () => BoardNode[];
+    types: Record<string, RawFeature<NodeType>>;
+};
+
+export interface NodeType extends Feature {
+    tooltip?: string | ((node: BoardNode) => string);
+    title: string | ((node: BoardNode) => string);
+    size: number | ((node: BoardNode) => number);
+    draggable: boolean | ((node: BoardNode) => boolean);
+    canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean);
+    progress?: number | ((node: BoardNode) => number);
+    progressDisplay: ProgressDisplay | ((node: BoardNode) => ProgressDisplay);
+    progressColor: string | ((node: BoardNode) => string);
+    fillColor?: string | ((node: BoardNode) => string);
+    outlineColor?: string | ((node: BoardNode) => string);
+    titleColor?: string | ((node: BoardNode) => string);
+    onClick: (node: BoardNode) => void;
+    nodes: BoardNode[];
+}
diff --git a/src/typings/layer.d.ts b/src/typings/layer.d.ts
index 9059174..9dfd56a 100644
--- a/src/typings/layer.d.ts
+++ b/src/typings/layer.d.ts
@@ -3,6 +3,7 @@ import Decimal, { DecimalSource } from "@/util/bignum";
 import { CoercableComponent } from "./component";
 import { Achievement } from "./features/achievement";
 import { Bar } from "./features/bar";
+import { Board, RawBoard } from "./features/board";
 import { Buyable } from "./features/buyable";
 import { Challenge } from "./features/challenge";
 import { Clickable } from "./features/clickable";
@@ -31,6 +32,7 @@ export interface RawLayer extends RawFeature<Layer> {
     challenges?: RawGridFeatures<NonNullable<Layer["challenges"]>, Challenge>;
     clickables?: RawGridFeatures<NonNullable<Layer["clickables"]>, Clickable>;
     grids?: RawFeatures<NonNullable<Layer["grids"]>, Grid>;
+    boards?: RawFeatures<NonNullable<Layer["boards"]>, Board, RawBoard>;
     hotkeys?: RawFeature<Hotkey>[];
     infoboxes?: RawFeatures<NonNullable<Layer["infoboxes"]>, Infoboxe>;
     milestones?: RawFeatures<NonNullable<Layer["milestones"]>, Milestone>;
@@ -108,6 +110,7 @@ export interface Layer extends Feature {
         showMasterButton?: boolean;
     };
     grids?: Features<Grid>;
+    boards?: Features<Board>;
     hotkeys?: Hotkey[];
     infoboxes?: Features<Infobox>;
     milestones?: Features<Milestone>;
@@ -142,5 +145,6 @@ export interface ComponentStyles {
     "prestige-button"?: Partial<CSSStyleDeclaration>;
     "respec-button"?: Partial<CSSStyleDeclaration>;
     upgrade?: Partial<CSSStyleDeclaration>;
+    board?: Partial<CSSStyleDeclaration>;
     "tab-button"?: Partial<CSSStyleDeclaration>;
 }
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
index f45c2cc..b75d2f3 100644
--- a/src/typings/player.d.ts
+++ b/src/typings/player.d.ts
@@ -1,6 +1,7 @@
 import { Themes } from "@/data/themes";
 import { DecimalSource } from "@/lib/break_eternity";
 import Decimal from "@/util/bignum";
+import { BoardNode } from "./features/board";
 import { MilestoneDisplay } from "./features/milestone";
 import { State } from "./state";
 
@@ -53,14 +54,15 @@ export interface LayerSaveData {
     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>>;
+    upgrades: Array<string>;
+    achievements: Array<string>;
+    milestones: Array<string>;
+    infoboxes: Record<string, boolean>;
+    buyables: Record<string, Decimal>;
+    clickables: Record<string, State>;
+    challenges: Record<string, Decimal>;
+    grids: Record<string, Record<string, State>>;
+    boards: Record<string, Array<BoardNode>>;
     confirmRespecBuyables: boolean;
     [index: string]: unknown;
 }
diff --git a/src/typings/theme.d.ts b/src/typings/theme.d.ts
index d05ddbb..b3236e4 100644
--- a/src/typings/theme.d.ts
+++ b/src/typings/theme.d.ts
@@ -1,6 +1,18 @@
 export interface Theme {
     variables: {
-        [index: string]: string;
+        "--background": string;
+        "--background-tooltip": string;
+        "--secondary-background": string;
+        "--color": string;
+        "--points": string;
+        "--locked": string;
+        "--bought": string;
+        "--link": string;
+        "--separator": string;
+        "--border-radius": string;
+        "--danger": string;
+        "--modal-border": string;
+        "--feature-margin": string;
     };
     stackedInfoboxes: boolean;
     floatingTabs: boolean;
diff --git a/src/util/layers.ts b/src/util/layers.ts
index 5bbb2e2..519ca43 100644
--- a/src/util/layers.ts
+++ b/src/util/layers.ts
@@ -1,6 +1,7 @@
 import { hotkeys, layers } from "@/game/layers";
 import player from "@/game/player";
 import { CacheableFunction } from "@/typings/cacheableFunction";
+import { Board, BoardNode, RawBoard } from "@/typings/features/board";
 import { Buyable } from "@/typings/features/buyable";
 import { Challenge } from "@/typings/features/challenge";
 import { Clickable } from "@/typings/features/clickable";
@@ -70,6 +71,20 @@ export function getStartingChallenges(
         : {};
 }
 
+export function getStartingBoards(
+    boards?: Record<string, Board> | Record<string, RawBoard> | undefined
+): Record<string, Array<BoardNode>> {
+    return boards
+        ? Object.keys(boards).reduce((acc: Record<string, Array<BoardNode>>, curr: string): Record<
+              string,
+              Array<BoardNode>
+          > => {
+              acc[curr] = boards[curr].startNodes?.() || [];
+              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> => {
diff --git a/src/util/proxies.ts b/src/util/proxies.ts
index 57fa533..83c1c58 100644
--- a/src/util/proxies.ts
+++ b/src/util/proxies.ts
@@ -13,7 +13,6 @@ export function createLayerProxy(object: Record<string, any>): Record<string, an
     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(

From f3b934337f94d94a01f4307df38596967943b9fc Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 19 Aug 2021 00:26:34 -0500
Subject: [PATCH 06/49] Added board with basic time resource node

---
 src/data/layers/main.ts | 69 ++++++++++++++++++++++++++++++++++++++---
 1 file changed, 65 insertions(+), 4 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 0206502..f6a2046 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -1,11 +1,72 @@
+import player from "@/game/player";
+import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
+import themes from "../themes";
+
+type ResourceNodeData = {
+    resourceType: string;
+    amount: DecimalSource;
+    maxAmount: DecimalSource;
+};
 
 export default {
     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.devSpeed === 0">Game Paused</div>
+        <div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
         <div>TODO: Board</div>
-		`,
-    minimizable: false
+        <Board id="main" />
+    `,
+    minimizable: false,
+    componentStyles: {
+        board: {
+            position: "absolute",
+            top: "0",
+            left: "0"
+        }
+    },
+    boards: {
+        data: {
+            main: {
+                height: "100%",
+                startNodes() {
+                    return [
+                        {
+                            position: { x: 0, y: 0 },
+                            type: "resource",
+                            data: {
+                                resourceType: "time",
+                                amount: new Decimal(24 * 60 * 60),
+                                maxAmount: new Decimal(24 * 60 * 60)
+                            }
+                        }
+                    ];
+                },
+                types: {
+                    resource: {
+                        title(node) {
+                            return (node.data as ResourceNodeData).resourceType;
+                        },
+                        draggable: true,
+                        progress(node) {
+                            const data = node.data as ResourceNodeData;
+                            return Decimal.div(data.amount, data.maxAmount).toNumber();
+                        },
+                        fillColor() {
+                            return themes[player.theme].variables["--background"];
+                        },
+                        progressColor(node) {
+                            const data = node.data as ResourceNodeData;
+                            switch (data.resourceType) {
+                                case "time":
+                                    return "#0FF3";
+                                default:
+                                    return "none";
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 } as RawLayer;

From 02443bbb0ce4aa223d342992a1d9c6a4348bd760 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 00:47:56 -0500
Subject: [PATCH 07/49] Implemented dropping nodes into each other Also
 improved node z-ordering and other related things

---
 src/components/board/Board.vue     | 133 ++++++++++++++++++++++++---
 src/components/board/BoardNode.vue | 140 ++++++++++++-----------------
 src/data/layers/main.ts            |  28 ++++++
 src/game/player.ts                 |   6 ++
 src/typings/features/board.d.ts    |   8 +-
 src/typings/player.d.ts            |   2 +-
 src/util/features.ts               |  20 +++++
 src/util/layers.ts                 |  12 ++-
 8 files changed, 249 insertions(+), 100 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index f490b60..21e966d 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -2,18 +2,35 @@
     <panZoom
         :style="style"
         selector="#g1"
-        @init="onInit"
         :options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10 }"
         ref="stage"
+        @init="onInit"
+        @mousemove="drag"
+        @mouseup="() => endDragging(dragging)"
+        @mouseleave="() => endDragging(dragging)"
     >
         <svg class="stage" width="100%" height="100%">
             <g id="g1">
                 <BoardNode
-                    v-for="(node, nodeIndex) in nodes"
-                    :key="nodeIndex"
-                    :index="nodeIndex"
+                    v-for="node in nodes"
+                    :key="node.id"
                     :node="node"
                     :nodeType="board.types[node.type]"
+                    :dragging="draggingNode"
+                    :dragged="dragged"
+                    :receivingNode="receivingNode?.id === node.id"
+                    @startDragging="startDragging"
+                    @endDragging="endDragging"
+                />
+                <BoardNode
+                    v-if="draggingNode"
+                    :node="draggingNode"
+                    :nodeType="board.types[draggingNode.type]"
+                    :dragging="draggingNode"
+                    :dragged="dragged"
+                    :receivingNode="receivingNode?.id === draggingNode.id"
+                    @startDragging="startDragging"
+                    @endDragging="endDragging"
                 />
             </g>
         </svg>
@@ -23,24 +40,30 @@
 <script lang="ts">
 import { layers } from "@/game/layers";
 import player from "@/game/player";
-import { Board } from "@/typings/features/board";
+import { Board, BoardNode } from "@/typings/features/board";
 import { InjectLayerMixin } from "@/util/vue";
 import { defineComponent } from "vue";
 
 export default defineComponent({
     name: "Board",
     mixins: [InjectLayerMixin],
+    data() {
+        return {
+            lastMousePosition: { x: 0, y: 0 },
+            dragged: { x: 0, y: 0 },
+            dragging: null
+        } as {
+            lastMousePosition: { x: number; y: number };
+            dragged: { x: number; y: number };
+            dragging: string | null;
+        };
+    },
     props: {
         id: {
             type: [Number, String],
             required: true
         }
     },
-    provide() {
-        return {
-            getZoomLevel: () => (this.$refs.stage as any).$panZoomInstance.getTransform().scale
-        };
-    },
     computed: {
         board(): Board {
             return layers[this.layer].boards!.data[this.id];
@@ -55,13 +78,101 @@ export default defineComponent({
                 this.board.style
             ];
         },
+        draggingNode() {
+            return this.dragging
+                ? player.layers[this.layer].boards[this.id].find(node => node.id === this.dragging)
+                : null;
+        },
         nodes() {
-            return player.layers[this.layer].boards[this.id];
+            return player.layers[this.layer].boards[this.id].filter(
+                node => node !== this.draggingNode
+            );
+        },
+        receivingNode(): BoardNode | null {
+            if (this.draggingNode == null) {
+                return null;
+            }
+
+            const position = {
+                x: this.draggingNode.position.x + this.dragged.x,
+                y: this.draggingNode.position.y + this.dragged.y
+            };
+            let smallestDistance = Number.MAX_VALUE;
+            return this.nodes.reduce((smallest: BoardNode | null, curr: BoardNode) => {
+                const nodeType = this.board.types[curr.type];
+                const canAccept =
+                    typeof nodeType.canAccept === "boolean"
+                        ? nodeType.canAccept
+                        : nodeType.canAccept(curr, this.draggingNode!);
+                if (!canAccept) {
+                    return smallest;
+                }
+
+                const distanceSquared =
+                    Math.pow(position.x - curr.position.x, 2) +
+                    Math.pow(position.y - curr.position.y, 2);
+                const size =
+                    typeof nodeType.size === "number" ? nodeType.size : nodeType.size(curr);
+                if (distanceSquared > smallestDistance || distanceSquared > size * size) {
+                    return smallest;
+                }
+
+                smallestDistance = distanceSquared;
+                return curr;
+            }, null);
         }
     },
     methods: {
+        getZoomLevel(): number {
+            return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
+        },
         onInit: function(panzoomInstance) {
             panzoomInstance.setTransformOrigin(null);
+        },
+        startDragging(e: MouseEvent, nodeID: string) {
+            if (this.dragging == null) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                this.lastMousePosition = {
+                    x: e.clientX,
+                    y: e.clientY
+                };
+                this.dragged = { x: 0, y: 0 };
+
+                this.dragging = nodeID;
+            }
+        },
+        drag(e: MouseEvent) {
+            if (this.dragging) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                const zoom = (this.getZoomLevel as () => number)();
+                this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
+                this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
+                this.lastMousePosition = {
+                    x: e.clientX,
+                    y: e.clientY
+                };
+            }
+        },
+        endDragging(nodeID: string | null) {
+            if (this.dragging != null && this.dragging === nodeID) {
+                const nodes = player.layers[this.layer].boards[this.id];
+                const draggingNode = this.draggingNode!;
+                const receivingNode = this.receivingNode;
+                draggingNode.position.x += Math.round(this.dragged.x / 25) * 25;
+                draggingNode.position.y += Math.round(this.dragged.y / 25) * 25;
+                nodes.splice(nodes.indexOf(draggingNode), 1);
+                nodes.push(draggingNode);
+
+                if (receivingNode) {
+                    this.board.types[receivingNode.type].onDrop(receivingNode, draggingNode);
+                }
+
+                this.dragging = null;
+            }
         }
     }
 });
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index bec3081..ac3d2a6 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -1,11 +1,19 @@
 <template>
     <g
         class="boardnode"
-        :style="{ opacity: dragging ? 0.5 : 1 }"
+        :style="{ opacity: dragging?.id === node.id ? 0.5 : 1 }"
         :transform="`translate(${position.x},${position.y})`"
-        @mousedown="mouseDown"
+        @mouseenter="mouseEnter"
+        @mouseleave="mouseLeave"
+        @mousedown="e => $emit('startDragging', e, node.id)"
     >
-        <circle :r="size + 8" :fill="backgroundColor" stroke="#0F03" :stroke-width="2" />
+        <circle
+            v-if="canAccept"
+            :r="size + 8"
+            :fill="backgroundColor"
+            :stroke="receivingNode ? '#0F0' : '#0F03'"
+            :stroke-width="2"
+        />
 
         <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
 
@@ -34,45 +42,22 @@ import themes from "@/data/themes";
 import { ProgressDisplay } from "@/game/enums";
 import player from "@/game/player";
 import { BoardNode, NodeType } from "@/typings/features/board";
+import { getNodeTypeProperty } from "@/util/features";
 import { InjectLayerMixin } from "@/util/vue";
 import { defineComponent, PropType } from "vue";
 
-// TODO will blindly use any T given (can't restrict it to S[R] because I can't figure out how
-//  to make it support narrowing the return type)
-function getTypeProperty<T, S extends NodeType, R extends keyof S>(
-    nodeType: S,
-    node: BoardNode,
-    property: R
-): S[R] extends Pick<
-    S,
-    {
-        [K in keyof S]-?: undefined extends S[K] ? never : K;
-    }[keyof S]
->
-    ? T
-    : T | undefined {
-    return typeof nodeType[property] === "function"
-        ? (nodeType[property] as (node: BoardNode) => T)(node)
-        : (nodeType[property] as T);
-}
-
 export default defineComponent({
     name: "BoardNode",
     mixins: [InjectLayerMixin],
-    inject: ["getZoomLevel"],
     data() {
         return {
             ProgressDisplay,
             lastMousePosition: { x: 0, y: 0 },
-            dragged: { x: 0, y: 0 },
-            dragging: false
+            hovering: false
         };
     },
+    emits: ["startDragging", "endDragging"],
     props: {
-        index: {
-            type: Number,
-            required: true
-        },
         node: {
             type: Object as PropType<BoardNode>,
             required: true
@@ -80,14 +65,25 @@ export default defineComponent({
         nodeType: {
             type: Object as PropType<NodeType>,
             required: true
+        },
+        dragging: {
+            type: Object as PropType<BoardNode>
+        },
+        dragged: {
+            type: Object as PropType<{ x: number; y: number }>,
+            required: true
+        },
+        receivingNode: {
+            type: Boolean,
+            default: false
         }
     },
     computed: {
         draggable(): boolean {
-            return getTypeProperty(this.nodeType, this.node, "draggable");
+            return getNodeTypeProperty(this.nodeType, this.node, "draggable");
         },
         position(): { x: number; y: number } {
-            return this.draggable && this.dragging
+            return this.draggable && this.dragging?.id === this.node.id
                 ? {
                       x: this.node.position.x + Math.round(this.dragged.x / 25) * 25,
                       y: this.node.position.y + Math.round(this.dragged.y / 25) * 25
@@ -95,89 +91,71 @@ export default defineComponent({
                 : this.node.position;
         },
         size(): number {
-            return getTypeProperty(this.nodeType, this.node, "size");
+            let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
+            if (this.receivingNode) {
+                size *= 1.25;
+            } else if (this.hovering) {
+                size *= 1.15;
+            }
+            return size;
         },
         title(): string {
-            return getTypeProperty(this.nodeType, this.node, "title");
+            return getNodeTypeProperty(this.nodeType, this.node, "title");
         },
         progress(): number {
-            return getTypeProperty(this.nodeType, this.node, "progress") || 0;
+            return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
         },
         backgroundColor(): string {
             return themes[player.theme].variables["--background"];
         },
         outlineColor(): string {
             return (
-                getTypeProperty(this.nodeType, this.node, "outlineColor") ||
+                getNodeTypeProperty(this.nodeType, this.node, "outlineColor") ||
                 themes[player.theme].variables["--separator"]
             );
         },
         fillColor(): string {
             return (
-                getTypeProperty(this.nodeType, this.node, "fillColor") ||
+                getNodeTypeProperty(this.nodeType, this.node, "fillColor") ||
                 themes[player.theme].variables["--secondary-background"]
             );
         },
         progressColor(): string {
-            return getTypeProperty(this.nodeType, this.node, "progressColor") || "none";
+            return getNodeTypeProperty(this.nodeType, this.node, "progressColor") || "none";
         },
         titleColor(): string {
             return (
-                getTypeProperty(this.nodeType, this.node, "titleColor") ||
+                getNodeTypeProperty(this.nodeType, this.node, "titleColor") ||
                 themes[player.theme].variables["--color"]
             );
         },
         progressDisplay(): ProgressDisplay {
             return (
-                getTypeProperty(this.nodeType, this.node, "progressDisplay") ||
+                getNodeTypeProperty(this.nodeType, this.node, "progressDisplay") ||
                 ProgressDisplay.Outline
             );
+        },
+        canAccept(): boolean {
+            if (this.dragging == null) {
+                return false;
+            }
+            return typeof this.nodeType.canAccept === "boolean"
+                ? this.nodeType.canAccept
+                : this.nodeType.canAccept(this.node, this.dragging);
         }
     },
     methods: {
-        mouseDown(e: MouseEvent) {
-            if (this.draggable) {
-                e.preventDefault();
-                e.stopPropagation();
-
-                this.lastMousePosition = {
-                    x: e.clientX,
-                    y: e.clientY
-                };
-                this.dragged = { x: 0, y: 0 };
-
-                this.dragging = true;
-                document.onmouseup = this.mouseUp;
-                document.onmousemove = this.mouseMove;
-            }
+        mouseEnter() {
+            this.hovering = true;
         },
-        mouseMove(e: MouseEvent) {
-            if (this.draggable && this.dragging) {
-                e.preventDefault();
-                e.stopPropagation();
-
-                const zoom = (this.getZoomLevel as () => number)();
-                console.log(zoom);
-                this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
-                this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
-                this.lastMousePosition = {
-                    x: e.clientX,
-                    y: e.clientY
-                };
-            }
-        },
-        mouseUp(e: MouseEvent) {
-            if (this.draggable && this.dragging) {
-                e.preventDefault();
-                e.stopPropagation();
-
-                let node = player.layers[this.nodeType.layer].boards[this.nodeType.id][this.index];
-                node.position.x += Math.round(this.dragged.x / 25) * 25;
-                node.position.y += Math.round(this.dragged.y / 25) * 25;
-
-                this.dragging = false;
-                document.onmouseup = null;
-                document.onmousemove = null;
+        mouseLeave() {
+            this.hovering = false;
+        }
+    },
+    watch: {
+        onDraggableChanged() {
+            if (this.dragging && !this.draggable) {
+                this.$emit("endDragging", this.node.id);
             }
         }
     }
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index f6a2046..f16b1d4 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -9,6 +9,11 @@ type ResourceNodeData = {
     maxAmount: DecimalSource;
 };
 
+type ItemNodeData = {
+    itemType: string;
+    amount: DecimalSource;
+};
+
 export default {
     id: "main",
     display: `
@@ -39,6 +44,14 @@ export default {
                                 amount: new Decimal(24 * 60 * 60),
                                 maxAmount: new Decimal(24 * 60 * 60)
                             }
+                        },
+                        {
+                            position: { x: 0, y: 150 },
+                            type: "item",
+                            data: {
+                                itemType: "speed",
+                                amount: new Decimal(5 * 60 * 60)
+                            }
                         }
                     ];
                 },
@@ -63,7 +76,22 @@ export default {
                                 default:
                                     return "none";
                             }
+                        },
+                        canAccept(node, otherNode) {
+                            return otherNode.type === "item";
+                        },
+                        onDrop(node, otherNode) {
+                            const index = player.layers[this.layer].boards[this.id].indexOf(
+                                otherNode
+                            );
+                            player.layers[this.layer].boards[this.id].splice(index, 1);
                         }
+                    },
+                    item: {
+                        title(node) {
+                            return (node.data as ItemNodeData).itemType;
+                        },
+                        draggable: true
                     }
                 }
             }
diff --git a/src/game/player.ts b/src/game/player.ts
index c1f0e2a..aabeb10 100644
--- a/src/game/player.ts
+++ b/src/game/player.ts
@@ -104,6 +104,12 @@ const playerHandler: ProxyHandler<Record<string, any>> = {
             }
         }
         return true;
+    },
+    ownKeys(target: Record<string, any>) {
+        return Reflect.ownKeys(target.__state);
+    },
+    has(target: Record<string, any>, key: string) {
+        return Reflect.has(target.__state, key);
     }
 };
 export default window.player = new Proxy(
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 520411e..6346516 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -2,6 +2,7 @@ import { State } from "../state";
 import { Feature, RawFeature } from "./feature";
 
 export interface BoardNode {
+    id: string;
     position: {
         x: number;
         y: number;
@@ -16,15 +17,15 @@ export interface CardOption {
 }
 
 export interface Board extends Feature {
-    startNodes: () => BoardNode[];
+    startNodes: () => Omit<BoardNode, "id">[];
     style?: Partial<CSSStyleDeclaration>;
     height: string;
     width: string;
     types: Record<string, NodeType>;
 }
 
-export type RawBoard = Omit<RawFeature<Board>, "types"> & {
-    startNodes: () => BoardNode[];
+export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
+    startNodes: () => Omit<BoardNode, "id">[];
     types: Record<string, RawFeature<NodeType>>;
 };
 
@@ -41,5 +42,6 @@ export interface NodeType extends Feature {
     outlineColor?: string | ((node: BoardNode) => string);
     titleColor?: string | ((node: BoardNode) => string);
     onClick: (node: BoardNode) => void;
+    onDrop: (node: BoardNode, otherNode: BoardNode) => void;
     nodes: BoardNode[];
 }
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
index b75d2f3..dc54d03 100644
--- a/src/typings/player.d.ts
+++ b/src/typings/player.d.ts
@@ -62,7 +62,7 @@ export interface LayerSaveData {
     clickables: Record<string, State>;
     challenges: Record<string, Decimal>;
     grids: Record<string, Record<string, State>>;
-    boards: Record<string, Array<BoardNode>>;
+    boards: Record<string, BoardNode[]>;
     confirmRespecBuyables: boolean;
     [index: string]: unknown;
 }
diff --git a/src/util/features.ts b/src/util/features.ts
index 8e31c71..40a6809 100644
--- a/src/util/features.ts
+++ b/src/util/features.ts
@@ -1,4 +1,5 @@
 import { layers } from "@/game/layers";
+import { NodeType, BoardNode } from "@/typings/features/board";
 import { GridCell } from "@/typings/features/grid";
 import { State } from "@/typings/state";
 import Decimal, { DecimalSource } from "@/util/bignum";
@@ -87,3 +88,22 @@ export function achievementEffect(layer: string, id: string | number): State | u
 export function gridEffect(layer: string, id: string, cell: string | number): State | undefined {
     return (layers[layer].grids?.data[id][cell] as GridCell).effect;
 }
+
+// TODO will blindly use any T given (can't restrict it to S[R] because I can't figure out how
+//  to make it support narrowing the return type)
+export function getNodeTypeProperty<T, S extends NodeType, R extends keyof S>(
+    nodeType: S,
+    node: BoardNode,
+    property: R
+): S[R] extends Pick<
+    S,
+    {
+        [K in keyof S]-?: undefined extends S[K] ? never : K;
+    }[keyof S]
+>
+    ? T
+    : T | undefined {
+    return typeof nodeType[property] === "function"
+        ? (nodeType[property] as (node: BoardNode) => T)(node)
+        : (nodeType[property] as T);
+}
diff --git a/src/util/layers.ts b/src/util/layers.ts
index 519ca43..d9be21b 100644
--- a/src/util/layers.ts
+++ b/src/util/layers.ts
@@ -73,13 +73,17 @@ export function getStartingChallenges(
 
 export function getStartingBoards(
     boards?: Record<string, Board> | Record<string, RawBoard> | undefined
-): Record<string, Array<BoardNode>> {
+): Record<string, BoardNode[]> {
     return boards
-        ? Object.keys(boards).reduce((acc: Record<string, Array<BoardNode>>, curr: string): Record<
+        ? Object.keys(boards).reduce((acc: Record<string, BoardNode[]>, curr: string): Record<
               string,
-              Array<BoardNode>
+              BoardNode[]
           > => {
-              acc[curr] = boards[curr].startNodes?.() || [];
+              const nodes = boards[curr].startNodes?.() || [];
+              acc[curr] = nodes.map((node, index) => ({
+                  id: index.toString(),
+                  ...node
+              })) as BoardNode[];
               return acc;
           }, {})
         : {};

From 00ddf767dae84c7571807c26846475ca4218bae0 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 21:13:31 -0500
Subject: [PATCH 08/49] Added replit support

---
 .replit           |  2 ++
 package-lock.json | 11 +++--------
 package.json      |  1 +
 vue.config.js     |  6 ++++++
 4 files changed, 12 insertions(+), 8 deletions(-)
 create mode 100644 .replit

diff --git a/.replit b/.replit
new file mode 100644
index 0000000..9978ad8
--- /dev/null
+++ b/.replit
@@ -0,0 +1,2 @@
+language = "nodejs"
+run = "npm run serve"
diff --git a/package-lock.json b/package-lock.json
index b496392..1871ec0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
         "@vue/eslint-config-typescript": "^7.0.0",
         "babel-eslint": "^10.1.0",
         "eslint": "^6.7.2",
+        "eslint-plugin-prettier": "^3.4.0",
         "eslint-plugin-vue": "^7.0.0-alpha.0",
         "prettier": "^1.19.1",
         "raw-loader": "^4.0.2",
@@ -18245,7 +18246,6 @@
       "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"
       },
@@ -18894,8 +18894,7 @@
       "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
+      "dev": true
     },
     "node_modules/fast-glob": {
       "version": "2.2.7",
@@ -23775,7 +23774,6 @@
       "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"
       },
@@ -33899,7 +33897,6 @@
       "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"
       }
@@ -34361,8 +34358,7 @@
       "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
+      "dev": true
     },
     "fast-glob": {
       "version": "2.2.7",
@@ -38256,7 +38252,6 @@
       "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,
       "requires": {
         "fast-diff": "^1.1.2"
       }
diff --git a/package.json b/package.json
index d09e8ea..f9fbcff 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "@vue/eslint-config-typescript": "^7.0.0",
     "babel-eslint": "^10.1.0",
     "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.4.0",
     "eslint-plugin-vue": "^7.0.0-alpha.0",
     "prettier": "^1.19.1",
     "raw-loader": "^4.0.2",
diff --git a/vue.config.js b/vue.config.js
index b707485..34b03af 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -7,5 +7,11 @@ module.exports = {
             .plugin("tsconfig-paths")
             // eslint-disable-next-line @typescript-eslint/no-var-requires
             .use(require("tsconfig-paths-webpack-plugin"));
+        // Remove this if/when all "core" code has no non-ignored more type errors
+        // https://github.com/vuejs/vue-cli/issues/3157#issuecomment-657090338
+        config.plugins.delete('fork-ts-checker');
+    },
+    devServer: {
+        disableHostCheck: true
     }
 };

From 3163b58ba68defd66eabaa53719164b96c990dd4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 21:14:08 -0500
Subject: [PATCH 09/49] Fixed saves issues

---
 src/util/save.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/util/save.ts b/src/util/save.ts
index ccf6a53..e46b561 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -18,6 +18,7 @@ export function getInitialStore(playerData: Partial<PlayerData> = {}): PlayerDat
             time: Date.now(),
             autosave: true,
             offlineProd: true,
+            offlineTime: new Decimal(0),
             timePlayed: new Decimal(0),
             keepGoing: false,
             lastTenTicks: [],

From fbe4f7883f41d7def6f6f99bac585a6968014ece Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 21:42:11 -0500
Subject: [PATCH 10/49] Fixed starting nodes returning on refresh after being
 removed

---
 src/game/layers.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/game/layers.ts b/src/game/layers.ts
index d20d094..d0ff4e9 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -73,7 +73,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
             buyables: getStartingBuyables(layer.buyables?.data),
             clickables: getStartingClickables(layer.clickables?.data),
             challenges: getStartingChallenges(layer.challenges?.data),
-            boards: getStartingBoards(layer.boards?.data),
+            boards: player.layers[layer.id]?.boards || getStartingBoards(layer.boards?.data),
             grids: {},
             confirmRespecBuyables: false,
             ...(layer.startData?.() || {})

From 0ced1d7cd7de9066d21ecb9e2108e8a8916885f3 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 22:07:34 -0500
Subject: [PATCH 11/49] Added update() method to nodes

---
 src/game/gameLoop.ts            | 8 ++++++++
 src/typings/features/board.d.ts | 6 ++++--
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/game/gameLoop.ts b/src/game/gameLoop.ts
index f197526..3016185 100644
--- a/src/game/gameLoop.ts
+++ b/src/game/gameLoop.ts
@@ -68,6 +68,14 @@ function updateLayers(diff: DecimalSource) {
             );
         }
         layers[layer].update?.(diff);
+        if (layers[layer].boards) {
+            Reflect.ownKeys(player.layers[layer].boards).forEach(board => {
+                player.layers[layer].boards[board.toString()].forEach(node => {
+                    const nodeType = layers[layer].boards!.data[board.toString()].types[node.type];
+                    nodeType.update?.(node, diff);
+                });
+            });
+        }
     });
     // Automate each active layer
     activeLayers.forEach(layer => {
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 6346516..bea8579 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -1,3 +1,4 @@
+import { DecimalSource } from "@/lib/break_eternity";
 import { State } from "../state";
 import { Feature, RawFeature } from "./feature";
 
@@ -41,7 +42,8 @@ export interface NodeType extends Feature {
     fillColor?: string | ((node: BoardNode) => string);
     outlineColor?: string | ((node: BoardNode) => string);
     titleColor?: string | ((node: BoardNode) => string);
-    onClick: (node: BoardNode) => void;
-    onDrop: (node: BoardNode, otherNode: BoardNode) => void;
+    onClick?: (node: BoardNode) => void;
+    onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
+    update?: (node: BoardNode, diff: DecimalSource) => void;
     nodes: BoardNode[];
 }

From b43f3003b531c1bf36df7012e0b5d5baa02d4bba Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 23:21:13 -0500
Subject: [PATCH 12/49] Added diamond shaped nodes

---
 src/components/board/BoardNode.vue | 100 ++++++++++++++++++++++-------
 src/data/layers/main.ts            |  39 +++++++++++
 src/game/enums.ts                  |   5 ++
 src/game/layers.ts                 |   3 +-
 src/typings/features/board.d.ts    |   2 +
 5 files changed, 124 insertions(+), 25 deletions(-)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index ac3d2a6..6d8298b 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -7,31 +7,79 @@
         @mouseleave="mouseLeave"
         @mousedown="e => $emit('startDragging', e, node.id)"
     >
-        <circle
-            v-if="canAccept"
-            :r="size + 8"
-            :fill="backgroundColor"
-            :stroke="receivingNode ? '#0F0' : '#0F03'"
-            :stroke-width="2"
-        />
+        <g v-if="shape === Shape.Circle">
+            <circle
+                v-if="canAccept"
+                :r="size + 8"
+                :fill="backgroundColor"
+                :stroke="receivingNode ? '#0F0' : '#0F03'"
+                :stroke-width="2"
+            />
 
-        <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
+            <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
 
-        <circle
-            v-if="progressDisplay === ProgressDisplay.Fill"
-            :r="size * progress"
-            :fill="progressColor"
-        />
-        <circle
-            v-else
-            :r="size + 4.5"
-            class="progressRing"
-            fill="transparent"
-            :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
-            :stroke-width="5"
-            :stroke-dashoffset="(size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI"
-            :stroke="progressColor"
-        />
+            <circle
+                v-if="progressDisplay === ProgressDisplay.Fill"
+                :r="Math.max(size * progress - 2, 0)"
+                :fill="progressColor"
+            />
+            <circle
+                v-else
+                :r="size + 4.5"
+                class="progressRing"
+                fill="transparent"
+                :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
+                :stroke-width="5"
+                :stroke-dashoffset="
+                    (size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
+                "
+                :stroke="progressColor"
+            />
+        </g>
+        <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
+            <rect
+                v-if="canAccept"
+                :width="size + 16"
+                :height="size + 16"
+                :transform="`translate(${-(size + 16) / 2}, ${-(size + 16) / 2})`"
+                :fill="backgroundColor"
+                :stroke="receivingNode ? '#0F0' : '#0F03'"
+                :stroke-width="2"
+            />
+
+            <rect
+                :width="size"
+                :height="size"
+                :transform="`translate(${-size / 2}, ${-size / 2})`"
+                :fill="fillColor"
+                :stroke="outlineColor"
+                :stroke-width="4"
+            />
+
+            <rect
+                v-if="progressDisplay === ProgressDisplay.Fill"
+                :width="Math.max(size * progress - 2, 0)"
+                :height="Math.max(size * progress - 2, 0)"
+                :transform="
+                    `translate(${-Math.max(size * progress - 2, 0) / 2}, ${-Math.max(
+                        size * progress - 2,
+                        0
+                    ) / 2})`
+                "
+                :fill="progressColor"
+            />
+            <rect
+                v-else
+                :width="size + 9"
+                :height="size + 9"
+                :transform="`translate(${-(size + 9) / 2}, ${-(size + 9) / 2})`"
+                fill="transparent"
+                :stroke-dasharray="(size + 9) * 4"
+                :stroke-width="5"
+                :stroke-dashoffset="(size + 9) * 4 - progress * (size + 9) * 4"
+                :stroke="progressColor"
+            />
+        </g>
 
         <text :fill="titleColor" class="node-title">{{ title }}</text>
     </g>
@@ -39,7 +87,7 @@
 
 <script lang="ts">
 import themes from "@/data/themes";
-import { ProgressDisplay } from "@/game/enums";
+import { ProgressDisplay, Shape } from "@/game/enums";
 import player from "@/game/player";
 import { BoardNode, NodeType } from "@/typings/features/board";
 import { getNodeTypeProperty } from "@/util/features";
@@ -52,6 +100,7 @@ export default defineComponent({
     data() {
         return {
             ProgressDisplay,
+            Shape,
             lastMousePosition: { x: 0, y: 0 },
             hovering: false
         };
@@ -90,6 +139,9 @@ export default defineComponent({
                   }
                 : this.node.position;
         },
+        shape(): Shape {
+            return getNodeTypeProperty(this.nodeType, this.node, "shape");
+        },
         size(): number {
             let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
             if (this.receivingNode) {
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index f16b1d4..255b46c 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -1,6 +1,8 @@
+import { ProgressDisplay, Shape } from "@/game/enums";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
+import { camelToTitle } from "@/util/common";
 import themes from "../themes";
 
 type ResourceNodeData = {
@@ -14,6 +16,10 @@ type ItemNodeData = {
     amount: DecimalSource;
 };
 
+type ActionNodeData = {
+    actionType: string;
+};
+
 export default {
     id: "main",
     display: `
@@ -21,7 +27,15 @@ export default {
         <div v-else-if="player.devSpeed && player.devSpeed !== 1">Dev Speed: {{ format(player.devSpeed) }}x</div>
         <div>TODO: Board</div>
         <Board id="main" />
+
     `,
+    startData() {
+        return {
+            openNode: null
+        } as {
+            openNode: string | null;
+        };
+    },
     minimizable: false,
     componentStyles: {
         board: {
@@ -52,6 +66,13 @@ export default {
                                 itemType: "speed",
                                 amount: new Decimal(5 * 60 * 60)
                             }
+                        },
+                        {
+                            position: { x: -150, y: 150 },
+                            type: "action",
+                            data: {
+                                actionType: "browse"
+                            }
                         }
                     ];
                 },
@@ -92,6 +113,24 @@ export default {
                             return (node.data as ItemNodeData).itemType;
                         },
                         draggable: true
+                    },
+                    action: {
+                        title(node) {
+                            return camelToTitle((node.data as ActionNodeData).actionType);
+                        },
+                        tooltip(node) {
+                            switch ((node.data as ActionNodeData).actionType) {
+                                default:
+                                    return camelToTitle((node.data as ActionNodeData).actionType);
+                                case "browse":
+                                    return "Browse the internet";
+                            }
+                        },
+                        draggable: false,
+                        shape: Shape.Diamond,
+                        size: 100,
+                        progressColor: "#0FF3",
+                        progressDisplay: ProgressDisplay.Outline
                     }
                 }
             }
diff --git a/src/game/enums.ts b/src/game/enums.ts
index a9047d3..ae66db4 100644
--- a/src/game/enums.ts
+++ b/src/game/enums.ts
@@ -33,3 +33,8 @@ export enum ProgressDisplay {
     Outline = "Outline",
     Fill = "Fill"
 }
+
+export enum Shape {
+    Circle = "Circle",
+    Diamond = "Triangle"
+}
diff --git a/src/game/layers.ts b/src/game/layers.ts
index d0ff4e9..998b91b 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -34,7 +34,7 @@ import { createGridProxy, createLayerProxy } from "@/util/proxies";
 import { applyPlayerData } from "@/util/save";
 import clone from "lodash.clonedeep";
 import { isRef } from "vue";
-import { ProgressDisplay } from "./enums";
+import { ProgressDisplay, Shape } from "./enums";
 import { default as playerProxy } from "./player";
 
 export const layers: Record<string, Readonly<Layer>> = {};
@@ -438,6 +438,7 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
                 layer.boards.data[id].types[nodeType].type = nodeType;
                 setDefault(layer.boards.data[id].types[nodeType], "size", 50);
                 setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
+                setDefault(layer.boards.data[id].types[nodeType], "shape", Shape.Circle);
                 setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
                 setDefault(
                     layer.boards.data[id].types[nodeType],
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index bea8579..0ab3535 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -1,3 +1,4 @@
+import { Shape } from "@/game/enums";
 import { DecimalSource } from "@/lib/break_eternity";
 import { State } from "../state";
 import { Feature, RawFeature } from "./feature";
@@ -35,6 +36,7 @@ export interface NodeType extends Feature {
     title: string | ((node: BoardNode) => string);
     size: number | ((node: BoardNode) => number);
     draggable: boolean | ((node: BoardNode) => boolean);
+    shape: Shape | ((node: BoardNode) => Shape);
     canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean);
     progress?: number | ((node: BoardNode) => number);
     progressDisplay: ProgressDisplay | ((node: BoardNode) => ProgressDisplay);

From 4e7dfe8cbf7019b59adb103c86788567e56d969d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 23:33:54 -0500
Subject: [PATCH 13/49] Removed tooltip field from board node type

---
 src/typings/features/board.d.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 0ab3535..13e59ca 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -32,7 +32,6 @@ export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
 };
 
 export interface NodeType extends Feature {
-    tooltip?: string | ((node: BoardNode) => string);
     title: string | ((node: BoardNode) => string);
     size: number | ((node: BoardNode) => number);
     draggable: boolean | ((node: BoardNode) => boolean);

From 0e139785a5cc6907f3e6f5e3886b1b1cf2f48da3 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 23:45:57 -0500
Subject: [PATCH 14/49] Fixed a couple type errors

---
 src/components/board/Board.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 21e966d..8ee94e0 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -126,7 +126,7 @@ export default defineComponent({
         getZoomLevel(): number {
             return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
         },
-        onInit: function(panzoomInstance) {
+        onInit: function(panzoomInstance: any) {
             panzoomInstance.setTransformOrigin(null);
         },
         startDragging(e: MouseEvent, nodeID: string) {
@@ -168,7 +168,7 @@ export default defineComponent({
                 nodes.push(draggingNode);
 
                 if (receivingNode) {
-                    this.board.types[receivingNode.type].onDrop(receivingNode, draggingNode);
+                    this.board.types[receivingNode.type].onDrop?.(receivingNode, draggingNode);
                 }
 
                 this.dragging = null;

From 0dc1af27b57add63407296e876f9564c4cdcbfb9 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 23:53:40 -0500
Subject: [PATCH 15/49] Fixed nodes always being draggable

---
 src/components/board/Board.vue     | 6 ++++--
 src/components/board/BoardNode.vue | 7 ++++++-
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 8ee94e0..b904f8e 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -149,8 +149,10 @@ export default defineComponent({
                 e.stopPropagation();
 
                 const zoom = (this.getZoomLevel as () => number)();
-                this.dragged.x += (e.clientX - this.lastMousePosition.x) / zoom;
-                this.dragged.y += (e.clientY - this.lastMousePosition.y) / zoom;
+                this.dragged = {
+                    x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom,
+                    y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom
+                }
                 this.lastMousePosition = {
                     x: e.clientX,
                     y: e.clientY
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 6d8298b..93cd9c4 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -5,7 +5,7 @@
         :transform="`translate(${position.x},${position.y})`"
         @mouseenter="mouseEnter"
         @mouseleave="mouseLeave"
-        @mousedown="e => $emit('startDragging', e, node.id)"
+        @mousedown="mouseDown"
     >
         <g v-if="shape === Shape.Circle">
             <circle
@@ -197,6 +197,11 @@ export default defineComponent({
         }
     },
     methods: {
+        mouseDown(e: MouseEvent) {
+            if (this.draggable) {
+                this.$emit('startDragging', e, this.node.id);
+            }
+        },
         mouseEnter() {
             this.hovering = true;
         },

From 35b3226995b6529d945829941212087b2248ce86 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 20 Aug 2021 23:53:59 -0500
Subject: [PATCH 16/49] Updated action node type

---
 src/data/layers/main.ts | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 255b46c..9b9f41b 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -118,15 +118,10 @@ export default {
                         title(node) {
                             return camelToTitle((node.data as ActionNodeData).actionType);
                         },
-                        tooltip(node) {
-                            switch ((node.data as ActionNodeData).actionType) {
-                                default:
-                                    return camelToTitle((node.data as ActionNodeData).actionType);
-                                case "browse":
-                                    return "Browse the internet";
-                            }
+                        fillColor() {
+                            return themes[player.theme].variables["--background-tooltip"];
                         },
-                        draggable: false,
+                        draggable: true,
                         shape: Shape.Diamond,
                         size: 100,
                         progressColor: "#0FF3",

From 11df9853d0f86a9230ae5bcecbca9f26b956f360 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 22 Aug 2021 01:50:03 -0500
Subject: [PATCH 17/49] Implemented selecting nodes and running immediate
 actions

---
 src/components/board/Board.vue     |  76 +++++++++-------
 src/components/board/BoardNode.vue | 137 +++++++++++++++++++++++------
 src/data/layers/Main.vue           |  80 +++++++++++++++++
 src/data/layers/main.ts            |  61 +++++++++----
 src/game/gameLoop.ts               |   8 +-
 src/game/layers.ts                 |  29 +++++-
 src/typings/component.d.ts         |   4 +-
 src/typings/features/board.d.ts    |  19 +++-
 src/typings/player.d.ts            |   4 +-
 src/util/layers.ts                 |  20 +++--
 src/util/proxies.ts                |   9 +-
 src/util/vue.ts                    |   2 +-
 12 files changed, 346 insertions(+), 103 deletions(-)
 create mode 100644 src/data/layers/Main.vue

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index b904f8e..78d6ca0 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -6,6 +6,7 @@
         ref="stage"
         @init="onInit"
         @mousemove="drag"
+        @mousedown="deselect"
         @mouseup="() => endDragging(dragging)"
         @mouseleave="() => endDragging(dragging)"
     >
@@ -18,18 +19,9 @@
                     :nodeType="board.types[node.type]"
                     :dragging="draggingNode"
                     :dragged="dragged"
+                    :hasDragged="hasDragged"
                     :receivingNode="receivingNode?.id === node.id"
-                    @startDragging="startDragging"
-                    @endDragging="endDragging"
-                />
-                <BoardNode
-                    v-if="draggingNode"
-                    :node="draggingNode"
-                    :nodeType="board.types[draggingNode.type]"
-                    :dragging="draggingNode"
-                    :dragged="dragged"
-                    :receivingNode="receivingNode?.id === draggingNode.id"
-                    @startDragging="startDragging"
+                    @mouseDown="mouseDown"
                     @endDragging="endDragging"
                 />
             </g>
@@ -51,11 +43,13 @@ export default defineComponent({
         return {
             lastMousePosition: { x: 0, y: 0 },
             dragged: { x: 0, y: 0 },
-            dragging: null
+            dragging: null,
+            hasDragged: false
         } as {
             lastMousePosition: { x: number; y: number };
             dragged: { x: number; y: number };
             dragging: string | null;
+            hasDragged: boolean;
         };
     },
     props: {
@@ -79,14 +73,15 @@ export default defineComponent({
             ];
         },
         draggingNode() {
-            return this.dragging
-                ? player.layers[this.layer].boards[this.id].find(node => node.id === this.dragging)
-                : null;
+            return this.dragging ? this.board.nodes.find(node => node.id === this.dragging) : null;
         },
         nodes() {
-            return player.layers[this.layer].boards[this.id].filter(
-                node => node !== this.draggingNode
-            );
+            const nodes = this.board.nodes.slice();
+            if (this.draggingNode) {
+                const draggingNode = nodes.splice(nodes.indexOf(this.draggingNode), 1)[0];
+                nodes.push(draggingNode);
+            }
+            return nodes;
         },
         receivingNode(): BoardNode | null {
             if (this.draggingNode == null) {
@@ -99,6 +94,9 @@ export default defineComponent({
             };
             let smallestDistance = Number.MAX_VALUE;
             return this.nodes.reduce((smallest: BoardNode | null, curr: BoardNode) => {
+                if (curr.id === this.draggingNode!.id) {
+                    return smallest;
+                }
                 const nodeType = this.board.types[curr.type];
                 const canAccept =
                     typeof nodeType.canAccept === "boolean"
@@ -126,10 +124,14 @@ export default defineComponent({
         getZoomLevel(): number {
             return (this.$refs.stage as any).$panZoomInstance.getTransform().scale;
         },
-        onInit: function(panzoomInstance: any) {
+        onInit(panzoomInstance: any) {
             panzoomInstance.setTransformOrigin(null);
         },
-        startDragging(e: MouseEvent, nodeID: string) {
+        deselect() {
+            player.layers[this.layer].boards[this.id].selectedNode = null;
+            player.layers[this.layer].boards[this.id].selectedAction = null;
+        },
+        mouseDown(e: MouseEvent, nodeID: string, draggable: boolean) {
             if (this.dragging == null) {
                 e.preventDefault();
                 e.stopPropagation();
@@ -139,33 +141,43 @@ export default defineComponent({
                     y: e.clientY
                 };
                 this.dragged = { x: 0, y: 0 };
+                this.hasDragged = false;
 
-                this.dragging = nodeID;
+                if (draggable) {
+                    this.dragging = nodeID;
+                }
             }
+            player.layers[this.layer].boards[this.id].selectedNode = null;
+            player.layers[this.layer].boards[this.id].selectedAction = null;
         },
         drag(e: MouseEvent) {
+            const zoom = (this.getZoomLevel as () => number)();
+            this.dragged = {
+                x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom,
+                y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom
+            };
+            this.lastMousePosition = {
+                x: e.clientX,
+                y: e.clientY
+            };
+
+            if (Math.abs(this.dragged.x) > 10 || Math.abs(this.dragged.y) > 10) {
+                this.hasDragged = true;
+            }
+
             if (this.dragging) {
                 e.preventDefault();
                 e.stopPropagation();
-
-                const zoom = (this.getZoomLevel as () => number)();
-                this.dragged = {
-                    x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom,
-                    y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom
-                }
-                this.lastMousePosition = {
-                    x: e.clientX,
-                    y: e.clientY
-                };
             }
         },
         endDragging(nodeID: string | null) {
             if (this.dragging != null && this.dragging === nodeID) {
-                const nodes = player.layers[this.layer].boards[this.id];
                 const draggingNode = this.draggingNode!;
                 const receivingNode = this.receivingNode;
                 draggingNode.position.x += Math.round(this.dragged.x / 25) * 25;
                 draggingNode.position.y += Math.round(this.dragged.y / 25) * 25;
+
+                const nodes = this.board.nodes;
                 nodes.splice(nodes.indexOf(draggingNode), 1);
                 nodes.push(draggingNode);
 
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 93cd9c4..c4d5746 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -1,13 +1,38 @@
 <template>
     <g
         class="boardnode"
-        :style="{ opacity: dragging?.id === node.id ? 0.5 : 1 }"
+        :style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
         :transform="`translate(${position.x},${position.y})`"
-        @mouseenter="mouseEnter"
-        @mouseleave="mouseLeave"
-        @mousedown="mouseDown"
     >
-        <g v-if="shape === Shape.Circle">
+        <transition name="actions" appear>
+            <g v-if="selected && actions">
+                <g
+                    v-for="(action, index) in actions"
+                    :key="action.id"
+                    class="action"
+                    :transform="
+                        `translate(
+                            ${(-size - 30) *
+                                Math.sin(((actions.length - 1) / 2 - index) * actionDistance)},
+                            ${(size + 30) *
+                                Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
+                        )`
+                    "
+                    @click="performAction(action)"
+                >
+                    <circle :fill="fillColor" r="20" />
+                    <text :fill="titleColor" class="material-icons">{{ action.icon }}</text>
+                </g>
+            </g>
+        </transition>
+
+        <g
+            v-if="shape === Shape.Circle"
+            @mouseenter="mouseEnter"
+            @mouseleave="mouseLeave"
+            @mousedown="mouseDown"
+            @mouseup="mouseUp"
+        >
             <circle
                 v-if="canAccept"
                 :r="size + 8"
@@ -36,21 +61,30 @@
                 :stroke="progressColor"
             />
         </g>
-        <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
+        <g
+            v-else-if="shape === Shape.Diamond"
+            transform="rotate(45, 0, 0)"
+            @mouseenter="mouseEnter"
+            @mouseleave="mouseLeave"
+            @mousedown="mouseDown"
+            @mouseup="mouseUp"
+        >
             <rect
                 v-if="canAccept"
-                :width="size + 16"
-                :height="size + 16"
-                :transform="`translate(${-(size + 16) / 2}, ${-(size + 16) / 2})`"
+                :width="size * sqrtTwo + 16"
+                :height="size * sqrtTwo + 16"
+                :transform="
+                    `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) / 2})`
+                "
                 :fill="backgroundColor"
                 :stroke="receivingNode ? '#0F0' : '#0F03'"
                 :stroke-width="2"
             />
 
             <rect
-                :width="size"
-                :height="size"
-                :transform="`translate(${-size / 2}, ${-size / 2})`"
+                :width="size * sqrtTwo"
+                :height="size * sqrtTwo"
+                :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
                 :fill="fillColor"
                 :stroke="outlineColor"
                 :stroke-width="4"
@@ -58,11 +92,11 @@
 
             <rect
                 v-if="progressDisplay === ProgressDisplay.Fill"
-                :width="Math.max(size * progress - 2, 0)"
-                :height="Math.max(size * progress - 2, 0)"
+                :width="Math.max(size * sqrtTwo * progress - 2, 0)"
+                :height="Math.max(size * sqrtTwo * progress - 2, 0)"
                 :transform="
-                    `translate(${-Math.max(size * progress - 2, 0) / 2}, ${-Math.max(
-                        size * progress - 2,
+                    `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${-Math.max(
+                        size * sqrtTwo * progress - 2,
                         0
                     ) / 2})`
                 "
@@ -70,13 +104,13 @@
             />
             <rect
                 v-else
-                :width="size + 9"
-                :height="size + 9"
-                :transform="`translate(${-(size + 9) / 2}, ${-(size + 9) / 2})`"
+                :width="size * sqrtTwo + 9"
+                :height="size * sqrtTwo + 9"
+                :transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`"
                 fill="transparent"
-                :stroke-dasharray="(size + 9) * 4"
+                :stroke-dasharray="(size * sqrtTwo + 9) * 4"
                 :stroke-width="5"
-                :stroke-dashoffset="(size + 9) * 4 - progress * (size + 9) * 4"
+                :stroke-dashoffset="(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4"
                 :stroke="progressColor"
             />
         </g>
@@ -88,8 +122,9 @@
 <script lang="ts">
 import themes from "@/data/themes";
 import { ProgressDisplay, Shape } from "@/game/enums";
+import { layers } from "@/game/layers";
 import player from "@/game/player";
-import { BoardNode, NodeType } from "@/typings/features/board";
+import { BoardNode, BoardNodeAction, NodeType } from "@/typings/features/board";
 import { getNodeTypeProperty } from "@/util/features";
 import { InjectLayerMixin } from "@/util/vue";
 import { defineComponent, PropType } from "vue";
@@ -102,10 +137,11 @@ export default defineComponent({
             ProgressDisplay,
             Shape,
             lastMousePosition: { x: 0, y: 0 },
-            hovering: false
+            hovering: false,
+            sqrtTwo: Math.sqrt(2)
         };
     },
-    emits: ["startDragging", "endDragging"],
+    emits: ["mouseDown", "endDragging"],
     props: {
         node: {
             type: Object as PropType<BoardNode>,
@@ -122,12 +158,25 @@ export default defineComponent({
             type: Object as PropType<{ x: number; y: number }>,
             required: true
         },
+        hasDragged: {
+            type: Boolean,
+            default: false
+        },
         receivingNode: {
             type: Boolean,
             default: false
         }
     },
     computed: {
+        board() {
+            return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
+        },
+        selected() {
+            return this.board.selectedNode?.id === this.node.id;
+        },
+        actions(): BoardNodeAction[] | null | undefined {
+            return getNodeTypeProperty(this.nodeType, this.node, "actions");
+        },
         draggable(): boolean {
             return getNodeTypeProperty(this.nodeType, this.node, "draggable");
         },
@@ -146,7 +195,7 @@ export default defineComponent({
             let size: number = getNodeTypeProperty(this.nodeType, this.node, "size");
             if (this.receivingNode) {
                 size *= 1.25;
-            } else if (this.hovering) {
+            } else if (this.hovering || this.selected) {
                 size *= 1.15;
             }
             return size;
@@ -188,18 +237,24 @@ export default defineComponent({
             );
         },
         canAccept(): boolean {
-            if (this.dragging == null) {
+            if (this.dragging == null || !this.hasDragged) {
                 return false;
             }
             return typeof this.nodeType.canAccept === "boolean"
                 ? this.nodeType.canAccept
                 : this.nodeType.canAccept(this.node, this.dragging);
+        },
+        actionDistance(): number {
+            return getNodeTypeProperty(this.nodeType, this.node, "actionDistance");
         }
     },
     methods: {
         mouseDown(e: MouseEvent) {
-            if (this.draggable) {
-                this.$emit('startDragging', e, this.node.id);
+            this.$emit("mouseDown", e, this.node.id, this.draggable);
+        },
+        mouseUp() {
+            if (!this.hasDragged) {
+                this.nodeType.onClick?.(this.node);
             }
         },
         mouseEnter() {
@@ -207,11 +262,14 @@ export default defineComponent({
         },
         mouseLeave() {
             this.hovering = false;
+        },
+        performAction(action: BoardNodeAction) {
+            action.onClick(this.node);
         }
     },
     watch: {
         onDraggableChanged() {
-            if (this.dragging && !this.draggable) {
+            if (this.dragging?.id === this.node.id && !this.draggable) {
                 this.$emit("endDragging", this.node.id);
             }
         }
@@ -230,9 +288,30 @@ export default defineComponent({
     dominant-baseline: middle;
     font-family: monospace;
     font-size: 200%;
+    pointer-events: none;
 }
 
 .progressRing {
     transform: rotate(-90deg);
 }
+
+.action:hover circle {
+    r: 25;
+}
+
+.action:hover text {
+    font-size: 187.5%; /* 150% * 1.25 */
+}
+
+.action text {
+    text-anchor: middle;
+    dominant-baseline: central;
+}
+</style>
+
+<style>
+.actions-enter-from .action,
+.actions-leave-to .action {
+    transform: translate(0, 0);
+}
 </style>
diff --git a/src/data/layers/Main.vue b/src/data/layers/Main.vue
new file mode 100644
index 0000000..84d1fec
--- /dev/null
+++ b/src/data/layers/Main.vue
@@ -0,0 +1,80 @@
+<template>
+    <div v-if="devSpeed === 0">Game Paused</div>
+    <div v-else-if="devSpeed && devSpeed !== 1">Dev Speed: {{ formattedDevSpeed }}x</div>
+    <Board id="main" />
+    <Modal :show="showModal" @close="closeModal">
+        <template v-slot:header v-if="title">
+            <component :is="title" />
+        </template>
+        <template v-slot:body v-if="body">
+            <component :is="body" />
+        </template>
+        <template v-slot:footer v-if="footer">
+            <component :is="footer" />
+        </template>
+    </Modal>
+</template>
+
+<script lang="ts">
+import player from "@/game/player";
+import { CoercableComponent } from "@/typings/component";
+import { format } from "@/util/break_eternity";
+import { camelToTitle } from "@/util/common";
+import { coerceComponent } from "@/util/vue";
+import { computed, defineComponent, shallowRef, watchEffect } from "vue";
+import { ActionNodeData, ResourceNodeData } from "./main";
+
+export default defineComponent(function Main() {
+    const title = shallowRef<CoercableComponent | null>(null);
+    const body = shallowRef<CoercableComponent | null>(null);
+    const footer = shallowRef<CoercableComponent | null>(null);
+
+    watchEffect(() => {
+        const node = player.layers.main.boards.main.nodes.find(
+            node => node.id === player.layers.main.openNode
+        );
+        if (node == null) {
+            player.layers.main.showModal = false;
+            return;
+        }
+        switch (node.type) {
+            default:
+                player.layers.main.showModal = false;
+                break;
+            case "resource":
+                switch ((node.data as ResourceNodeData).resourceType) {
+                    default:
+                        player.layers.main.showModal = false;
+                        break;
+                    case "time":
+                        title.value = coerceComponent("<h2>Time</h2>");
+                        body.value = coerceComponent(
+                            "The ultimate resource, that you'll never have enough of."
+                        );
+                        break;
+                }
+                break;
+            case "action":
+                title.value = coerceComponent(
+                    camelToTitle((node.data as ActionNodeData).actionType)
+                );
+                body.value = coerceComponent(
+                    "<div><div>" +
+                        (node.data as ActionNodeData).log.join("</div><div>") +
+                        "</div></div>"
+                );
+                break;
+        }
+    });
+
+    const showModal = computed(() => player.layers.main.showModal);
+    const closeModal = () => {
+        player.layers.main.showModal = false;
+    };
+
+    const devSpeed = computed(() => player.devSpeed);
+    const formattedDevSpeed = computed(() => player.devSpeed && format(player.devSpeed));
+
+    return { title, body, footer, showModal, closeModal, devSpeed, formattedDevSpeed };
+});
+</script>
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 9b9f41b..d15d95c 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -4,36 +4,34 @@ import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
 import { camelToTitle } from "@/util/common";
 import themes from "../themes";
+import Main from "./Main.vue";
 
-type ResourceNodeData = {
+export type ResourceNodeData = {
     resourceType: string;
     amount: DecimalSource;
     maxAmount: DecimalSource;
 };
 
-type ItemNodeData = {
+export type ItemNodeData = {
     itemType: string;
     amount: DecimalSource;
 };
 
-type ActionNodeData = {
+export type ActionNodeData = {
     actionType: string;
+    log: string[];
 };
 
 export default {
     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>TODO: Board</div>
-        <Board id="main" />
-
-    `,
+    display: Main,
     startData() {
         return {
-            openNode: null
+            openNode: null,
+            showModal: false
         } as {
             openNode: string | null;
+            showModal: boolean;
         };
     },
     minimizable: false,
@@ -71,7 +69,8 @@ export default {
                             position: { x: -150, y: 150 },
                             type: "action",
                             data: {
-                                actionType: "browse"
+                                actionType: "web",
+                                log: []
                             }
                         }
                     ];
@@ -101,31 +100,55 @@ export default {
                         canAccept(node, otherNode) {
                             return otherNode.type === "item";
                         },
+                        onClick(node) {
+                            player.layers.main.openNode = node.id;
+                            player.layers.main.showModal = true;
+                        },
                         onDrop(node, otherNode) {
-                            const index = player.layers[this.layer].boards[this.id].indexOf(
+                            const index = player.layers[this.layer].boards[this.id].nodes.indexOf(
                                 otherNode
                             );
-                            player.layers[this.layer].boards[this.id].splice(index, 1);
+                            player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
                         }
                     },
                     item: {
                         title(node) {
                             return (node.data as ItemNodeData).itemType;
                         },
+                        onClick(node) {
+                            player.layers.main.openNode = node.id;
+                            player.layers.main.showModal = true;
+                        },
                         draggable: true
                     },
                     action: {
                         title(node) {
                             return camelToTitle((node.data as ActionNodeData).actionType);
                         },
-                        fillColor() {
-                            return themes[player.theme].variables["--background-tooltip"];
-                        },
+                        fillColor: "#000",
                         draggable: true,
                         shape: Shape.Diamond,
-                        size: 100,
                         progressColor: "#0FF3",
-                        progressDisplay: ProgressDisplay.Outline
+                        progressDisplay: ProgressDisplay.Outline,
+                        actions: [
+                            {
+                                id: "info",
+                                icon: "history_edu",
+                                tooltip: "Log",
+                                onClick(node) {
+                                    player.layers.main.openNode = node.id;
+                                    player.layers.main.showModal = true;
+                                }
+                            },
+                            {
+                                id: "reddit",
+                                icon: "reddit",
+                                tooltip: "Browse Reddit",
+                                onClick(node) {
+                                    // TODO
+                                }
+                            }
+                        ]
                     }
                 }
             }
diff --git a/src/game/gameLoop.ts b/src/game/gameLoop.ts
index 3016185..d6c3f52 100644
--- a/src/game/gameLoop.ts
+++ b/src/game/gameLoop.ts
@@ -68,10 +68,10 @@ function updateLayers(diff: DecimalSource) {
             );
         }
         layers[layer].update?.(diff);
-        if (layers[layer].boards) {
-            Reflect.ownKeys(player.layers[layer].boards).forEach(board => {
-                player.layers[layer].boards[board.toString()].forEach(node => {
-                    const nodeType = layers[layer].boards!.data[board.toString()].types[node.type];
+        if (layers[layer].boards && layers[layer].boards?.data) {
+            Object.values(layers[layer].boards!.data!).forEach(board => {
+                board.nodes.forEach(node => {
+                    const nodeType = board.types[node.type];
                     nodeType.update?.(node, diff);
                 });
             });
diff --git a/src/game/layers.ts b/src/game/layers.ts
index 998b91b..a1b62c1 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -432,6 +432,29 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
         for (const id in layer.boards.data) {
             setDefault(layer.boards.data[id], "width", "100%");
             setDefault(layer.boards.data[id], "height", "400px");
+            setDefault(layer.boards.data[id], "nodes", function() {
+                return playerProxy.layers[this.layer].boards[this.id].nodes;
+            });
+            setDefault(layer.boards.data[id], "selectedNode", function() {
+                return playerProxy.layers[this.layer].boards[this.id].nodes.find(
+                    node => node.id === playerProxy.layers[this.layer].boards[this.id].selectedNode
+                );
+            });
+            setDefault(layer.boards.data[id], "selectedAction", function() {
+                if (this.selectedNode == null) {
+                    return null;
+                }
+                const nodeType = layers[this.layer].boards!.data[this.id].types[
+                    this.selectedNode.type
+                ];
+                if (nodeType.actions === null) {
+                    return null;
+                }
+                if (typeof nodeType.actions === "function") {
+                    return nodeType.actions(this.selectedNode);
+                }
+                return nodeType.actions;
+            });
             for (const nodeType in layer.boards.data[id].types) {
                 layer.boards.data[id].types[nodeType].layer = layer.id;
                 layer.boards.data[id].types[nodeType].id = id;
@@ -440,16 +463,20 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
                 setDefault(layer.boards.data[id].types[nodeType], "draggable", false);
                 setDefault(layer.boards.data[id].types[nodeType], "shape", Shape.Circle);
                 setDefault(layer.boards.data[id].types[nodeType], "canAccept", false);
+                setDefault(layer.boards.data[id].types[nodeType], "actionDistance", Math.PI / 6);
                 setDefault(
                     layer.boards.data[id].types[nodeType],
                     "progressDisplay",
                     ProgressDisplay.Fill
                 );
                 setDefault(layer.boards.data[id].types[nodeType], "nodes", function() {
-                    return playerProxy.layers[this.layer].boards[this.id].filter(
+                    return playerProxy.layers[this.layer].boards[this.id].nodes.filter(
                         node => node.type === this.type
                     );
                 });
+                setDefault(layer.boards.data[id].types[nodeType], "onClick", function(node) {
+                    playerProxy.layers[this.layer].boards[this.id].selectedNode = node.id;
+                });
             }
         }
     }
diff --git a/src/typings/component.d.ts b/src/typings/component.d.ts
index 5b3c011..52620a1 100644
--- a/src/typings/component.d.ts
+++ b/src/typings/component.d.ts
@@ -1,3 +1,3 @@
-import { ComponentOptions } from "vue";
+import { Component, ComponentOptions } from "vue";
 
-export type CoercableComponent = string | ComponentOptions;
+export type CoercableComponent = string | ComponentOptions | Component;
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 13e59ca..a10d348 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -13,9 +13,10 @@ export interface BoardNode {
     data?: State;
 }
 
-export interface CardOption {
-    text: string;
-    selected: (node: BoardNode) => void;
+export interface BoardData {
+    nodes: BoardNode[];
+    selectedNode: string | null;
+    selectedAction: string | null;
 }
 
 export interface Board extends Feature {
@@ -24,6 +25,9 @@ export interface Board extends Feature {
     height: string;
     width: string;
     types: Record<string, NodeType>;
+    nodes: BoardNode[];
+    selectedNode: BoardNode | null;
+    selectedAction: BoardNodeAction | null;
 }
 
 export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
@@ -43,8 +47,17 @@ export interface NodeType extends Feature {
     fillColor?: string | ((node: BoardNode) => string);
     outlineColor?: string | ((node: BoardNode) => string);
     titleColor?: string | ((node: BoardNode) => string);
+    actions?: BoardNodeAction[] | ((node: BoardNode) => BoardNodeAction[]);
+    actionDistance: number | ((node: BoardNode) => number);
     onClick?: (node: BoardNode) => void;
     onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
     update?: (node: BoardNode, diff: DecimalSource) => void;
     nodes: BoardNode[];
 }
+
+export interface BoardNodeAction {
+    id: string;
+    icon: string;
+    tooltip: string;
+    onClick: (node: BoardNode) => void;
+}
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
index dc54d03..c901591 100644
--- a/src/typings/player.d.ts
+++ b/src/typings/player.d.ts
@@ -1,7 +1,7 @@
 import { Themes } from "@/data/themes";
 import { DecimalSource } from "@/lib/break_eternity";
 import Decimal from "@/util/bignum";
-import { BoardNode } from "./features/board";
+import { BoardData, BoardNode } from "./features/board";
 import { MilestoneDisplay } from "./features/milestone";
 import { State } from "./state";
 
@@ -62,7 +62,7 @@ export interface LayerSaveData {
     clickables: Record<string, State>;
     challenges: Record<string, Decimal>;
     grids: Record<string, Record<string, State>>;
-    boards: Record<string, BoardNode[]>;
+    boards: Record<string, BoardData>;
     confirmRespecBuyables: boolean;
     [index: string]: unknown;
 }
diff --git a/src/util/layers.ts b/src/util/layers.ts
index d9be21b..ff4d011 100644
--- a/src/util/layers.ts
+++ b/src/util/layers.ts
@@ -1,7 +1,7 @@
 import { hotkeys, layers } from "@/game/layers";
 import player from "@/game/player";
 import { CacheableFunction } from "@/typings/cacheableFunction";
-import { Board, BoardNode, RawBoard } from "@/typings/features/board";
+import { Board, BoardData, BoardNode, RawBoard } from "@/typings/features/board";
 import { Buyable } from "@/typings/features/buyable";
 import { Challenge } from "@/typings/features/challenge";
 import { Clickable } from "@/typings/features/clickable";
@@ -73,17 +73,21 @@ export function getStartingChallenges(
 
 export function getStartingBoards(
     boards?: Record<string, Board> | Record<string, RawBoard> | undefined
-): Record<string, BoardNode[]> {
+): Record<string, BoardData> {
     return boards
-        ? Object.keys(boards).reduce((acc: Record<string, BoardNode[]>, curr: string): Record<
+        ? Object.keys(boards).reduce((acc: Record<string, BoardData>, curr: string): Record<
               string,
-              BoardNode[]
+              BoardData
           > => {
               const nodes = boards[curr].startNodes?.() || [];
-              acc[curr] = nodes.map((node, index) => ({
-                  id: index.toString(),
-                  ...node
-              })) as BoardNode[];
+              acc[curr] = {
+                  nodes: nodes.map((node, index) => ({
+                      id: index.toString(),
+                      ...node
+                  })),
+                  selectedNode: null,
+                  selectedAction: null
+              } as BoardData;
               return acc;
           }, {})
         : {};
diff --git a/src/util/proxies.ts b/src/util/proxies.ts
index 83c1c58..a541eeb 100644
--- a/src/util/proxies.ts
+++ b/src/util/proxies.ts
@@ -43,7 +43,8 @@ function travel(
             object[key] = computed(object[key].bind(objectProxy));
         } else if (
             (isPlainObject(object[key]) || Array.isArray(object[key])) &&
-            !(object[key] instanceof Decimal)
+            !(object[key] instanceof Decimal) &&
+            typeof object[key].render !== "function"
         ) {
             object[key] = callback(object[key]);
         }
@@ -62,7 +63,11 @@ const layerHandler: ProxyHandler<Record<string, any>> = {
 
         if (isRef(target[key])) {
             return target[key].value;
-        } else if (target[key].isProxy || target[key] instanceof Decimal) {
+        } else if (
+            target[key].isProxy ||
+            target[key] instanceof Decimal ||
+            typeof target[key].render === "function"
+        ) {
             return target[key];
         } else if (
             (isPlainObject(target[key]) || Array.isArray(target[key])) &&
diff --git a/src/util/vue.ts b/src/util/vue.ts
index 28f12ad..b3d953a 100644
--- a/src/util/vue.ts
+++ b/src/util/vue.ts
@@ -35,7 +35,7 @@ const data = function(): Record<string, unknown> {
     return { Decimal, player, layers, hasWon, pointGain, ...numberUtils };
 };
 export function coerceComponent(
-    component: string | ComponentOptions,
+    component: string | ComponentOptions | Component,
     defaultWrapper = "span"
 ): Component | string {
     if (typeof component === "string") {

From c4db2a51c7ddf6842ea882c7deccbbca5f29629f Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 22 Aug 2021 02:00:38 -0500
Subject: [PATCH 18/49] Fixed hardReset not updating the active save file

---
 src/util/save.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/util/save.ts b/src/util/save.ts
index e46b561..45ef182 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -187,6 +187,9 @@ window.onbeforeunload = () => {
     }
 };
 window.save = save;
-window.hardReset = () => {
-    loadSave(newSave());
+window.hardReset = async () => {
+    await loadSave(newSave());
+    const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!))));
+    modData.active = player.id;
+    localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
 };

From 7618ee291a6c757ee1af1d425ac7996813a514d2 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 22 Aug 2021 22:57:59 -0500
Subject: [PATCH 19/49] Implemented confirmable actions (plus logs and other
 related features)

---
 src/components/board/Board.vue     |  32 +++++---
 src/components/board/BoardLink.vue |  55 +++++++++++++
 src/components/board/BoardNode.vue |  93 +++++++++++++++++++---
 src/components/tree/BranchLine.vue |   8 +-
 src/data/layers/Main.vue           |  12 ++-
 src/data/layers/main.ts            | 120 ++++++++++++++++++++++++++++-
 src/data/mod.ts                    |   1 +
 src/game/layers.ts                 |  22 +++++-
 src/typings/branches.d.ts          |   3 +-
 src/typings/features/board.d.ts    |  27 ++++++-
 src/util/features.ts               |  12 ++-
 src/util/save.ts                   |   2 +-
 12 files changed, 351 insertions(+), 36 deletions(-)
 create mode 100644 src/components/board/BoardLink.vue

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 78d6ca0..1b2eb99 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -2,16 +2,24 @@
     <panZoom
         :style="style"
         selector="#g1"
-        :options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10 }"
+        :options="{ initialZoom: 1, minZoom: 0.1, maxZoom: 10, zoomDoubleClickSpeed: 1 }"
         ref="stage"
         @init="onInit"
         @mousemove="drag"
-        @mousedown="deselect"
+        @touchmove="drag"
+        @mousedown="mouseDown"
+        @touchstart="mouseDown"
         @mouseup="() => endDragging(dragging)"
+        @touchend="() => endDragging(dragging)"
         @mouseleave="() => endDragging(dragging)"
     >
         <svg class="stage" width="100%" height="100%">
             <g id="g1">
+                <transition-group name="link" appear>
+                    <g v-for="(link, index) in board.links || []" :key="index">
+                        <BoardLink :link="link" />
+                    </g>
+                </transition-group>
                 <BoardNode
                     v-for="node in nodes"
                     :key="node.id"
@@ -127,11 +135,7 @@ export default defineComponent({
         onInit(panzoomInstance: any) {
             panzoomInstance.setTransformOrigin(null);
         },
-        deselect() {
-            player.layers[this.layer].boards[this.id].selectedNode = null;
-            player.layers[this.layer].boards[this.id].selectedAction = null;
-        },
-        mouseDown(e: MouseEvent, nodeID: string, draggable: boolean) {
+        mouseDown(e: MouseEvent, nodeID: string | null = null, draggable = false) {
             if (this.dragging == null) {
                 e.preventDefault();
                 e.stopPropagation();
@@ -147,8 +151,10 @@ export default defineComponent({
                     this.dragging = nodeID;
                 }
             }
-            player.layers[this.layer].boards[this.id].selectedNode = null;
-            player.layers[this.layer].boards[this.id].selectedAction = null;
+            if (nodeID != null) {
+                player.layers[this.layer].boards[this.id].selectedNode = null;
+                player.layers[this.layer].boards[this.id].selectedAction = null;
+            }
         },
         drag(e: MouseEvent) {
             const zoom = (this.getZoomLevel as () => number)();
@@ -186,6 +192,9 @@ export default defineComponent({
                 }
 
                 this.dragging = null;
+            } else if (!this.hasDragged) {
+                player.layers[this.layer].boards[this.id].selectedNode = null;
+                player.layers[this.layer].boards[this.id].selectedAction = null;
             }
         }
     }
@@ -202,4 +211,9 @@ export default defineComponent({
 #g1 {
     transition-duration: 0s;
 }
+
+.link-enter-from,
+.link-leave-to {
+    opacity: 0;
+}
 </style>
diff --git a/src/components/board/BoardLink.vue b/src/components/board/BoardLink.vue
new file mode 100644
index 0000000..28af0ba
--- /dev/null
+++ b/src/components/board/BoardLink.vue
@@ -0,0 +1,55 @@
+<template>
+    <line
+        v-bind="link"
+        class="link"
+        :class="{ pulsing: link.pulsing }"
+        :x1="startPosition.x"
+        :y1="startPosition.y"
+        :x2="endPosition.x"
+        :y2="endPosition.y"
+    />
+</template>
+
+<script lang="ts">
+import { Position } from "@/typings/branches";
+import { BoardNodeLink } from "@/typings/features/board";
+import { defineComponent, PropType } from "vue";
+
+export default defineComponent({
+    name: "BoardLink",
+    props: {
+        link: {
+            type: Object as PropType<BoardNodeLink>,
+            required: true
+        }
+    },
+    computed: {
+        startPosition(): Position {
+            return this.link.from.position;
+        },
+        endPosition(): Position {
+            return this.link.to.position;
+        }
+    }
+});
+</script>
+
+<style scoped>
+.link.pulsing {
+    animation: pulsing 2s ease-in infinite;
+}
+
+@keyframes pulsing {
+    0% {
+        opacity: 0.25;
+    }
+
+    50% {
+        opacity: 1;
+    }
+
+    100% {
+        opacity: 0.25;
+    }
+}
+</style>
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index c4d5746..fb5405d 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -6,10 +6,12 @@
     >
         <transition name="actions" appear>
             <g v-if="selected && actions">
+                <!-- TODO move to separate file -->
                 <g
                     v-for="(action, index) in actions"
                     :key="action.id"
                     class="action"
+                    :class="{ selected: selectedAction === action }"
                     :transform="
                         `translate(
                             ${(-size - 30) *
@@ -18,10 +20,23 @@
                                 Math.cos(((actions.length - 1) / 2 - index) * actionDistance)}
                         )`
                     "
-                    @click="performAction(action)"
+                    @mousedown="e => performAction(e, action)"
                 >
-                    <circle :fill="fillColor" r="20" />
-                    <text :fill="titleColor" class="material-icons">{{ action.icon }}</text>
+                    <circle
+                        :fill="
+                            action.fillColor
+                                ? typeof action.fillColor === 'function'
+                                    ? action.fillColor(node)
+                                    : action.fillColor
+                                : fillColor
+                        "
+                        r="20"
+                        :stroke-width="selectedAction === action ? 4 : 0"
+                        :stroke="outlineColor"
+                    />
+                    <text :fill="titleColor" class="material-icons">{{
+                        typeof action.icon === "function" ? action.icon(node) : action.icon
+                    }}</text>
                 </g>
             </g>
         </transition>
@@ -31,7 +46,9 @@
             @mouseenter="mouseEnter"
             @mouseleave="mouseLeave"
             @mousedown="mouseDown"
+            @touchstart="mouseDown"
             @mouseup="mouseUp"
+            @touchend="mouseUp"
         >
             <circle
                 v-if="canAccept"
@@ -67,7 +84,9 @@
             @mouseenter="mouseEnter"
             @mouseleave="mouseLeave"
             @mousedown="mouseDown"
+            @touchstart="mouseDown"
             @mouseup="mouseUp"
+            @touchend="mouseUp"
         >
             <rect
                 v-if="canAccept"
@@ -116,6 +135,27 @@
         </g>
 
         <text :fill="titleColor" class="node-title">{{ title }}</text>
+
+        <transition name="fade" appear>
+            <text
+                v-if="label"
+                :fill="label.color || titleColor"
+                class="node-title"
+                :class="{ pulsing: label.pulsing }"
+                :y="-size - 20"
+                >{{ label.text }}</text
+            >
+        </transition>
+
+        <transition name="fade" appear>
+            <text
+                :fill="titleColor"
+                class="node-title"
+                :y="size + 75"
+                v-if="selected && selectedAction"
+                >Tap again to confirm</text
+            >
+        </transition>
     </g>
 </template>
 
@@ -124,14 +164,12 @@ import themes from "@/data/themes";
 import { ProgressDisplay, Shape } from "@/game/enums";
 import { layers } from "@/game/layers";
 import player from "@/game/player";
-import { BoardNode, BoardNodeAction, NodeType } from "@/typings/features/board";
+import { BoardNode, BoardNodeAction, NodeLabel, NodeType } from "@/typings/features/board";
 import { getNodeTypeProperty } from "@/util/features";
-import { InjectLayerMixin } from "@/util/vue";
 import { defineComponent, PropType } from "vue";
 
 export default defineComponent({
     name: "BoardNode",
-    mixins: [InjectLayerMixin],
     data() {
         return {
             ProgressDisplay,
@@ -174,6 +212,9 @@ export default defineComponent({
         selected() {
             return this.board.selectedNode?.id === this.node.id;
         },
+        selectedAction() {
+            return this.board.selectedAction;
+        },
         actions(): BoardNodeAction[] | null | undefined {
             return getNodeTypeProperty(this.nodeType, this.node, "actions");
         },
@@ -203,6 +244,9 @@ export default defineComponent({
         title(): string {
             return getNodeTypeProperty(this.nodeType, this.node, "title");
         },
+        label(): NodeLabel | null | undefined {
+            return getNodeTypeProperty(this.nodeType, this.node, "label");
+        },
         progress(): number {
             return getNodeTypeProperty(this.nodeType, this.node, "progress") || 0;
         },
@@ -263,8 +307,14 @@ export default defineComponent({
         mouseLeave() {
             this.hovering = false;
         },
-        performAction(action: BoardNodeAction) {
+        performAction(e: MouseEvent, action: BoardNodeAction) {
             action.onClick(this.node);
+            // If the onClick function made this action selected,
+            // don't propagate the event (which will deselect everything)
+            if (this.board.selectedAction === action) {
+                e.preventDefault();
+                e.stopPropagation();
+            }
         }
     },
     watch: {
@@ -295,11 +345,13 @@ export default defineComponent({
     transform: rotate(-90deg);
 }
 
-.action:hover circle {
+.action:hover circle,
+.action.selected circle {
     r: 25;
 }
 
-.action:hover text {
+.action:hover text,
+.action.selected text {
     font-size: 187.5%; /* 150% * 1.25 */
 }
 
@@ -307,6 +359,29 @@ export default defineComponent({
     text-anchor: middle;
     dominant-baseline: central;
 }
+
+.fade-enter-from,
+.fade-leave-to {
+    opacity: 0;
+}
+
+.pulsing {
+    animation: pulsing 2s ease-in infinite;
+}
+
+@keyframes pulsing {
+    0% {
+        opacity: 0.25;
+    }
+
+    50% {
+        opacity: 1;
+    }
+
+    100% {
+        opacity: 0.25;
+    }
+}
 </style>
 
 <style>
diff --git a/src/components/tree/BranchLine.vue b/src/components/tree/BranchLine.vue
index 21fe68c..6eff98c 100644
--- a/src/components/tree/BranchLine.vue
+++ b/src/components/tree/BranchLine.vue
@@ -37,11 +37,11 @@ export default defineComponent({
             }
             return this.options.stroke!;
         },
-        strokeWidth(): string {
-            if (typeof this.options === "string" || !("stroke-width" in this.options)) {
-                return "15px";
+        strokeWidth(): number | string {
+            if (typeof this.options === "string" || !("strokeWidth" in this.options)) {
+                return "15";
             }
-            return this.options["stroke-width"]!;
+            return this.options["strokeWidth"]!;
         },
         startPosition(): Position {
             const position = { x: this.startNode.x || 0, y: this.startNode.y || 0 };
diff --git a/src/data/layers/Main.vue b/src/data/layers/Main.vue
index 84d1fec..2780aaa 100644
--- a/src/data/layers/Main.vue
+++ b/src/data/layers/Main.vue
@@ -59,8 +59,16 @@ export default defineComponent(function Main() {
                     camelToTitle((node.data as ActionNodeData).actionType)
                 );
                 body.value = coerceComponent(
-                    "<div><div>" +
-                        (node.data as ActionNodeData).log.join("</div><div>") +
+                    "<div><div class='entry'>" +
+                        (node.data as ActionNodeData).log
+                            .map(log => {
+                                let display = log.description;
+                                if (log.effectDescription) {
+                                    display += `<div style="font-style: italic;">${log.effectDescription}</div>`;
+                                }
+                                return display;
+                            })
+                            .join("</div><div class='entry'>") +
                         "</div></div>"
                 );
                 break;
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index d15d95c..03effcc 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -1,8 +1,10 @@
 import { ProgressDisplay, Shape } from "@/game/enums";
+import { layers } from "@/game/layers";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
 import { camelToTitle } from "@/util/common";
+import { getUniqueNodeID } from "@/util/features";
 import themes from "../themes";
 import Main from "./Main.vue";
 
@@ -19,9 +21,64 @@ export type ItemNodeData = {
 
 export type ActionNodeData = {
     actionType: string;
-    log: string[];
+    log: LogEntry[];
 };
 
+export type LogEntry = {
+    description: string;
+    effectDescription?: string;
+};
+
+export type WeightedEvent = {
+    event: () => LogEntry;
+    weight: number;
+};
+
+const redditEvents = [
+    {
+        event: () => ({ description: "You blink and half an hour has passed before you know it." }),
+        weight: 1
+    },
+    {
+        event: () => {
+            const id = getUniqueNodeID(layers.main.boards!.data.main);
+            player.layers.main.boards.main.nodes.push({
+                id,
+                position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space
+                type: "item",
+                data: {
+                    itemType: "speed",
+                    amount: new Decimal(15 * 60)
+                }
+            });
+            return {
+                description: "You found some funny memes and actually feel a bit refreshed.",
+                effectDescription: `Added <span style="color: #0FF;">Speed</span> node`
+            };
+        },
+        weight: 0.5
+    }
+];
+
+function getRandomEvent(events: WeightedEvent[]): LogEntry | null {
+    if (events.length === 0) {
+        return null;
+    }
+    const totalWeight = events.reduce((acc, curr) => acc + curr.weight, 0);
+    const random = Math.random() * totalWeight;
+
+    let weight = 0;
+    for (const outcome of events) {
+        weight += outcome.weight;
+        if (random <= weight) {
+            return outcome.event();
+        }
+    }
+
+    // Should never reach here
+    return null;
+}
+
 export default {
     id: "main",
     display: Main,
@@ -80,6 +137,21 @@ export default {
                         title(node) {
                             return (node.data as ResourceNodeData).resourceType;
                         },
+                        label(node) {
+                            if (player.layers[this.layer].boards[this.id].selectedAction == null) {
+                                return null;
+                            }
+                            const action = player.layers[this.layer].boards[this.id].selectedAction;
+                            switch (action) {
+                                default:
+                                    return null;
+                                case "reddit":
+                                    if ((node.data as ResourceNodeData).resourceType === "time") {
+                                        return { text: "30m", color: "red", pulsing: true };
+                                    }
+                                    return null;
+                            }
+                        },
                         draggable: true,
                         progress(node) {
                             const data = node.data as ResourceNodeData;
@@ -109,6 +181,10 @@ export default {
                                 otherNode
                             );
                             player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
+                            (node.data as ResourceNodeData).amount = Decimal.add(
+                                (node.data as ResourceNodeData).amount,
+                                (otherNode.data as ItemNodeData).amount
+                            );
                         }
                     },
                     item: {
@@ -134,6 +210,9 @@ export default {
                             {
                                 id: "info",
                                 icon: "history_edu",
+                                fillColor() {
+                                    return themes[player.theme].variables["--separator"];
+                                },
                                 tooltip: "Log",
                                 onClick(node) {
                                     player.layers.main.openNode = node.id;
@@ -145,7 +224,44 @@ export default {
                                 icon: "reddit",
                                 tooltip: "Browse Reddit",
                                 onClick(node) {
-                                    // TODO
+                                    if (player.layers.main.boards.main.selectedAction === this.id) {
+                                        const timeNode = player.layers.main.boards.main.nodes.find(
+                                            node =>
+                                                node.type === "resource" &&
+                                                (node.data as ResourceNodeData).resourceType ===
+                                                    "time"
+                                        );
+                                        if (timeNode) {
+                                            (timeNode.data as ResourceNodeData).amount = Decimal.sub(
+                                                (timeNode.data as ResourceNodeData).amount,
+                                                30 * 60
+                                            );
+                                            player.layers.main.boards.main.selectedAction = null;
+                                            (node.data as ActionNodeData).log.push(
+                                                getRandomEvent(redditEvents)!
+                                            );
+                                        }
+                                    } else {
+                                        player.layers.main.boards.main.selectedAction = this.id;
+                                    }
+                                },
+                                links(node) {
+                                    return [
+                                        {
+                                            // TODO this is ridiculous and needs some utility
+                                            //  function to shrink it down
+                                            from: player.layers.main.boards.main.nodes.find(
+                                                node =>
+                                                    node.type === "resource" &&
+                                                    (node.data as ResourceNodeData).resourceType ===
+                                                        "time"
+                                            ),
+                                            to: node,
+                                            stroke: "red",
+                                            "stroke-width": 4,
+                                            pulsing: true
+                                        }
+                                    ];
                                 }
                             }
                         ]
diff --git a/src/data/mod.ts b/src/data/mod.ts
index 1c3ef96..3035aee 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -1,6 +1,7 @@
 import { RawLayer } from "@/typings/layer";
 import { PlayerData } from "@/typings/player";
 import Decimal from "@/util/bignum";
+import { hardReset } from "@/util/save";
 import { computed } from "vue";
 import main from "./layers/main";
 
diff --git a/src/game/layers.ts b/src/game/layers.ts
index a1b62c1..495aa50 100644
--- a/src/game/layers.ts
+++ b/src/game/layers.ts
@@ -450,10 +450,26 @@ export function addLayer(layer: RawLayer, player?: Partial<PlayerData>): void {
                 if (nodeType.actions === null) {
                     return null;
                 }
-                if (typeof nodeType.actions === "function") {
-                    return nodeType.actions(this.selectedNode);
+                const actions =
+                    typeof nodeType.actions === "function"
+                        ? nodeType.actions(this.selectedNode)
+                        : nodeType.actions;
+                return actions?.find(
+                    action =>
+                        action.id === playerProxy.layers[this.layer].boards[this.id].selectedAction
+                );
+            });
+            setDefault(layer.boards.data[id], "links", function() {
+                if (this.selectedAction == null) {
+                    return null;
                 }
-                return nodeType.actions;
+                if (this.selectedAction.links) {
+                    if (typeof this.selectedAction.links === "function") {
+                        return this.selectedAction.links(this.selectedNode);
+                    }
+                    return this.selectedAction.links;
+                }
+                return null;
             });
             for (const nodeType in layer.boards.data[id].types) {
                 layer.boards.data[id].types[nodeType].layer = layer.id;
diff --git a/src/typings/branches.d.ts b/src/typings/branches.d.ts
index 93b263b..cfd0ee6 100644
--- a/src/typings/branches.d.ts
+++ b/src/typings/branches.d.ts
@@ -17,9 +17,10 @@ export interface BranchOptions {
     target?: string;
     featureType?: string;
     stroke?: string;
-    "stroke-width"?: string;
+    strokeWidth?: number | string;
     startOffset?: Position;
     endOffset?: Position;
+    [key: string]: any;
 }
 
 export interface Position {
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index a10d348..f5d716c 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -4,7 +4,7 @@ import { State } from "../state";
 import { Feature, RawFeature } from "./feature";
 
 export interface BoardNode {
-    id: string;
+    id: number;
     position: {
         x: number;
         y: number;
@@ -28,6 +28,7 @@ export interface Board extends Feature {
     nodes: BoardNode[];
     selectedNode: BoardNode | null;
     selectedAction: BoardNodeAction | null;
+    links: BoardNodeLink[] | null;
 }
 
 export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
@@ -37,7 +38,8 @@ export type RawBoard = Omit<RawFeature<Board>, "types" | "startNodes"> & {
 
 export interface NodeType extends Feature {
     title: string | ((node: BoardNode) => string);
-    size: number | ((node: BoardNode) => number);
+    label?: NodeLabel | null | ((node: BoardNode) => NodeLabel | null);
+    size: number | string | ((node: BoardNode) => number | string);
     draggable: boolean | ((node: BoardNode) => boolean);
     shape: Shape | ((node: BoardNode) => Shape);
     canAccept: boolean | ((node: BoardNode, otherNode: BoardNode) => boolean);
@@ -57,7 +59,24 @@ export interface NodeType extends Feature {
 
 export interface BoardNodeAction {
     id: string;
-    icon: string;
-    tooltip: string;
+    icon: string | ((node: BoardNode) => string);
+    fillColor?: string | ((node: BoardNode) => string);
+    tooltip: string | ((node: BoardNode) => string);
     onClick: (node: BoardNode) => void;
+    links?: BoardNodeLink[] | ((node: BoardNode) => BoardNodeLink[]);
+}
+
+export interface BoardNodeLink {
+    from: BoardNode;
+    to: BoardNode;
+    stroke: string;
+    strokeWidth: number | string;
+    pulsing?: boolean;
+    [key: string]: any;
+}
+
+export interface NodeLabel {
+    text: string;
+    color?: string;
+    pulsing?: boolean;
 }
diff --git a/src/util/features.ts b/src/util/features.ts
index 40a6809..18794d9 100644
--- a/src/util/features.ts
+++ b/src/util/features.ts
@@ -1,5 +1,5 @@
 import { layers } from "@/game/layers";
-import { NodeType, BoardNode } from "@/typings/features/board";
+import { NodeType, BoardNode, Board } from "@/typings/features/board";
 import { GridCell } from "@/typings/features/grid";
 import { State } from "@/typings/state";
 import Decimal, { DecimalSource } from "@/util/bignum";
@@ -107,3 +107,13 @@ export function getNodeTypeProperty<T, S extends NodeType, R extends keyof S>(
         ? (nodeType[property] as (node: BoardNode) => T)(node)
         : (nodeType[property] as T);
 }
+
+export function getUniqueNodeID(board: Board): number {
+    let id = 0;
+    board.nodes.forEach(node => {
+        if (node.id >= id) {
+            id = node.id + 1;
+        }
+    });
+    return id;
+}
diff --git a/src/util/save.ts b/src/util/save.ts
index 45ef182..0b14d81 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -187,7 +187,7 @@ window.onbeforeunload = () => {
     }
 };
 window.save = save;
-window.hardReset = async () => {
+export const hardReset = window.hardReset = async () => {
     await loadSave(newSave());
     const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!))));
     modData.active = player.id;

From bb549044647b68685b54ee98dff7f9f15b8d037b Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 22 Aug 2021 23:16:14 -0500
Subject: [PATCH 20/49] Fixed mobile not being able to click actions

---
 src/components/board/BoardNode.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index fb5405d..0a370da 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -21,6 +21,7 @@
                         )`
                     "
                     @mousedown="e => performAction(e, action)"
+                    @touchstart="e => performAction(e, action)"
                 >
                     <circle
                         :fill="

From 040fcfdf5f919285aaf9355da9a7901b76af8715 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 23 Aug 2021 23:54:49 -0500
Subject: [PATCH 21/49] Fixed formatTime

---
 src/util/break_eternity.ts | 48 +++++++++++++++++---------------------
 1 file changed, 21 insertions(+), 27 deletions(-)

diff --git a/src/util/break_eternity.ts b/src/util/break_eternity.ts
index 9a92169..003d807 100644
--- a/src/util/break_eternity.ts
+++ b/src/util/break_eternity.ts
@@ -114,48 +114,42 @@ export function formatWhole(num: DecimalSource): string {
     return format(num, 0);
 }
 
-export function formatTime(s: DecimalSource): string {
-    if (Decimal.gt(s, 2 ^ 51)) {
+export function formatTime(seconds: DecimalSource): string {
+    if (Decimal.gt(seconds, 2 ** 51)) {
         // integer precision limit
-        return format(Decimal.div(s, 31536000)) + "y";
+        return format(Decimal.div(seconds, 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) {
+    seconds = new Decimal(seconds).toNumber();
+    if (seconds < 60) {
+        return format(seconds) + "s";
+    } else if (seconds < 3600) {
+        return formatWhole(Math.floor(seconds / 60)) + "m " + format(seconds % 60) + "s";
+    } else if (seconds < 86400) {
         return (
-            formatWhole(Math.floor(s / 3600)) +
+            formatWhole(Math.floor(seconds / 3600)) +
             "h " +
-            formatWhole(Math.floor(s / 60) % 60) +
+            formatWhole(Math.floor(seconds / 60) % 60) +
             "m " +
-            format(s % 60) +
+            format(seconds % 60) +
             "s"
         );
-    } else if (s < 31536000) {
+    } else if (seconds < 31536000) {
         return (
-            formatWhole(Math.floor(s / 84600) % 365) +
+            formatWhole(Math.floor(seconds / 84600) % 365) +
             "d " +
-            formatWhole(Math.floor(s / 3600) % 24) +
+            formatWhole(Math.floor(seconds / 3600) % 24) +
             "h " +
-            formatWhole(Math.floor(s / 60) % 60) +
-            "m " +
-            format(s % 60) +
-            "s"
+            formatWhole(Math.floor(seconds / 60) % 60) +
+            "m"
         );
     } else {
         return (
-            formatWhole(Math.floor(s / 31536000)) +
+            formatWhole(Math.floor(seconds / 31536000)) +
             "y " +
-            formatWhole(Math.floor(s / 84600) % 365) +
+            formatWhole(Math.floor(seconds / 84600) % 365) +
             "d " +
-            formatWhole(Math.floor(s / 3600) % 24) +
-            "h " +
-            formatWhole(Math.floor(s / 60) % 60) +
-            "m " +
-            format(s % 60) +
-            "s"
+            formatWhole(Math.floor(seconds / 3600) % 24) +
+            "h"
         );
     }
 }

From dc912ef54d0c1c194d3755dbe315019b089791c1 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 23 Aug 2021 23:55:43 -0500
Subject: [PATCH 22/49] Fixed typing issues with selected node

---
 src/typings/features/board.d.ts | 2 +-
 src/util/layers.ts              | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index f5d716c..234965f 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -15,7 +15,7 @@ export interface BoardNode {
 
 export interface BoardData {
     nodes: BoardNode[];
-    selectedNode: string | null;
+    selectedNode: number | null;
     selectedAction: string | null;
 }
 
diff --git a/src/util/layers.ts b/src/util/layers.ts
index ff4d011..e790ee5 100644
--- a/src/util/layers.ts
+++ b/src/util/layers.ts
@@ -82,7 +82,7 @@ export function getStartingBoards(
               const nodes = boards[curr].startNodes?.() || [];
               acc[curr] = {
                   nodes: nodes.map((node, index) => ({
-                      id: index.toString(),
+                      id: index,
                       ...node
                   })),
                   selectedNode: null,

From 8a38ecf92863bd0f9293fef6c19b2fdccc3d00b2 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Mon, 23 Aug 2021 23:56:21 -0500
Subject: [PATCH 23/49] Added amount displays to resource and item nodes

---
 src/data/layers/main.ts | 37 ++++++++++++++++++++++++++-----------
 1 file changed, 26 insertions(+), 11 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 03effcc..e9ef1ac 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -3,6 +3,8 @@ import { layers } from "@/game/layers";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
+import { formatTime } from "@/util/bignum";
+import { format } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { getUniqueNodeID } from "@/util/features";
 import themes from "../themes";
@@ -47,7 +49,7 @@ const redditEvents = [
                 position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space
                 type: "item",
                 data: {
-                    itemType: "speed",
+                    itemType: "time",
                     amount: new Decimal(15 * 60)
                 }
             });
@@ -118,7 +120,7 @@ export default {
                             position: { x: 0, y: 150 },
                             type: "item",
                             data: {
-                                itemType: "speed",
+                                itemType: "time",
                                 amount: new Decimal(5 * 60 * 60)
                             }
                         },
@@ -138,6 +140,13 @@ export default {
                             return (node.data as ResourceNodeData).resourceType;
                         },
                         label(node) {
+                            if (player.layers[this.layer].boards[this.id].selectedNode == node.id) {
+                                const data = node.data as ResourceNodeData;
+                                if (data.resourceType === "time") {
+                                    return { text: formatTime(data.amount), color: "#0FF3" };
+                                }
+                                return { text: format(data.amount), color: "#0FF3" };
+                            }
                             if (player.layers[this.layer].boards[this.id].selectedAction == null) {
                                 return null;
                             }
@@ -172,10 +181,6 @@ export default {
                         canAccept(node, otherNode) {
                             return otherNode.type === "item";
                         },
-                        onClick(node) {
-                            player.layers.main.openNode = node.id;
-                            player.layers.main.showModal = true;
-                        },
                         onDrop(node, otherNode) {
                             const index = player.layers[this.layer].boards[this.id].nodes.indexOf(
                                 otherNode
@@ -189,11 +194,21 @@ export default {
                     },
                     item: {
                         title(node) {
-                            return (node.data as ItemNodeData).itemType;
+                            switch ((node.data as ItemNodeData).itemType) {
+                                default:
+                                    return null;
+                                case "time":
+                                    return "speed";
+                            }
                         },
-                        onClick(node) {
-                            player.layers.main.openNode = node.id;
-                            player.layers.main.showModal = true;
+                        label(node) {
+                            if (player.layers[this.layer].boards[this.id].selectedNode == node.id) {
+                                const data = node.data as ItemNodeData;
+                                if (data.itemType === "time") {
+                                    return { text: formatTime(data.amount), color: "#0FF3" };
+                                }
+                                return { text: format(data.amount), color: "#0FF3" };
+                            }
                         },
                         draggable: true
                     },
@@ -211,7 +226,7 @@ export default {
                                 id: "info",
                                 icon: "history_edu",
                                 fillColor() {
-                                    return themes[player.theme].variables["--separator"];
+                                    return themes[player.theme].variables["--secondary-background"];
                                 },
                                 tooltip: "Log",
                                 onClick(node) {

From e52b3751c979333ecc5ae8a959164a14abbb08bc Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 00:40:36 -0500
Subject: [PATCH 24/49] Improved look of log

---
 src/data/layers/Main.vue | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/data/layers/Main.vue b/src/data/layers/Main.vue
index 2780aaa..3b238d6 100644
--- a/src/data/layers/Main.vue
+++ b/src/data/layers/Main.vue
@@ -86,3 +86,13 @@ export default defineComponent(function Main() {
     return { title, body, footer, showModal, closeModal, devSpeed, formattedDevSpeed };
 });
 </script>
+
+<style>
+.entry {
+    padding: var(--feature-margin);
+}
+
+.entry:not(:last-child) {
+    border-bottom: solid 4px var(--separator);
+}
+</style>

From 4312b7ac759ba6506e036c2c29187a4cbb1cb604 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 01:23:25 -0500
Subject: [PATCH 25/49] Fixed various node and action selection issues

---
 src/components/board/Board.vue     | 14 ++++++++------
 src/components/board/BoardNode.vue | 13 ++++++++++---
 2 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 1b2eb99..608dd27 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -7,8 +7,8 @@
         @init="onInit"
         @mousemove="drag"
         @touchmove="drag"
-        @mousedown="mouseDown"
-        @touchstart="mouseDown"
+        @mousedown="e => mouseDown(e)"
+        @touchstart="e => mouseDown(e)"
         @mouseup="() => endDragging(dragging)"
         @touchend="() => endDragging(dragging)"
         @mouseleave="() => endDragging(dragging)"
@@ -56,7 +56,7 @@ export default defineComponent({
         } as {
             lastMousePosition: { x: number; y: number };
             dragged: { x: number; y: number };
-            dragging: string | null;
+            dragging: number | null;
             hasDragged: boolean;
         };
     },
@@ -81,7 +81,9 @@ export default defineComponent({
             ];
         },
         draggingNode() {
-            return this.dragging ? this.board.nodes.find(node => node.id === this.dragging) : null;
+            return this.dragging == null
+                ? null
+                : this.board.nodes.find(node => node.id === this.dragging);
         },
         nodes() {
             const nodes = this.board.nodes.slice();
@@ -135,7 +137,7 @@ export default defineComponent({
         onInit(panzoomInstance: any) {
             panzoomInstance.setTransformOrigin(null);
         },
-        mouseDown(e: MouseEvent, nodeID: string | null = null, draggable = false) {
+        mouseDown(e: MouseEvent, nodeID: number | null = null, draggable = false) {
             if (this.dragging == null) {
                 e.preventDefault();
                 e.stopPropagation();
@@ -176,7 +178,7 @@ export default defineComponent({
                 e.stopPropagation();
             }
         },
-        endDragging(nodeID: string | null) {
+        endDragging(nodeID: number | null) {
             if (this.dragging != null && this.dragging === nodeID) {
                 const draggingNode = this.draggingNode!;
                 const receivingNode = this.receivingNode;
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 0a370da..4dd2ed8 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -22,6 +22,8 @@
                     "
                     @mousedown="e => performAction(e, action)"
                     @touchstart="e => performAction(e, action)"
+                    @mouseup="e => actionMouseUp(e, action)"
+                    @touchend.stop="e => actionMouseUp(e, action)"
                 >
                     <circle
                         :fill="
@@ -175,7 +177,6 @@ export default defineComponent({
         return {
             ProgressDisplay,
             Shape,
-            lastMousePosition: { x: 0, y: 0 },
             hovering: false,
             sqrtTwo: Math.sqrt(2)
         };
@@ -211,7 +212,7 @@ export default defineComponent({
             return layers[this.nodeType.layer].boards!.data[this.nodeType.id];
         },
         selected() {
-            return this.board.selectedNode?.id === this.node.id;
+            return this.board.selectedNode === this.node;
         },
         selectedAction() {
             return this.board.selectedAction;
@@ -316,11 +317,17 @@ export default defineComponent({
                 e.preventDefault();
                 e.stopPropagation();
             }
+        },
+        actionMouseUp(e: MouseEvent, action: BoardNodeAction) {
+            if (this.board.selectedAction === action) {
+                e.preventDefault();
+                e.stopPropagation();
+            }
         }
     },
     watch: {
         onDraggableChanged() {
-            if (this.dragging?.id === this.node.id && !this.draggable) {
+            if (this.dragging === this.node && !this.draggable) {
                 this.$emit("endDragging", this.node.id);
             }
         }

From b41e44e87ec61e76eb3d30664b67ee71fd7e876a Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 02:54:42 -0500
Subject: [PATCH 26/49] Implemented links between resource nodes

---
 src/data/layers/Main.vue |  15 +---
 src/data/layers/main.ts  | 182 +++++++++++++++++++++++++++++++++++++--
 2 files changed, 174 insertions(+), 23 deletions(-)

diff --git a/src/data/layers/Main.vue b/src/data/layers/Main.vue
index 3b238d6..c27b588 100644
--- a/src/data/layers/Main.vue
+++ b/src/data/layers/Main.vue
@@ -22,7 +22,7 @@ import { format } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { coerceComponent } from "@/util/vue";
 import { computed, defineComponent, shallowRef, watchEffect } from "vue";
-import { ActionNodeData, ResourceNodeData } from "./main";
+import { ActionNodeData } from "./main";
 
 export default defineComponent(function Main() {
     const title = shallowRef<CoercableComponent | null>(null);
@@ -41,19 +41,6 @@ export default defineComponent(function Main() {
             default:
                 player.layers.main.showModal = false;
                 break;
-            case "resource":
-                switch ((node.data as ResourceNodeData).resourceType) {
-                    default:
-                        player.layers.main.showModal = false;
-                        break;
-                    case "time":
-                        title.value = coerceComponent("<h2>Time</h2>");
-                        body.value = coerceComponent(
-                            "The ultimate resource, that you'll never have enough of."
-                        );
-                        break;
-                }
-                break;
             case "action":
                 title.value = coerceComponent(
                     camelToTitle((node.data as ActionNodeData).actionType)
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index e9ef1ac..21da979 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -4,9 +4,10 @@ import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
 import { RawLayer } from "@/typings/layer";
 import { formatTime } from "@/util/bignum";
-import { format } from "@/util/break_eternity";
+import { format, formatWhole } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { getUniqueNodeID } from "@/util/features";
+import { watch } from "vue";
 import themes from "../themes";
 import Main from "./Main.vue";
 
@@ -81,6 +82,71 @@ function getRandomEvent(events: WeightedEvent[]): LogEntry | null {
     return null;
 }
 
+enum LinkType {
+    LossOnly,
+    GainOnly,
+    Both
+}
+
+// Links cause gain/loss of one resource to also affect other resources
+const links = {
+    time: [
+        { resource: "social", amount: 1 / 60, linkType: LinkType.LossOnly },
+        { resource: "mental", amount: 1 / 120, linkType: LinkType.LossOnly }
+    ]
+} as Record<
+    string,
+    {
+        resource: string;
+        amount: DecimalSource;
+        linkType: LinkType;
+    }[]
+>;
+
+for (const resource in links) {
+    const resourceLinks = links[resource];
+    watch(
+        () =>
+            (player.layers.main?.boards.main.nodes.find(
+                node =>
+                    node.type === "resource" &&
+                    (node.data as ResourceNodeData).resourceType === resource
+            )?.data as ResourceNodeData | null)?.amount,
+        (amount, oldAmount) => {
+            if (amount == null || oldAmount == null) {
+                return;
+            }
+            const resourceGain = Decimal.sub(amount, oldAmount);
+            resourceLinks.forEach(link => {
+                switch (link.linkType) {
+                    case LinkType.LossOnly:
+                        if (Decimal.gt(amount, oldAmount)) {
+                            return;
+                        }
+                        break;
+                    case LinkType.GainOnly:
+                        if (Decimal.lt(amount, oldAmount)) {
+                            return;
+                        }
+                        break;
+                }
+                const node = player.layers.main.boards.main.nodes.find(
+                    node =>
+                        node.type === "resource" &&
+                        (node.data as ResourceNodeData).resourceType === link.resource
+                );
+                if (node) {
+                    const data = node.data as ResourceNodeData;
+                    data.amount = Decimal.add(
+                        data.amount,
+                        Decimal.times(link.amount, resourceGain)
+                    ).clamp(0, data.maxAmount);
+                }
+            });
+        }
+    );
+}
+
 export default {
     id: "main",
     display: Main,
@@ -116,6 +182,33 @@ export default {
                                 maxAmount: new Decimal(24 * 60 * 60)
                             }
                         },
+                        {
+                            position: { x: 0, y: 0 },
+                            type: "resource",
+                            data: {
+                                resourceType: "mental",
+                                amount: new Decimal(100),
+                                maxAmount: new Decimal(100)
+                            }
+                        },
+                        {
+                            position: { x: 0, y: 0 },
+                            type: "resource",
+                            data: {
+                                resourceType: "social",
+                                amount: new Decimal(100),
+                                maxAmount: new Decimal(100)
+                            }
+                        },
+                        {
+                            position: { x: 0, y: 0 },
+                            type: "resource",
+                            data: {
+                                resourceType: "focus",
+                                amount: new Decimal(100),
+                                maxAmount: new Decimal(100)
+                            }
+                        },
                         {
                             position: { x: 0, y: 150 },
                             type: "item",
@@ -145,8 +238,48 @@ export default {
                                 if (data.resourceType === "time") {
                                     return { text: formatTime(data.amount), color: "#0FF3" };
                                 }
+                                if (Decimal.eq(data.maxAmount, 100)) {
+                                    return { text: formatWhole(data.amount) + "%", color: "#0FF3" };
+                                }
                                 return { text: format(data.amount), color: "#0FF3" };
                             }
+                            if (player.layers[this.layer].boards[this.id].selectedNode == null) {
+                                return null;
+                            }
+                            const selectedNode = layers[this.layer].boards!.data[this.id]
+                                .selectedNode;
+                            if (selectedNode.type === "resource") {
+                                const data = selectedNode.data as ResourceNodeData;
+                                if (data.resourceType in links) {
+                                    const link = links[data.resourceType].find(
+                                        link =>
+                                            link.resource ===
+                                            (node.data as ResourceNodeData).resourceType
+                                    );
+                                    if (link) {
+                                        let text;
+                                        if (
+                                            (node.data as ResourceNodeData).resourceType === "time"
+                                        ) {
+                                            text = formatTime(link.amount);
+                                        } else if (
+                                            Decimal.eq(
+                                                (node.data as ResourceNodeData).maxAmount,
+                                                100
+                                            )
+                                        ) {
+                                            text = formatWhole(link.amount) + "%";
+                                        } else {
+                                            text = format(link.amount);
+                                        }
+                                        let negativeLink = Decimal.lt(link.amount, 0);
+                                        if (link.linkType === LinkType.LossOnly) {
+                                            negativeLink = !negativeLink;
+                                        }
+                                        return { text, color: negativeLink ? "red" : "green" };
+                                    }
+                                }
+                            }
                             if (player.layers[this.layer].boards[this.id].selectedAction == null) {
                                 return null;
                             }
@@ -170,13 +303,7 @@ export default {
                             return themes[player.theme].variables["--background"];
                         },
                         progressColor(node) {
-                            const data = node.data as ResourceNodeData;
-                            switch (data.resourceType) {
-                                case "time":
-                                    return "#0FF3";
-                                default:
-                                    return "none";
-                            }
+                            return "#0FF3";
                         },
                         canAccept(node, otherNode) {
                             return otherNode.type === "item";
@@ -189,7 +316,7 @@ export default {
                             (node.data as ResourceNodeData).amount = Decimal.add(
                                 (node.data as ResourceNodeData).amount,
                                 (otherNode.data as ItemNodeData).amount
-                            );
+                            ).min((node.data as ResourceNodeData).maxAmount);
                         }
                     },
                     item: {
@@ -281,6 +408,43 @@ export default {
                             }
                         ]
                     }
+                },
+                links() {
+                    if (this.selectedAction?.links) {
+                        if (typeof this.selectedAction!.links === "function") {
+                            return this.selectedAction!.links(this.selectedNode);
+                        }
+                        return this.selectedAction!.links;
+                    }
+                    if (player.layers[this.layer].boards[this.id].selectedNode == null) {
+                        return null;
+                    }
+                    const selectedNode = layers[this.layer].boards!.data[this.id].selectedNode;
+                    if (selectedNode.type === "resource") {
+                        const data = selectedNode.data as ResourceNodeData;
+                        if (data.resourceType in links) {
+                            return links[data.resourceType].map(link => {
+                                const node = player.layers.main.boards.main.nodes.find(
+                                    node =>
+                                        node.type === "resource" &&
+                                        (node.data as ResourceNodeData).resourceType ===
+                                            link.resource
+                                );
+                                let negativeLink = Decimal.lt(link.amount, 0);
+                                if (link.linkType === LinkType.LossOnly) {
+                                    negativeLink = !negativeLink;
+                                }
+                                return {
+                                    from: selectedNode,
+                                    to: node,
+                                    stroke: negativeLink ? "red" : "green",
+                                    "stroke-width": 4,
+                                    pulsing: true
+                                };
+                            });
+                        }
+                    }
+                    return null;
                 }
             }
         }

From a7009e416e114fd899ed1082472c99a6f224edcc Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 08:18:55 -0500
Subject: [PATCH 27/49] Implemented pinning resource amounts

---
 src/components/board/BoardNode.vue |  3 +-
 src/data/layers/main.ts            | 84 +++++++++++++++++++-----------
 src/typings/features/board.d.ts    |  3 +-
 src/util/proxies.ts                |  2 +-
 4 files changed, 59 insertions(+), 33 deletions(-)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 4dd2ed8..5936777 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -310,10 +310,9 @@ export default defineComponent({
             this.hovering = false;
         },
         performAction(e: MouseEvent, action: BoardNodeAction) {
-            action.onClick(this.node);
             // If the onClick function made this action selected,
             // don't propagate the event (which will deselect everything)
-            if (this.board.selectedAction === action) {
+            if (action.onClick(this.node) || this.board.selectedAction === action) {
                 e.preventDefault();
                 e.stopPropagation();
             }
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 21da979..1d34797 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -2,6 +2,7 @@ import { ProgressDisplay, Shape } from "@/game/enums";
 import { layers } from "@/game/layers";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
+import { BoardNodeAction } from "@/typings/features/board";
 import { RawLayer } from "@/typings/layer";
 import { formatTime } from "@/util/bignum";
 import { format, formatWhole } from "@/util/break_eternity";
@@ -147,6 +148,22 @@ for (const resource in links) {
     );
 }
 
+const pinAction = {
+    id: "pin",
+    icon: "push_pin",
+    fillColor(node) {
+        if (node.pinned) {
+            return themes[player.theme].variables["--bought"];
+        }
+        return themes[player.theme].variables["--secondary-background"];
+    },
+    tooltip: "Always show resource",
+    onClick(node) {
+        node.pinned = !node.pinned;
+        return true;
+    }
+} as BoardNodeAction;
+
 export default {
     id: "main",
     display: Main,
@@ -233,22 +250,29 @@ export default {
                             return (node.data as ResourceNodeData).resourceType;
                         },
                         label(node) {
-                            if (player.layers[this.layer].boards[this.id].selectedNode == node.id) {
-                                const data = node.data as ResourceNodeData;
-                                if (data.resourceType === "time") {
-                                    return { text: formatTime(data.amount), color: "#0FF3" };
-                                }
-                                if (Decimal.eq(data.maxAmount, 100)) {
-                                    return { text: formatWhole(data.amount) + "%", color: "#0FF3" };
-                                }
-                                return { text: format(data.amount), color: "#0FF3" };
-                            }
-                            if (player.layers[this.layer].boards[this.id].selectedNode == null) {
-                                return null;
-                            }
                             const selectedNode = layers[this.layer].boards!.data[this.id]
                                 .selectedNode;
-                            if (selectedNode.type === "resource") {
+                            if (
+                                selectedNode != node &&
+                                player.layers[this.layer].boards[this.id].selectedAction != null
+                            ) {
+                                const action =
+                                    player.layers[this.layer].boards[this.id].selectedAction;
+                                switch (action) {
+                                    case "reddit":
+                                        if (
+                                            (node.data as ResourceNodeData).resourceType === "time"
+                                        ) {
+                                            return { text: "30m", color: "red", pulsing: true };
+                                        }
+                                        break;
+                                }
+                            }
+                            if (
+                                selectedNode != node &&
+                                selectedNode != null &&
+                                selectedNode.type === "resource"
+                            ) {
                                 const data = selectedNode.data as ResourceNodeData;
                                 if (data.resourceType in links) {
                                     const link = links[data.resourceType].find(
@@ -280,18 +304,15 @@ export default {
                                     }
                                 }
                             }
-                            if (player.layers[this.layer].boards[this.id].selectedAction == null) {
-                                return null;
-                            }
-                            const action = player.layers[this.layer].boards[this.id].selectedAction;
-                            switch (action) {
-                                default:
-                                    return null;
-                                case "reddit":
-                                    if ((node.data as ResourceNodeData).resourceType === "time") {
-                                        return { text: "30m", color: "red", pulsing: true };
-                                    }
-                                    return null;
+                            if (selectedNode == node || node.pinned) {
+                                const data = node.data as ResourceNodeData;
+                                if (data.resourceType === "time") {
+                                    return { text: formatTime(data.amount), color: "#0FF3" };
+                                }
+                                if (Decimal.eq(data.maxAmount, 100)) {
+                                    return { text: formatWhole(data.amount) + "%", color: "#0FF3" };
+                                }
+                                return { text: format(data.amount), color: "#0FF3" };
                             }
                         },
                         draggable: true,
@@ -317,7 +338,8 @@ export default {
                                 (node.data as ResourceNodeData).amount,
                                 (otherNode.data as ItemNodeData).amount
                             ).min((node.data as ResourceNodeData).maxAmount);
-                        }
+                        },
+                        actions: [pinAction]
                     },
                     item: {
                         title(node) {
@@ -329,7 +351,10 @@ export default {
                             }
                         },
                         label(node) {
-                            if (player.layers[this.layer].boards[this.id].selectedNode == node.id) {
+                            if (
+                                player.layers[this.layer].boards[this.id].selectedNode == node.id ||
+                                node.pinned
+                            ) {
                                 const data = node.data as ItemNodeData;
                                 if (data.itemType === "time") {
                                     return { text: formatTime(data.amount), color: "#0FF3" };
@@ -337,7 +362,8 @@ export default {
                                 return { text: format(data.amount), color: "#0FF3" };
                             }
                         },
-                        draggable: true
+                        draggable: true,
+                        actions: [pinAction]
                     },
                     action: {
                         title(node) {
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 234965f..69d462e 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -11,6 +11,7 @@ export interface BoardNode {
     };
     type: string;
     data?: State;
+    pinned?: boolean;
 }
 
 export interface BoardData {
@@ -62,7 +63,7 @@ export interface BoardNodeAction {
     icon: string | ((node: BoardNode) => string);
     fillColor?: string | ((node: BoardNode) => string);
     tooltip: string | ((node: BoardNode) => string);
-    onClick: (node: BoardNode) => void;
+    onClick: (node: BoardNode) => boolean | undefined;
     links?: BoardNodeLink[] | ((node: BoardNode) => BoardNodeLink[]);
 }
 
diff --git a/src/util/proxies.ts b/src/util/proxies.ts
index a541eeb..52b32ea 100644
--- a/src/util/proxies.ts
+++ b/src/util/proxies.ts
@@ -30,7 +30,7 @@ function travel(
     objectProxy: Record<string, any>
 ) {
     for (const key in object) {
-        if (object[key] == undefined || object[key].isProxy) {
+        if (object[key] == undefined || object[key].isProxy || isRef(object[key])) {
             continue;
         }
         if (isFunction(object[key])) {

From d2e0ab29f28c681ecd5ae046455386eacc0ee08d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 19:19:55 -0500
Subject: [PATCH 28/49] Various mobile fixes

---
 src/components/board/Board.vue     | 43 ++++++++++++++++++++++++------
 src/components/board/BoardNode.vue |  6 ++---
 src/components/system/Nav.vue      |  4 +++
 src/data/layers/main.ts            |  1 +
 src/main.css                       |  1 -
 5 files changed, 43 insertions(+), 12 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 608dd27..d01789e 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -137,14 +137,26 @@ export default defineComponent({
         onInit(panzoomInstance: any) {
             panzoomInstance.setTransformOrigin(null);
         },
-        mouseDown(e: MouseEvent, nodeID: number | null = null, draggable = false) {
+        mouseDown(e: MouseEvent | TouchEvent, nodeID: number | null = null, draggable = false) {
             if (this.dragging == null) {
                 e.preventDefault();
                 e.stopPropagation();
 
+                let clientX, clientY;
+                if ("touches" in e) {
+                    if (e.touches.length === 1) {
+                        clientX = e.touches[0].clientX;
+                        clientY = e.touches[0].clientY;
+                    } else {
+                        return;
+                    }
+                } else {
+                    clientX = e.clientX;
+                    clientY = e.clientY;
+                }
                 this.lastMousePosition = {
-                    x: e.clientX,
-                    y: e.clientY
+                    x: clientX,
+                    y: clientY
                 };
                 this.dragged = { x: 0, y: 0 };
                 this.hasDragged = false;
@@ -158,15 +170,30 @@ export default defineComponent({
                 player.layers[this.layer].boards[this.id].selectedAction = null;
             }
         },
-        drag(e: MouseEvent) {
+        drag(e: MouseEvent | TouchEvent) {
             const zoom = (this.getZoomLevel as () => number)();
+
+            let clientX, clientY;
+            if ("touches" in e) {
+                if (e.touches.length === 1) {
+                    clientX = e.touches[0].clientX;
+                    clientY = e.touches[0].clientY;
+                } else {
+                    this.endDragging(this.dragging);
+                    return;
+                }
+            } else {
+                clientX = e.clientX;
+                clientY = e.clientY;
+            }
+
             this.dragged = {
-                x: this.dragged.x + (e.clientX - this.lastMousePosition.x) / zoom,
-                y: this.dragged.y + (e.clientY - this.lastMousePosition.y) / zoom
+                x: this.dragged.x + (clientX - this.lastMousePosition.x) / zoom,
+                y: this.dragged.y + (clientY - this.lastMousePosition.y) / zoom
             };
             this.lastMousePosition = {
-                x: e.clientX,
-                y: e.clientY
+                x: clientX,
+                y: clientY
             };
 
             if (Math.abs(this.dragged.x) > 10 || Math.abs(this.dragged.y) > 10) {
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 5936777..3ca807d 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -295,7 +295,7 @@ export default defineComponent({
         }
     },
     methods: {
-        mouseDown(e: MouseEvent) {
+        mouseDown(e: MouseEvent | TouchEvent) {
             this.$emit("mouseDown", e, this.node.id, this.draggable);
         },
         mouseUp() {
@@ -309,7 +309,7 @@ export default defineComponent({
         mouseLeave() {
             this.hovering = false;
         },
-        performAction(e: MouseEvent, action: BoardNodeAction) {
+        performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
             // If the onClick function made this action selected,
             // don't propagate the event (which will deselect everything)
             if (action.onClick(this.node) || this.board.selectedAction === action) {
@@ -317,7 +317,7 @@ export default defineComponent({
                 e.stopPropagation();
             }
         },
-        actionMouseUp(e: MouseEvent, action: BoardNodeAction) {
+        actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
             if (this.board.selectedAction === action) {
                 e.preventDefault();
                 e.stopPropagation();
diff --git a/src/components/system/Nav.vue b/src/components/system/Nav.vue
index 8e54b17..93a61b3 100644
--- a/src/components/system/Nav.vue
+++ b/src/components/system/Nav.vue
@@ -139,6 +139,7 @@ export default defineComponent({
     width: 46px;
     display: flex;
     cursor: pointer;
+    flex-shrink: 0;
 }
 
 .overlay-nav {
@@ -169,6 +170,9 @@ export default defineComponent({
 
 .nav > .title {
     width: unset;
+    flex-shrink: 1;
+    overflow: hidden;
+    white-space: nowrap;
 }
 
 .nav .saves,
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 1d34797..fdac1c1 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -167,6 +167,7 @@ const pinAction = {
 export default {
     id: "main",
     display: Main,
+    minWidth: undefined,
     startData() {
         return {
             openNode: null,
diff --git a/src/main.css b/src/main.css
index e204b96..519e0e2 100644
--- a/src/main.css
+++ b/src/main.css
@@ -16,7 +16,6 @@
 
 body {
 	overflow: hidden;
-	min-width: 640px;
 	transition: none;
 	text-align: center;
 }

From e90aac51f2826e0f1ac6899aa2e3e0cd00ded75a Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Tue, 24 Aug 2021 19:48:07 -0500
Subject: [PATCH 29/49] save before loading new save

---
 src/components/system/SavesManager.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/system/SavesManager.vue b/src/components/system/SavesManager.vue
index bfbcb5a..93073e4 100644
--- a/src/components/system/SavesManager.vue
+++ b/src/components/system/SavesManager.vue
@@ -181,6 +181,7 @@ export default defineComponent({
         },
         openSave(id: string) {
             this.saves[player.id].time = player.time;
+            save();
             loadSave(this.saves[id]);
             const modData = JSON.parse(
                 decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!)))

From e642b0e42093688a65f697d2bf07160d3a2e5f5e Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Wed, 25 Aug 2021 00:04:40 -0500
Subject: [PATCH 30/49] Some refactoring of resources

---
 src/data/layers/main.ts         | 300 ++++++++++++++++++--------------
 src/typings/features/board.d.ts |   2 +-
 2 files changed, 174 insertions(+), 128 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index fdac1c1..29d4506 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -2,20 +2,20 @@ import { ProgressDisplay, Shape } from "@/game/enums";
 import { layers } from "@/game/layers";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
-import { BoardNodeAction } from "@/typings/features/board";
+import { BoardNode, BoardNodeAction } from "@/typings/features/board";
 import { RawLayer } from "@/typings/layer";
 import { formatTime } from "@/util/bignum";
 import { format, formatWhole } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { getUniqueNodeID } from "@/util/features";
-import { watch } from "vue";
+import { computed, watch } from "vue";
 import themes from "../themes";
 import Main from "./Main.vue";
 
 export type ResourceNodeData = {
     resourceType: string;
     amount: DecimalSource;
-    maxAmount: DecimalSource;
+    [key: string]: any;
 };
 
 export type ItemNodeData = {
@@ -92,8 +92,8 @@ enum LinkType {
 // Links cause gain/loss of one resource to also affect other resources
 const links = {
     time: [
-        { resource: "social", amount: 1 / 60, linkType: LinkType.LossOnly },
-        { resource: "mental", amount: 1 / 120, linkType: LinkType.LossOnly }
+        { resource: "social", amount: 1 / (60 * 60), linkType: LinkType.LossOnly },
+        { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly }
     ]
 } as Record<
     string,
@@ -107,41 +107,25 @@ const links = {
 for (const resource in links) {
     const resourceLinks = links[resource];
     watch(
-        () =>
-            (player.layers.main?.boards.main.nodes.find(
-                node =>
-                    node.type === "resource" &&
-                    (node.data as ResourceNodeData).resourceType === resource
-            )?.data as ResourceNodeData | null)?.amount,
+        () => resources[resource].amount,
         (amount, oldAmount) => {
             if (amount == null || oldAmount == null) {
                 return;
             }
             const resourceGain = Decimal.sub(amount, oldAmount);
             resourceLinks.forEach(link => {
-                switch (link.linkType) {
-                    case LinkType.LossOnly:
-                        if (Decimal.gt(amount, oldAmount)) {
-                            return;
-                        }
-                        break;
-                    case LinkType.GainOnly:
-                        if (Decimal.lt(amount, oldAmount)) {
-                            return;
-                        }
-                        break;
+                if (link.linkType === LinkType.LossOnly && Decimal.gt(amount, oldAmount)) {
+                    return;
                 }
-                const node = player.layers.main.boards.main.nodes.find(
-                    node =>
-                        node.type === "resource" &&
-                        (node.data as ResourceNodeData).resourceType === link.resource
-                );
-                if (node) {
-                    const data = node.data as ResourceNodeData;
-                    data.amount = Decimal.add(
-                        data.amount,
+                if (link.linkType === LinkType.GainOnly && Decimal.lt(amount, oldAmount)) {
+                    return;
+                }
+                const resource = resources[link.resource];
+                if (resource.amount != null) {
+                    resource.amount = Decimal.add(
+                        resource.amount,
                         Decimal.times(link.amount, resourceGain)
-                    ).clamp(0, data.maxAmount);
+                    );
                 }
             });
         }
@@ -164,6 +148,53 @@ const pinAction = {
     }
 } as BoardNodeAction;
 
+type Resource = {
+    readonly name: string;
+    readonly color: string;
+    readonly node: BoardNode | undefined;
+    amount: DecimalSource | undefined;
+    readonly maxAmount: DecimalSource;
+};
+
+const resources = {
+    time: createResource("time", "#3EB48933", 24 * 60 * 60),
+    energy: createResource("energy", "#FFA50033", 100),
+    social: createResource("social", "#80008033", 100),
+    mental: createResource("mental", "#32CD3233", 100),
+    focus: createResource("focus", "#0000FF33", 100)
+} as Record<string, Resource>;
+
+function createResource(name: string, color: string, maxAmount: DecimalSource): Resource {
+    const node = computed(() =>
+        player.layers.main?.boards.main.nodes.find(
+            node =>
+                node.type === "resource" && (node.data as ResourceNodeData).resourceType === name
+        )
+    );
+    const data = computed(() => node.value?.data as ResourceNodeData);
+    return {
+        name,
+        color,
+        get node() {
+            return node.value;
+        },
+        get amount() {
+            return data.value?.amount;
+        },
+        set amount(amount: DecimalSource) {
+            data.value.amount = Decimal.clamp(amount, 0, maxAmount);
+        },
+        maxAmount
+    };
+}
+
+function getResource(node: BoardNode): Resource | undefined {
+    return Object.values(resources).find(resource => resource.node === node);
+}
+
+const selectedNode = computed(() => layers.main?.boards?.data.main.selectedNode);
+const selectedAction = computed(() => layers.main?.boards?.data.main.selectedAction);
+
 export default {
     id: "main",
     display: Main,
@@ -196,43 +227,40 @@ export default {
                             type: "resource",
                             data: {
                                 resourceType: "time",
-                                amount: new Decimal(24 * 60 * 60),
-                                maxAmount: new Decimal(24 * 60 * 60)
+                                amount: new Decimal(24 * 60 * 60)
                             }
                         },
                         {
-                            position: { x: 0, y: 0 },
+                            position: { x: 300, y: 0 },
                             type: "resource",
                             data: {
                                 resourceType: "mental",
-                                amount: new Decimal(100),
-                                maxAmount: new Decimal(100)
+                                amount: new Decimal(100)
                             }
                         },
                         {
-                            position: { x: 0, y: 0 },
+                            position: { x: 150, y: 0 },
                             type: "resource",
                             data: {
                                 resourceType: "social",
-                                amount: new Decimal(100),
-                                maxAmount: new Decimal(100)
+                                amount: new Decimal(100)
                             }
                         },
                         {
-                            position: { x: 0, y: 0 },
+                            position: { x: -150, y: 0 },
                             type: "resource",
                             data: {
                                 resourceType: "focus",
-                                amount: new Decimal(100),
-                                maxAmount: new Decimal(100)
+                                amount: new Decimal(0),
+                                currentFocus: ""
                             }
                         },
                         {
-                            position: { x: 0, y: 150 },
-                            type: "item",
+                            position: { x: -300, y: 0 },
+                            type: "resource",
                             data: {
-                                itemType: "time",
-                                amount: new Decimal(5 * 60 * 60)
+                                resourceType: "energy",
+                                amount: new Decimal(100)
                             }
                         },
                         {
@@ -251,48 +279,52 @@ export default {
                             return (node.data as ResourceNodeData).resourceType;
                         },
                         label(node) {
-                            const selectedNode = layers[this.layer].boards!.data[this.id]
-                                .selectedNode;
-                            if (
-                                selectedNode != node &&
-                                player.layers[this.layer].boards[this.id].selectedAction != null
-                            ) {
-                                const action =
-                                    player.layers[this.layer].boards[this.id].selectedAction;
-                                switch (action) {
+                            const resource = getResource(node)!;
+                            if (selectedNode.value != node && selectedAction.value != null) {
+                                if (resource && resource.name === "focus") {
+                                    const currentFocus =
+                                        (resource.node?.data as ResourceNodeData | undefined)
+                                            ?.currentFocus === selectedAction.value?.id;
+                                    return {
+                                        text: currentFocus ? "10%" : "X",
+                                        color: currentFocus ? "green" : "black",
+                                        pulsing: true
+                                    };
+                                }
+                                switch (selectedAction.value.id) {
                                     case "reddit":
-                                        if (
-                                            (node.data as ResourceNodeData).resourceType === "time"
-                                        ) {
-                                            return { text: "30m", color: "red", pulsing: true };
+                                        switch (resource.name) {
+                                            case "time":
+                                                return {
+                                                    text: "30m",
+                                                    color: "red",
+                                                    pulsing: true
+                                                };
+                                            case "energy":
+                                                return {
+                                                    text: "5%",
+                                                    color: "green",
+                                                    pulsing: true
+                                                };
                                         }
                                         break;
                                 }
                             }
                             if (
-                                selectedNode != node &&
-                                selectedNode != null &&
-                                selectedNode.type === "resource"
+                                selectedNode.value != node &&
+                                selectedNode.value != null &&
+                                selectedNode.value.type === "resource"
                             ) {
-                                const data = selectedNode.data as ResourceNodeData;
-                                if (data.resourceType in links) {
-                                    const link = links[data.resourceType].find(
-                                        link =>
-                                            link.resource ===
-                                            (node.data as ResourceNodeData).resourceType
+                                const selectedResource = getResource(selectedNode.value);
+                                if (selectedResource && selectedResource.name in links) {
+                                    const link = links[selectedResource.name].find(
+                                        link => link.resource === resource.name
                                     );
                                     if (link) {
                                         let text;
-                                        if (
-                                            (node.data as ResourceNodeData).resourceType === "time"
-                                        ) {
+                                        if (resource.name === "time") {
                                             text = formatTime(link.amount);
-                                        } else if (
-                                            Decimal.eq(
-                                                (node.data as ResourceNodeData).maxAmount,
-                                                100
-                                            )
-                                        ) {
+                                        } else if (Decimal.eq(resource.maxAmount, 100)) {
                                             text = formatWhole(link.amount) + "%";
                                         } else {
                                             text = format(link.amount);
@@ -305,12 +337,12 @@ export default {
                                     }
                                 }
                             }
-                            if (selectedNode == node || node.pinned) {
+                            if (selectedNode.value == node || node.pinned) {
                                 const data = node.data as ResourceNodeData;
                                 if (data.resourceType === "time") {
                                     return { text: formatTime(data.amount), color: "#0FF3" };
                                 }
-                                if (Decimal.eq(data.maxAmount, 100)) {
+                                if (Decimal.eq(resource.maxAmount, 100)) {
                                     return { text: formatWhole(data.amount) + "%", color: "#0FF3" };
                                 }
                                 return { text: format(data.amount), color: "#0FF3" };
@@ -318,27 +350,28 @@ export default {
                         },
                         draggable: true,
                         progress(node) {
-                            const data = node.data as ResourceNodeData;
-                            return Decimal.div(data.amount, data.maxAmount).toNumber();
+                            const resource = getResource(node)!;
+                            return Decimal.div(resource.amount || 0, resource.maxAmount).toNumber();
                         },
                         fillColor() {
                             return themes[player.theme].variables["--background"];
                         },
                         progressColor(node) {
-                            return "#0FF3";
+                            return getResource(node)!.color;
                         },
                         canAccept(node, otherNode) {
                             return otherNode.type === "item";
                         },
                         onDrop(node, otherNode) {
+                            const resource = getResource(node)!;
                             const index = player.layers[this.layer].boards[this.id].nodes.indexOf(
                                 otherNode
                             );
                             player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
-                            (node.data as ResourceNodeData).amount = Decimal.add(
-                                (node.data as ResourceNodeData).amount,
+                            resource.amount = Decimal.add(
+                                resource.amount || 0,
                                 (otherNode.data as ItemNodeData).amount
-                            ).min((node.data as ResourceNodeData).maxAmount);
+                            );
                         },
                         actions: [pinAction]
                     },
@@ -352,10 +385,7 @@ export default {
                             }
                         },
                         label(node) {
-                            if (
-                                player.layers[this.layer].boards[this.id].selectedNode == node.id ||
-                                node.pinned
-                            ) {
+                            if (selectedNode.value == node || node.pinned) {
                                 const data = node.data as ItemNodeData;
                                 if (data.itemType === "time") {
                                     return { text: formatTime(data.amount), color: "#0FF3" };
@@ -393,23 +423,34 @@ export default {
                                 icon: "reddit",
                                 tooltip: "Browse Reddit",
                                 onClick(node) {
-                                    if (player.layers.main.boards.main.selectedAction === this.id) {
-                                        const timeNode = player.layers.main.boards.main.nodes.find(
-                                            node =>
-                                                node.type === "resource" &&
-                                                (node.data as ResourceNodeData).resourceType ===
-                                                    "time"
-                                        );
-                                        if (timeNode) {
-                                            (timeNode.data as ResourceNodeData).amount = Decimal.sub(
-                                                (timeNode.data as ResourceNodeData).amount,
-                                                30 * 60
-                                            );
-                                            player.layers.main.boards.main.selectedAction = null;
-                                            (node.data as ActionNodeData).log.push(
-                                                getRandomEvent(redditEvents)!
+                                    if (selectedAction.value?.id === this.id) {
+                                        const focusData = resources.focus.node
+                                            ?.data as ResourceNodeData;
+                                        if (focusData.currentFocus === "reddit") {
+                                            resources.focus.amount = Decimal.add(
+                                                resources.focus.amount || 0,
+                                                10
                                             );
+                                        } else {
+                                            focusData.currentFocus = "reddit";
+                                            resources.focus.amount = 10;
                                         }
+                                        const focusMult = Decimal.div(
+                                            resources.focus.amount,
+                                            100
+                                        ).add(1);
+                                        resources.time.amount = Decimal.sub(
+                                            resources.time.amount || 0,
+                                            Decimal.times(30 * 60, focusMult)
+                                        );
+                                        resources.energy.amount = Decimal.sub(
+                                            resources.energy.amount || 0,
+                                            Decimal.times(5, focusMult)
+                                        );
+                                        player.layers.main.boards.main.selectedAction = null;
+                                        (node.data as ActionNodeData).log.push(
+                                            getRandomEvent(redditEvents)!
+                                        );
                                     } else {
                                         player.layers.main.boards.main.selectedAction = this.id;
                                     }
@@ -417,18 +458,29 @@ export default {
                                 links(node) {
                                     return [
                                         {
-                                            // TODO this is ridiculous and needs some utility
-                                            //  function to shrink it down
-                                            from: player.layers.main.boards.main.nodes.find(
-                                                node =>
-                                                    node.type === "resource" &&
-                                                    (node.data as ResourceNodeData).resourceType ===
-                                                        "time"
-                                            ),
+                                            from: resources.time.node,
                                             to: node,
                                             stroke: "red",
                                             "stroke-width": 4,
                                             pulsing: true
+                                        },
+                                        {
+                                            from: resources.energy.node,
+                                            to: node,
+                                            stroke: "green",
+                                            "stroke-width": 4,
+                                            pulsing: true
+                                        },
+                                        {
+                                            from: resources.focus.node,
+                                            to: node,
+                                            stroke:
+                                                (resources.focus.node?.data as ResourceNodeData)
+                                                    .currentFocus === selectedAction.value?.id
+                                                    ? "green"
+                                                    : "black",
+                                            "stroke-width": 4,
+                                            pulsing: true
                                         }
                                     ];
                                 }
@@ -443,27 +495,21 @@ export default {
                         }
                         return this.selectedAction!.links;
                     }
-                    if (player.layers[this.layer].boards[this.id].selectedNode == null) {
+                    if (selectedNode.value == null) {
                         return null;
                     }
-                    const selectedNode = layers[this.layer].boards!.data[this.id].selectedNode;
-                    if (selectedNode.type === "resource") {
-                        const data = selectedNode.data as ResourceNodeData;
-                        if (data.resourceType in links) {
-                            return links[data.resourceType].map(link => {
-                                const node = player.layers.main.boards.main.nodes.find(
-                                    node =>
-                                        node.type === "resource" &&
-                                        (node.data as ResourceNodeData).resourceType ===
-                                            link.resource
-                                );
+                    if (selectedNode.value.type === "resource") {
+                        const resource = getResource(selectedNode.value)!;
+                        if (resource.name in links) {
+                            return links[resource.name].map(link => {
+                                const linkResource = resources[link.resource];
                                 let negativeLink = Decimal.lt(link.amount, 0);
                                 if (link.linkType === LinkType.LossOnly) {
                                     negativeLink = !negativeLink;
                                 }
                                 return {
-                                    from: selectedNode,
-                                    to: node,
+                                    from: selectedNode.value,
+                                    to: linkResource.node,
                                     stroke: negativeLink ? "red" : "green",
                                     "stroke-width": 4,
                                     pulsing: true
diff --git a/src/typings/features/board.d.ts b/src/typings/features/board.d.ts
index 69d462e..bc224b6 100644
--- a/src/typings/features/board.d.ts
+++ b/src/typings/features/board.d.ts
@@ -65,13 +65,13 @@ export interface BoardNodeAction {
     tooltip: string | ((node: BoardNode) => string);
     onClick: (node: BoardNode) => boolean | undefined;
     links?: BoardNodeLink[] | ((node: BoardNode) => BoardNodeLink[]);
+    [key: string]: any;
 }
 
 export interface BoardNodeLink {
     from: BoardNode;
     to: BoardNode;
     stroke: string;
-    strokeWidth: number | string;
     pulsing?: boolean;
     [key: string]: any;
 }

From 1ee8d2a41c5d5dcb711f7aae697a201831d186f4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Wed, 25 Aug 2021 08:41:49 -0500
Subject: [PATCH 31/49] Fixed issue with registering things out of order

---
 src/data/layers/main.ts | 94 ++++++++++++++++++++---------------------
 1 file changed, 47 insertions(+), 47 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 29d4506..269a4e4 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -28,6 +28,53 @@ export type ActionNodeData = {
     log: LogEntry[];
 };
 
+type Resource = {
+    readonly name: string;
+    readonly color: string;
+    readonly node: BoardNode | undefined;
+    amount: DecimalSource | undefined;
+    readonly maxAmount: DecimalSource;
+};
+
+const resources = {
+    time: createResource("time", "#3EB48933", 24 * 60 * 60),
+    energy: createResource("energy", "#FFA50033", 100),
+    social: createResource("social", "#80008033", 100),
+    mental: createResource("mental", "#32CD3233", 100),
+    focus: createResource("focus", "#0000FF33", 100)
+} as Record<string, Resource>;
+
+function createResource(name: string, color: string, maxAmount: DecimalSource): Resource {
+    const node = computed(() =>
+        player.layers.main?.boards.main.nodes.find(
+            node =>
+                node.type === "resource" && (node.data as ResourceNodeData).resourceType === name
+        )
+    );
+    const data = computed(() => node.value?.data as ResourceNodeData);
+    return {
+        name,
+        color,
+        get node() {
+            return node.value;
+        },
+        get amount() {
+            return data.value?.amount;
+        },
+        set amount(amount: DecimalSource) {
+            data.value.amount = Decimal.clamp(amount, 0, maxAmount);
+        },
+        maxAmount
+    };
+}
+
+function getResource(node: BoardNode): Resource | undefined {
+    return Object.values(resources).find(resource => resource.node === node);
+}
+
+const selectedNode = computed(() => layers.main?.boards?.data.main.selectedNode);
+const selectedAction = computed(() => layers.main?.boards?.data.main.selectedAction);
+
 export type LogEntry = {
     description: string;
     effectDescription?: string;
@@ -148,53 +195,6 @@ const pinAction = {
     }
 } as BoardNodeAction;
 
-type Resource = {
-    readonly name: string;
-    readonly color: string;
-    readonly node: BoardNode | undefined;
-    amount: DecimalSource | undefined;
-    readonly maxAmount: DecimalSource;
-};
-
-const resources = {
-    time: createResource("time", "#3EB48933", 24 * 60 * 60),
-    energy: createResource("energy", "#FFA50033", 100),
-    social: createResource("social", "#80008033", 100),
-    mental: createResource("mental", "#32CD3233", 100),
-    focus: createResource("focus", "#0000FF33", 100)
-} as Record<string, Resource>;
-
-function createResource(name: string, color: string, maxAmount: DecimalSource): Resource {
-    const node = computed(() =>
-        player.layers.main?.boards.main.nodes.find(
-            node =>
-                node.type === "resource" && (node.data as ResourceNodeData).resourceType === name
-        )
-    );
-    const data = computed(() => node.value?.data as ResourceNodeData);
-    return {
-        name,
-        color,
-        get node() {
-            return node.value;
-        },
-        get amount() {
-            return data.value?.amount;
-        },
-        set amount(amount: DecimalSource) {
-            data.value.amount = Decimal.clamp(amount, 0, maxAmount);
-        },
-        maxAmount
-    };
-}
-
-function getResource(node: BoardNode): Resource | undefined {
-    return Object.values(resources).find(resource => resource.node === node);
-}
-
-const selectedNode = computed(() => layers.main?.boards?.data.main.selectedNode);
-const selectedAction = computed(() => layers.main?.boards?.data.main.selectedAction);
-
 export default {
     id: "main",
     display: Main,

From ae0fbd09eb1c4eee9a90cff9e8c24332c0c1dc96 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:04:51 -0500
Subject: [PATCH 32/49] Refactored actions and added Bed node

---
 src/data/layers/main.ts    | 725 +++++++++++++++++++++++--------------
 src/util/break_eternity.ts |   5 +-
 2 files changed, 462 insertions(+), 268 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 269a4e4..214ba73 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -2,7 +2,14 @@ import { ProgressDisplay, Shape } from "@/game/enums";
 import { layers } from "@/game/layers";
 import player from "@/game/player";
 import Decimal, { DecimalSource } from "@/lib/break_eternity";
-import { BoardNode, BoardNodeAction } from "@/typings/features/board";
+import {
+    Board,
+    BoardNode,
+    BoardNodeAction,
+    BoardNodeLink,
+    NodeType
+} from "@/typings/features/board";
+import { RawFeature } from "@/typings/features/feature";
 import { RawLayer } from "@/typings/layer";
 import { formatTime } from "@/util/bignum";
 import { format, formatWhole } from "@/util/break_eternity";
@@ -12,18 +19,19 @@ import { computed, watch } from "vue";
 import themes from "../themes";
 import Main from "./Main.vue";
 
-export type ResourceNodeData = {
+type ResourceNodeData = {
     resourceType: string;
     amount: DecimalSource;
-    [key: string]: any;
+    [key: string]: unknown;
 };
 
-export type ItemNodeData = {
-    itemType: string;
+type ItemNodeData = {
+    resource: string;
     amount: DecimalSource;
+    display: string;
 };
 
-export type ActionNodeData = {
+type ActionNodeData = {
     actionType: string;
     log: LogEntry[];
 };
@@ -31,49 +39,71 @@ export type ActionNodeData = {
 type Resource = {
     readonly name: string;
     readonly color: string;
-    readonly node: BoardNode | undefined;
-    amount: DecimalSource | undefined;
+    readonly node: BoardNode;
     readonly maxAmount: DecimalSource;
+    amount: DecimalSource;
 };
 
 const resources = {
-    time: createResource("time", "#3EB48933", 24 * 60 * 60),
-    energy: createResource("energy", "#FFA50033", 100),
-    social: createResource("social", "#80008033", 100),
-    mental: createResource("mental", "#32CD3233", 100),
-    focus: createResource("focus", "#0000FF33", 100)
+    time: createResource("time", "#3EB48933", 24 * 60 * 60, 24 * 60 * 60),
+    energy: createResource("energy", "#FFA50033", 100, 100),
+    social: createResource("social", "#80008033", 100, 100),
+    mental: createResource("mental", "#32CD3233", 100, 100),
+    focus: createResource("focus", "#0000FF33", 100, 0)
 } as Record<string, Resource>;
 
-function createResource(name: string, color: string, maxAmount: DecimalSource): Resource {
+function createResource(
+    name: string,
+    color: string,
+    maxAmount: DecimalSource,
+    defaultAmount: DecimalSource
+): Resource {
     const node = computed(() =>
         player.layers.main?.boards.main.nodes.find(
             node =>
                 node.type === "resource" && (node.data as ResourceNodeData).resourceType === name
         )
     );
-    const data = computed(() => node.value?.data as ResourceNodeData);
     return {
         name,
         color,
         get node() {
+            // Should only run once, but this tricks TS into knowing node.value exists
+            while (node.value == null) {
+                player.layers.main.boards.main.nodes.push({
+                    id: getUniqueNodeID(layers.main.boards!.data.main),
+                    position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space
+                    type: "resource",
+                    data: {
+                        resourceType: name,
+                        amount: defaultAmount
+                    }
+                });
+            }
             return node.value;
         },
         get amount() {
-            return data.value?.amount;
+            return node.value ? (node.value.data as ResourceNodeData).amount : defaultAmount;
         },
         set amount(amount: DecimalSource) {
-            data.value.amount = Decimal.clamp(amount, 0, maxAmount);
+            (this.node.data as ResourceNodeData).amount = Decimal.clamp(amount, 0, maxAmount);
         },
         maxAmount
     };
 }
 
-function getResource(node: BoardNode): Resource | undefined {
-    return Object.values(resources).find(resource => resource.node === node);
+function getResource(node: BoardNode): Resource {
+    const resource = Object.values(resources).find(resource => resource.node === node);
+    if (resource == null) {
+        console.error("No resource associated with node", node);
+        throw Error();
+    }
+    return resource;
 }
 
 const selectedNode = computed(() => layers.main?.boards?.data.main.selectedNode);
 const selectedAction = computed(() => layers.main?.boards?.data.main.selectedAction);
+const focusMult = computed(() => Decimal.div(resources.focus.amount, 100).add(1));
 
 export type LogEntry = {
     description: string;
@@ -85,31 +115,190 @@ export type WeightedEvent = {
     weight: number;
 };
 
-const redditEvents = [
-    {
-        event: () => ({ description: "You blink and half an hour has passed before you know it." }),
-        weight: 1
+function createItem(resource: string, amount: DecimalSource, display?: string) {
+    display = display || camelToTitle(resource);
+    const item = {
+        id: getUniqueNodeID(layers.main.boards!.data.main),
+        position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space
+        type: "item",
+        data: { resource, amount, display } as ItemNodeData
+    };
+    player.layers.main.boards.main.nodes.push(item);
+    return item;
+}
+
+type Action = {
+    icon: string;
+    fillColor?: string;
+    tooltip?: string;
+    events: Array<{
+        event: () => LogEntry;
+        weight: number;
+    }>;
+    baseChanges: Array<{
+        resource: string;
+        amount: DecimalSource;
+        assign?: boolean;
+    }>;
+};
+
+const actions = {
+    reddit: {
+        icon: "reddit",
+        tooltip: "Browse Reddit",
+        events: [
+            {
+                event: () => ({
+                    description: "You blink and half an hour has passed before you know it."
+                }),
+                weight: 1
+            },
+            {
+                event: () => {
+                    createItem("time", 15 * 60, "Speed");
+                    return {
+                        description:
+                            "You found some funny memes and actually feel a bit refreshed.",
+                        effectDescription: `Added <span style="color: #0FF;">Speed</span> node`
+                    };
+                },
+                weight: 0.5
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -30 * 60 },
+            { resource: "energy", amount: 5 }
+        ]
     },
-    {
-        event: () => {
-            const id = getUniqueNodeID(layers.main.boards!.data.main);
-            player.layers.main.boards.main.nodes.push({
-                id,
-                position: { x: 0, y: 150 }, // TODO function to get nearest unoccupied space
-                type: "item",
-                data: {
-                    itemType: "time",
-                    amount: new Decimal(15 * 60)
+    sleep: {
+        icon: "bed",
+        tooltip: "Sleep",
+        events: [
+            {
+                event: () => ({ description: "You have a normal evening of undisturbed sleep" }),
+                weight: 90
+            },
+            {
+                event: () => {
+                    resources.energy.amount = 50;
+                    return {
+                        description: "You had a very restless sleep filled with nightmares :(",
+                        effectDescription: `50% <span style="color: ${resources.energy.color};">Energy</span>`
+                    };
+                },
+                weight() {
+                    return Decimal.sub(100, resources.mental.amount || 100);
                 }
-            });
-            return {
-                description: "You found some funny memes and actually feel a bit refreshed.",
-                effectDescription: `Added <span style="color: #0FF;">Speed</span> node`
-            };
-        },
-        weight: 0.5
+            },
+            {
+                event: () => {
+                    createItem("energy", 25, "Refreshed");
+                    return {
+                        description:
+                            "You dreamt of your future and woke up feeling extra refreshed",
+                        effectDescription: `Added <span style="color: ${resources.energy.color};">Refreshed</span> node`
+                    };
+                },
+                weight() {
+                    return Decimal.sub(resources.mental.amount || 100, 75).max(5);
+                }
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -8 * 30 * 60 },
+            { resource: "energy", amount: 100, assign: true }
+        ]
+    },
+    rest: {
+        icon: "chair",
+        tooltip: "Rest",
+        events: [
+            {
+                event: () => {
+                    resources.energy.amount = Decimal.sub(
+                        resources.energy.amount || 100,
+                        Decimal.times(10, focusMult.value)
+                    );
+                    return { description: "You rest your eyes for a bit and wake up rejuvenated" };
+                },
+                weight: 90
+            },
+            {
+                event: () => ({
+                    description:
+                        "You close your eyes and it feels like no time has gone by before you wake up with a start, slightly less rested than you feel you should be given the time that's passed",
+                    effectDescription: `-25% effective <span style="color: ${resources.energy.color};">Energy</span> restoration`
+                }),
+                weight: 5
+            },
+            {
+                event: () => {
+                    resources.energy.amount = Decimal.add(
+                        resources.energy.amount,
+                        Decimal.times(20, focusMult.value)
+                    );
+                    return {
+                        description:
+                            "You take an incredible power nap and wake up significantly more refreshed",
+                        effectDescription: `+50% effectvie <span style="color: ${resources.energy.color};">Energy</span> restoration`
+                    };
+                },
+                weight: 5
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -4 * 30 * 60 },
+            // 30 is the lowest it can be from any event
+            // typically you'll get 40 though
+            { resource: "energy", amount: 30 }
+        ]
     }
-];
+} as Record<string, Action>;
+
+const pinAction = {
+    id: "pin",
+    icon: "push_pin",
+    fillColor(node) {
+        if (node.pinned) {
+            return themes[player.theme].variables["--bought"];
+        }
+        return themes[player.theme].variables["--secondary-background"];
+    },
+    tooltip: "Always show resource",
+    onClick(node) {
+        node.pinned = !node.pinned;
+        return true;
+    }
+} as BoardNodeAction;
+
+const logAction = {
+    id: "info",
+    icon: "history_edu",
+    fillColor() {
+        return themes[player.theme].variables["--secondary-background"];
+    },
+    tooltip: "Log",
+    onClick(node) {
+        player.layers.main.openNode = node.id;
+        player.layers.main.showModal = true;
+    }
+} as BoardNodeAction;
+
+type ActionNode = {
+    actions: string[];
+    display: string;
+};
+
+const actionNodes = {
+    web: {
+        actions: ["reddit"],
+        display: "Web"
+    },
+    bed: {
+        actions: ["sleep", "rest"],
+        display: "Bed"
+    }
+} as Record<string, ActionNode>;
 
 function getRandomEvent(events: WeightedEvent[]): LogEntry | null {
     if (events.length === 0) {
@@ -179,21 +368,206 @@ for (const resource in links) {
     );
 }
 
-const pinAction = {
-    id: "pin",
-    icon: "push_pin",
-    fillColor(node) {
-        if (node.pinned) {
-            return themes[player.theme].variables["--bought"];
-        }
-        return themes[player.theme].variables["--secondary-background"];
+const resourceNodeType = {
+    title(node) {
+        return (node.data as ResourceNodeData).resourceType;
     },
-    tooltip: "Always show resource",
-    onClick(node) {
-        node.pinned = !node.pinned;
-        return true;
+    label(node) {
+        const resource = getResource(node);
+        if (selectedNode.value != node && selectedAction.value != null) {
+            if (resource.name === "focus") {
+                const currentFocus =
+                    (resource.node.data as ResourceNodeData).currentFocus ===
+                    selectedAction.value?.id;
+                return {
+                    text: currentFocus ? "10%" : "X",
+                    color: currentFocus ? "green" : "black",
+                    pulsing: true
+                };
+            }
+            const action = actions[selectedAction.value.id];
+            const change = action.baseChanges.find(change => change.resource === resource.name);
+            if (change != null) {
+                let text;
+                if (resource.name === "time") {
+                    text = formatTime(change.amount);
+                } else if (Decimal.eq(resource.maxAmount, 100)) {
+                    text = formatWhole(change.amount) + "%";
+                } else {
+                    text = format(change.amount);
+                }
+                let color;
+                if (change.assign) {
+                    color = "black";
+                } else {
+                    color = Decimal.gt(change.amount, 0) ? "green" : "red";
+                }
+                return { text, color, pulsing: true };
+            }
+        }
+
+        if (
+            selectedNode.value != node &&
+            selectedNode.value != null &&
+            selectedNode.value.type === "resource"
+        ) {
+            const selectedResource = getResource(selectedNode.value);
+            if (selectedResource.name in links) {
+                const link = links[selectedResource.name].find(
+                    link => link.resource === resource.name
+                );
+                if (link) {
+                    let text;
+                    if (resource.name === "time") {
+                        text = formatTime(link.amount);
+                    } else if (Decimal.eq(resource.maxAmount, 100)) {
+                        text = formatWhole(link.amount) + "%";
+                    } else {
+                        text = format(link.amount);
+                    }
+                    let negativeLink = Decimal.lt(link.amount, 0);
+                    if (link.linkType === LinkType.LossOnly) {
+                        negativeLink = !negativeLink;
+                    }
+                    return { text, color: negativeLink ? "red" : "green" };
+                }
+            }
+        }
+
+        if (selectedNode.value == node || node.pinned) {
+            const data = node.data as ResourceNodeData;
+            if (data.resourceType === "time") {
+                return { text: formatTime(data.amount), color: resource.color };
+            }
+            if (Decimal.eq(resource.maxAmount, 100)) {
+                return {
+                    text: formatWhole(data.amount) + "%",
+                    color: resource.color
+                };
+            }
+            return { text: format(data.amount), color: resource.color };
+        }
+    },
+    draggable: true,
+    progress(node) {
+        const resource = getResource(node);
+        return Decimal.div(resource.amount, resource.maxAmount).toNumber();
+    },
+    fillColor() {
+        return themes[player.theme].variables["--background"];
+    },
+    progressColor(node) {
+        return getResource(node).color;
+    },
+    canAccept(node, otherNode) {
+        return (
+            otherNode.type === "item" &&
+            (otherNode.data as ItemNodeData).resource === getResource(node).name
+        );
+    },
+    onDrop(node, otherNode) {
+        const resource = getResource(node);
+        const index = player.layers[this.layer].boards[this.id].nodes.indexOf(otherNode);
+        player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
+        resource.amount = Decimal.add(resource.amount, (otherNode.data as ItemNodeData).amount);
+    },
+    actions: [pinAction]
+} as RawFeature<NodeType>;
+
+const actionNodeType = {
+    title(node) {
+        return actionNodes[(node.data as ActionNodeData).actionType].display;
+    },
+    label(node) {
+        if (selectedNode.value == node && selectedAction.value != null) {
+            return { text: selectedAction.value.tooltip, color: "#000" };
+        }
+    },
+    fillColor: "#000",
+    draggable: true,
+    shape: Shape.Diamond,
+    progressColor: "#0FF3",
+    progressDisplay: ProgressDisplay.Outline,
+    actions(node) {
+        const actionNode = actionNodes[(node.data as ActionNodeData).actionType];
+        return [
+            logAction,
+            ...actionNode.actions.map(id => {
+                const action = actions[id];
+                return {
+                    id,
+                    icon: action.icon,
+                    tooltip: action.tooltip,
+                    fillColor: action.fillColor,
+                    onClick(node) {
+                        if (selectedAction.value?.id === this.id) {
+                            const focusData = resources.focus.node.data as ResourceNodeData;
+                            if (focusData.currentFocus === id) {
+                                resources.focus.amount = Decimal.add(resources.focus.amount, 10);
+                            } else {
+                                focusData.currentFocus = id;
+                                resources.focus.amount = 10;
+                            }
+                            for (const change of action.baseChanges) {
+                                if (change.assign) {
+                                    resources[change.resource].amount = change.amount;
+                                } else if (change.resource === "time") {
+                                    // Time isn't affected by focus multiplier
+                                    resources.time.amount = Decimal.add(
+                                        resources.time.amount,
+                                        change.amount
+                                    );
+                                } else {
+                                    resources.time.amount = Decimal.add(
+                                        resources.time.amount,
+                                        Decimal.times(change.amount, focusMult.value)
+                                    );
+                                }
+                            }
+                            player.layers.main.boards.main.selectedAction = null;
+                            const logEntry = getRandomEvent(action.events);
+                            if (logEntry) {
+                                (node.data as ActionNodeData).log.push(logEntry);
+                            }
+                        } else {
+                            player.layers.main.boards.main.selectedAction = this.id;
+                        }
+                    },
+                    links(node) {
+                        return [
+                            {
+                                from: resources.focus.node,
+                                to: node,
+                                stroke:
+                                    (resources.focus.node.data as ResourceNodeData).currentFocus ===
+                                    selectedAction.value?.id
+                                        ? "green"
+                                        : "black",
+                                "stroke-width": 4,
+                                pulsing: true
+                            },
+                            ...action.baseChanges.map(change => {
+                                let color;
+                                if (change.assign) {
+                                    color = "black";
+                                } else {
+                                    color = Decimal.gt(change.amount, 0) ? "green" : "red";
+                                }
+                                return {
+                                    from: resources[change.resource].node,
+                                    to: node,
+                                    stroke: color,
+                                    "stroke-width": 4,
+                                    pulsing: true
+                                } as BoardNodeLink;
+                            })
+                        ];
+                    }
+                } as BoardNodeAction;
+            })
+        ];
     }
-} as BoardNodeAction;
+} as RawFeature<NodeType>;
 
 export default {
     id: "main",
@@ -228,7 +602,7 @@ export default {
                             data: {
                                 resourceType: "time",
                                 amount: new Decimal(24 * 60 * 60)
-                            }
+                            } as ResourceNodeData
                         },
                         {
                             position: { x: 300, y: 0 },
@@ -236,7 +610,7 @@ export default {
                             data: {
                                 resourceType: "mental",
                                 amount: new Decimal(100)
-                            }
+                            } as ResourceNodeData
                         },
                         {
                             position: { x: 150, y: 0 },
@@ -244,7 +618,7 @@ export default {
                             data: {
                                 resourceType: "social",
                                 amount: new Decimal(100)
-                            }
+                            } as ResourceNodeData
                         },
                         {
                             position: { x: -150, y: 0 },
@@ -253,7 +627,7 @@ export default {
                                 resourceType: "focus",
                                 amount: new Decimal(0),
                                 currentFocus: ""
-                            }
+                            } as ResourceNodeData
                         },
                         {
                             position: { x: -300, y: 0 },
@@ -261,7 +635,7 @@ export default {
                             data: {
                                 resourceType: "energy",
                                 amount: new Decimal(100)
-                            }
+                            } as ResourceNodeData
                         },
                         {
                             position: { x: -150, y: 150 },
@@ -269,237 +643,54 @@ export default {
                             data: {
                                 actionType: "web",
                                 log: []
-                            }
+                            } as ActionNodeData
+                        },
+                        {
+                            position: { x: 150, y: 150 },
+                            type: "action",
+                            data: {
+                                actionType: "bed",
+                                log: []
+                            } as ActionNodeData
                         }
                     ];
                 },
                 types: {
-                    resource: {
-                        title(node) {
-                            return (node.data as ResourceNodeData).resourceType;
-                        },
-                        label(node) {
-                            const resource = getResource(node)!;
-                            if (selectedNode.value != node && selectedAction.value != null) {
-                                if (resource && resource.name === "focus") {
-                                    const currentFocus =
-                                        (resource.node?.data as ResourceNodeData | undefined)
-                                            ?.currentFocus === selectedAction.value?.id;
-                                    return {
-                                        text: currentFocus ? "10%" : "X",
-                                        color: currentFocus ? "green" : "black",
-                                        pulsing: true
-                                    };
-                                }
-                                switch (selectedAction.value.id) {
-                                    case "reddit":
-                                        switch (resource.name) {
-                                            case "time":
-                                                return {
-                                                    text: "30m",
-                                                    color: "red",
-                                                    pulsing: true
-                                                };
-                                            case "energy":
-                                                return {
-                                                    text: "5%",
-                                                    color: "green",
-                                                    pulsing: true
-                                                };
-                                        }
-                                        break;
-                                }
-                            }
-                            if (
-                                selectedNode.value != node &&
-                                selectedNode.value != null &&
-                                selectedNode.value.type === "resource"
-                            ) {
-                                const selectedResource = getResource(selectedNode.value);
-                                if (selectedResource && selectedResource.name in links) {
-                                    const link = links[selectedResource.name].find(
-                                        link => link.resource === resource.name
-                                    );
-                                    if (link) {
-                                        let text;
-                                        if (resource.name === "time") {
-                                            text = formatTime(link.amount);
-                                        } else if (Decimal.eq(resource.maxAmount, 100)) {
-                                            text = formatWhole(link.amount) + "%";
-                                        } else {
-                                            text = format(link.amount);
-                                        }
-                                        let negativeLink = Decimal.lt(link.amount, 0);
-                                        if (link.linkType === LinkType.LossOnly) {
-                                            negativeLink = !negativeLink;
-                                        }
-                                        return { text, color: negativeLink ? "red" : "green" };
-                                    }
-                                }
-                            }
-                            if (selectedNode.value == node || node.pinned) {
-                                const data = node.data as ResourceNodeData;
-                                if (data.resourceType === "time") {
-                                    return { text: formatTime(data.amount), color: "#0FF3" };
-                                }
-                                if (Decimal.eq(resource.maxAmount, 100)) {
-                                    return { text: formatWhole(data.amount) + "%", color: "#0FF3" };
-                                }
-                                return { text: format(data.amount), color: "#0FF3" };
-                            }
-                        },
-                        draggable: true,
-                        progress(node) {
-                            const resource = getResource(node)!;
-                            return Decimal.div(resource.amount || 0, resource.maxAmount).toNumber();
-                        },
-                        fillColor() {
-                            return themes[player.theme].variables["--background"];
-                        },
-                        progressColor(node) {
-                            return getResource(node)!.color;
-                        },
-                        canAccept(node, otherNode) {
-                            return otherNode.type === "item";
-                        },
-                        onDrop(node, otherNode) {
-                            const resource = getResource(node)!;
-                            const index = player.layers[this.layer].boards[this.id].nodes.indexOf(
-                                otherNode
-                            );
-                            player.layers[this.layer].boards[this.id].nodes.splice(index, 1);
-                            resource.amount = Decimal.add(
-                                resource.amount || 0,
-                                (otherNode.data as ItemNodeData).amount
-                            );
-                        },
-                        actions: [pinAction]
-                    },
+                    resource: resourceNodeType,
+                    action: actionNodeType,
                     item: {
                         title(node) {
-                            switch ((node.data as ItemNodeData).itemType) {
-                                default:
-                                    return null;
-                                case "time":
-                                    return "speed";
-                            }
+                            return (node.data as ItemNodeData).display;
                         },
                         label(node) {
                             if (selectedNode.value == node || node.pinned) {
                                 const data = node.data as ItemNodeData;
-                                if (data.itemType === "time") {
-                                    return { text: formatTime(data.amount), color: "#0FF3" };
+                                const resource = resources[data.resource];
+                                let text;
+                                if (data.resource === "time") {
+                                    text = formatTime(data.amount);
+                                } else if (Decimal.eq(100, resource.maxAmount)) {
+                                    text = format(data.amount) + "%";
                                 }
-                                return { text: format(data.amount), color: "#0FF3" };
+                                return { text, color: resource.color };
                             }
                         },
                         draggable: true,
                         actions: [pinAction]
-                    },
-                    action: {
-                        title(node) {
-                            return camelToTitle((node.data as ActionNodeData).actionType);
-                        },
-                        fillColor: "#000",
-                        draggable: true,
-                        shape: Shape.Diamond,
-                        progressColor: "#0FF3",
-                        progressDisplay: ProgressDisplay.Outline,
-                        actions: [
-                            {
-                                id: "info",
-                                icon: "history_edu",
-                                fillColor() {
-                                    return themes[player.theme].variables["--secondary-background"];
-                                },
-                                tooltip: "Log",
-                                onClick(node) {
-                                    player.layers.main.openNode = node.id;
-                                    player.layers.main.showModal = true;
-                                }
-                            },
-                            {
-                                id: "reddit",
-                                icon: "reddit",
-                                tooltip: "Browse Reddit",
-                                onClick(node) {
-                                    if (selectedAction.value?.id === this.id) {
-                                        const focusData = resources.focus.node
-                                            ?.data as ResourceNodeData;
-                                        if (focusData.currentFocus === "reddit") {
-                                            resources.focus.amount = Decimal.add(
-                                                resources.focus.amount || 0,
-                                                10
-                                            );
-                                        } else {
-                                            focusData.currentFocus = "reddit";
-                                            resources.focus.amount = 10;
-                                        }
-                                        const focusMult = Decimal.div(
-                                            resources.focus.amount,
-                                            100
-                                        ).add(1);
-                                        resources.time.amount = Decimal.sub(
-                                            resources.time.amount || 0,
-                                            Decimal.times(30 * 60, focusMult)
-                                        );
-                                        resources.energy.amount = Decimal.sub(
-                                            resources.energy.amount || 0,
-                                            Decimal.times(5, focusMult)
-                                        );
-                                        player.layers.main.boards.main.selectedAction = null;
-                                        (node.data as ActionNodeData).log.push(
-                                            getRandomEvent(redditEvents)!
-                                        );
-                                    } else {
-                                        player.layers.main.boards.main.selectedAction = this.id;
-                                    }
-                                },
-                                links(node) {
-                                    return [
-                                        {
-                                            from: resources.time.node,
-                                            to: node,
-                                            stroke: "red",
-                                            "stroke-width": 4,
-                                            pulsing: true
-                                        },
-                                        {
-                                            from: resources.energy.node,
-                                            to: node,
-                                            stroke: "green",
-                                            "stroke-width": 4,
-                                            pulsing: true
-                                        },
-                                        {
-                                            from: resources.focus.node,
-                                            to: node,
-                                            stroke:
-                                                (resources.focus.node?.data as ResourceNodeData)
-                                                    .currentFocus === selectedAction.value?.id
-                                                    ? "green"
-                                                    : "black",
-                                            "stroke-width": 4,
-                                            pulsing: true
-                                        }
-                                    ];
-                                }
-                            }
-                        ]
                     }
                 },
-                links() {
-                    if (this.selectedAction?.links) {
-                        if (typeof this.selectedAction!.links === "function") {
-                            return this.selectedAction!.links(this.selectedNode);
+                links(this: Board) {
+                    if (this.selectedNode && this.selectedAction?.links) {
+                        if (typeof this.selectedAction.links === "function") {
+                            return this.selectedAction.links(this.selectedNode);
                         }
-                        return this.selectedAction!.links;
+                        return this.selectedAction.links;
                     }
                     if (selectedNode.value == null) {
                         return null;
                     }
                     if (selectedNode.value.type === "resource") {
-                        const resource = getResource(selectedNode.value)!;
+                        const resource = getResource(selectedNode.value);
                         if (resource.name in links) {
                             return links[resource.name].map(link => {
                                 const linkResource = resources[link.resource];
diff --git a/src/util/break_eternity.ts b/src/util/break_eternity.ts
index 003d807..57ef17a 100644
--- a/src/util/break_eternity.ts
+++ b/src/util/break_eternity.ts
@@ -115,6 +115,9 @@ export function formatWhole(num: DecimalSource): string {
 }
 
 export function formatTime(seconds: DecimalSource): string {
+    if (Decimal.lt(seconds, 0)) {
+        return "-" + formatTime(Decimal.neg(seconds));
+    }
     if (Decimal.gt(seconds, 2 ** 51)) {
         // integer precision limit
         return format(Decimal.div(seconds, 31536000)) + "y";
@@ -130,7 +133,7 @@ export function formatTime(seconds: DecimalSource): string {
             "h " +
             formatWhole(Math.floor(seconds / 60) % 60) +
             "m " +
-            format(seconds % 60) +
+            formatWhole(seconds % 60) +
             "s"
         );
     } else if (seconds < 31536000) {

From af57840dc6fd3ec2c5b37a24a3f9eff6dce87522 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:13:42 -0500
Subject: [PATCH 33/49] Added animation to nodes appearing/disappearing

---
 src/components/board/BoardNode.vue | 180 +++++++++++++++--------------
 1 file changed, 91 insertions(+), 89 deletions(-)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 3ca807d..2bc3395 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -44,100 +44,97 @@
             </g>
         </transition>
 
-        <g
-            v-if="shape === Shape.Circle"
-            @mouseenter="mouseEnter"
-            @mouseleave="mouseLeave"
-            @mousedown="mouseDown"
-            @touchstart="mouseDown"
-            @mouseup="mouseUp"
-            @touchend="mouseUp"
-        >
-            <circle
-                v-if="canAccept"
-                :r="size + 8"
-                :fill="backgroundColor"
-                :stroke="receivingNode ? '#0F0' : '#0F03'"
-                :stroke-width="2"
-            />
+        <transition name="grow" appear>
+            <g
+                @mouseenter="mouseEnter"
+                @mouseleave="mouseLeave"
+                @mousedown="mouseDown"
+                @touchstart="mouseDown"
+                @mouseup="mouseUp"
+                @touchend="mouseUp"
+            >
+                <g v-if="shape === Shape.Circle">
+                    <circle
+                        v-if="canAccept"
+                        :r="size + 8"
+                        :fill="backgroundColor"
+                        :stroke="receivingNode ? '#0F0' : '#0F03'"
+                        :stroke-width="2"
+                    />
 
-            <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
+                    <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
 
-            <circle
-                v-if="progressDisplay === ProgressDisplay.Fill"
-                :r="Math.max(size * progress - 2, 0)"
-                :fill="progressColor"
-            />
-            <circle
-                v-else
-                :r="size + 4.5"
-                class="progressRing"
-                fill="transparent"
-                :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
-                :stroke-width="5"
-                :stroke-dashoffset="
-                    (size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
-                "
-                :stroke="progressColor"
-            />
-        </g>
-        <g
-            v-else-if="shape === Shape.Diamond"
-            transform="rotate(45, 0, 0)"
-            @mouseenter="mouseEnter"
-            @mouseleave="mouseLeave"
-            @mousedown="mouseDown"
-            @touchstart="mouseDown"
-            @mouseup="mouseUp"
-            @touchend="mouseUp"
-        >
-            <rect
-                v-if="canAccept"
-                :width="size * sqrtTwo + 16"
-                :height="size * sqrtTwo + 16"
-                :transform="
-                    `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) / 2})`
-                "
-                :fill="backgroundColor"
-                :stroke="receivingNode ? '#0F0' : '#0F03'"
-                :stroke-width="2"
-            />
+                    <circle
+                        v-if="progressDisplay === ProgressDisplay.Fill"
+                        :r="Math.max(size * progress - 2, 0)"
+                        :fill="progressColor"
+                    />
+                    <circle
+                        v-else
+                        :r="size + 4.5"
+                        class="progressRing"
+                        fill="transparent"
+                        :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
+                        :stroke-width="5"
+                        :stroke-dashoffset="
+                            (size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
+                        "
+                        :stroke="progressColor"
+                    />
+                </g>
+                <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
+                    <rect
+                        v-if="canAccept"
+                        :width="size * sqrtTwo + 16"
+                        :height="size * sqrtTwo + 16"
+                        :transform="
+                            `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
+                                2})`
+                        "
+                        :fill="backgroundColor"
+                        :stroke="receivingNode ? '#0F0' : '#0F03'"
+                        :stroke-width="2"
+                    />
 
-            <rect
-                :width="size * sqrtTwo"
-                :height="size * sqrtTwo"
-                :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
-                :fill="fillColor"
-                :stroke="outlineColor"
-                :stroke-width="4"
-            />
+                    <rect
+                        :width="size * sqrtTwo"
+                        :height="size * sqrtTwo"
+                        :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
+                        :fill="fillColor"
+                        :stroke="outlineColor"
+                        :stroke-width="4"
+                    />
 
-            <rect
-                v-if="progressDisplay === ProgressDisplay.Fill"
-                :width="Math.max(size * sqrtTwo * progress - 2, 0)"
-                :height="Math.max(size * sqrtTwo * progress - 2, 0)"
-                :transform="
-                    `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${-Math.max(
-                        size * sqrtTwo * progress - 2,
-                        0
-                    ) / 2})`
-                "
-                :fill="progressColor"
-            />
-            <rect
-                v-else
-                :width="size * sqrtTwo + 9"
-                :height="size * sqrtTwo + 9"
-                :transform="`translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`"
-                fill="transparent"
-                :stroke-dasharray="(size * sqrtTwo + 9) * 4"
-                :stroke-width="5"
-                :stroke-dashoffset="(size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4"
-                :stroke="progressColor"
-            />
-        </g>
+                    <rect
+                        v-if="progressDisplay === ProgressDisplay.Fill"
+                        :width="Math.max(size * sqrtTwo * progress - 2, 0)"
+                        :height="Math.max(size * sqrtTwo * progress - 2, 0)"
+                        :transform="
+                            `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
+                                2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
+                        "
+                        :fill="progressColor"
+                    />
+                    <rect
+                        v-else
+                        :width="size * sqrtTwo + 9"
+                        :height="size * sqrtTwo + 9"
+                        :transform="
+                            `translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`
+                        "
+                        fill="transparent"
+                        :stroke-dasharray="(size * sqrtTwo + 9) * 4"
+                        :stroke-width="5"
+                        :stroke-dashoffset="
+                            (size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
+                        "
+                        :stroke="progressColor"
+                    />
+                </g>
 
-        <text :fill="titleColor" class="node-title">{{ title }}</text>
+                <text :fill="titleColor" class="node-title">{{ title }}</text>
+            </g>
+        </transition>
 
         <transition name="fade" appear>
             <text
@@ -389,6 +386,11 @@ export default defineComponent({
         opacity: 0.25;
     }
 }
+
+.grow-enter-from,
+.fade-leave-to {
+    transform: scale(0);
+}
 </style>
 
 <style>

From 1709fe3079df41d7029d178b4cdac5d0a6920d16 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:28:03 -0500
Subject: [PATCH 34/49] Fixed grow animation not working on nodes disappearing

---
 src/components/board/Board.vue     |  26 ++--
 src/components/board/BoardNode.vue | 185 ++++++++++++++---------------
 2 files changed, 106 insertions(+), 105 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index d01789e..5d60a07 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -20,18 +20,20 @@
                         <BoardLink :link="link" />
                     </g>
                 </transition-group>
-                <BoardNode
-                    v-for="node in nodes"
-                    :key="node.id"
-                    :node="node"
-                    :nodeType="board.types[node.type]"
-                    :dragging="draggingNode"
-                    :dragged="dragged"
-                    :hasDragged="hasDragged"
-                    :receivingNode="receivingNode?.id === node.id"
-                    @mouseDown="mouseDown"
-                    @endDragging="endDragging"
-                />
+                <transition-group name="grow" :duration="500" appear>
+                    <g v-for="node in nodes" :key="node.id" style="transition-duration: 0s">
+                        <BoardNode
+                            :node="node"
+                            :nodeType="board.types[node.type]"
+                            :dragging="draggingNode"
+                            :dragged="dragged"
+                            :hasDragged="hasDragged"
+                            :receivingNode="receivingNode?.id === node.id"
+                            @mouseDown="mouseDown"
+                            @endDragging="endDragging"
+                        />
+                    </g>
+                </transition-group>
             </g>
         </svg>
     </panZoom>
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index 2bc3395..e1baf43 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -44,97 +44,96 @@
             </g>
         </transition>
 
-        <transition name="grow" appear>
-            <g
-                @mouseenter="mouseEnter"
-                @mouseleave="mouseLeave"
-                @mousedown="mouseDown"
-                @touchstart="mouseDown"
-                @mouseup="mouseUp"
-                @touchend="mouseUp"
-            >
-                <g v-if="shape === Shape.Circle">
-                    <circle
-                        v-if="canAccept"
-                        :r="size + 8"
-                        :fill="backgroundColor"
-                        :stroke="receivingNode ? '#0F0' : '#0F03'"
-                        :stroke-width="2"
-                    />
+        <g
+            class="node-container"
+            @mouseenter="mouseEnter"
+            @mouseleave="mouseLeave"
+            @mousedown="mouseDown"
+            @touchstart="mouseDown"
+            @mouseup="mouseUp"
+            @touchend="mouseUp"
+        >
+            <g v-if="shape === Shape.Circle">
+                <circle
+                    v-if="canAccept"
+                    :r="size + 8"
+                    :fill="backgroundColor"
+                    :stroke="receivingNode ? '#0F0' : '#0F03'"
+                    :stroke-width="2"
+                />
 
-                    <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
+                <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
 
-                    <circle
-                        v-if="progressDisplay === ProgressDisplay.Fill"
-                        :r="Math.max(size * progress - 2, 0)"
-                        :fill="progressColor"
-                    />
-                    <circle
-                        v-else
-                        :r="size + 4.5"
-                        class="progressRing"
-                        fill="transparent"
-                        :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
-                        :stroke-width="5"
-                        :stroke-dashoffset="
-                            (size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
-                        "
-                        :stroke="progressColor"
-                    />
-                </g>
-                <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
-                    <rect
-                        v-if="canAccept"
-                        :width="size * sqrtTwo + 16"
-                        :height="size * sqrtTwo + 16"
-                        :transform="
-                            `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
-                                2})`
-                        "
-                        :fill="backgroundColor"
-                        :stroke="receivingNode ? '#0F0' : '#0F03'"
-                        :stroke-width="2"
-                    />
-
-                    <rect
-                        :width="size * sqrtTwo"
-                        :height="size * sqrtTwo"
-                        :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
-                        :fill="fillColor"
-                        :stroke="outlineColor"
-                        :stroke-width="4"
-                    />
-
-                    <rect
-                        v-if="progressDisplay === ProgressDisplay.Fill"
-                        :width="Math.max(size * sqrtTwo * progress - 2, 0)"
-                        :height="Math.max(size * sqrtTwo * progress - 2, 0)"
-                        :transform="
-                            `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
-                                2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
-                        "
-                        :fill="progressColor"
-                    />
-                    <rect
-                        v-else
-                        :width="size * sqrtTwo + 9"
-                        :height="size * sqrtTwo + 9"
-                        :transform="
-                            `translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`
-                        "
-                        fill="transparent"
-                        :stroke-dasharray="(size * sqrtTwo + 9) * 4"
-                        :stroke-width="5"
-                        :stroke-dashoffset="
-                            (size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
-                        "
-                        :stroke="progressColor"
-                    />
-                </g>
-
-                <text :fill="titleColor" class="node-title">{{ title }}</text>
+                <circle
+                    v-if="progressDisplay === ProgressDisplay.Fill"
+                    :r="Math.max(size * progress - 2, 0)"
+                    :fill="progressColor"
+                />
+                <circle
+                    v-else
+                    :r="size + 4.5"
+                    class="progressRing"
+                    fill="transparent"
+                    :stroke-dasharray="(size + 4.5) * 2 * Math.PI"
+                    :stroke-width="5"
+                    :stroke-dashoffset="
+                        (size + 4.5) * 2 * Math.PI - progress * (size + 4.5) * 2 * Math.PI
+                    "
+                    :stroke="progressColor"
+                />
             </g>
-        </transition>
+            <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
+                <rect
+                    v-if="canAccept"
+                    :width="size * sqrtTwo + 16"
+                    :height="size * sqrtTwo + 16"
+                    :transform="
+                        `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
+                            2})`
+                    "
+                    :fill="backgroundColor"
+                    :stroke="receivingNode ? '#0F0' : '#0F03'"
+                    :stroke-width="2"
+                />
+
+                <rect
+                    :width="size * sqrtTwo"
+                    :height="size * sqrtTwo"
+                    :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
+                    :fill="fillColor"
+                    :stroke="outlineColor"
+                    :stroke-width="4"
+                />
+
+                <rect
+                    v-if="progressDisplay === ProgressDisplay.Fill"
+                    :width="Math.max(size * sqrtTwo * progress - 2, 0)"
+                    :height="Math.max(size * sqrtTwo * progress - 2, 0)"
+                    :transform="
+                        `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
+                            2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
+                    "
+                    :fill="progressColor"
+                />
+                <rect
+                    v-else
+                    :width="size * sqrtTwo + 9"
+                    :height="size * sqrtTwo + 9"
+                    :transform="
+                        `translate(${-(size * sqrtTwo + 9) / 2}, ${-(size * sqrtTwo + 9) / 2})`
+                    "
+                    fill="transparent"
+                    :stroke-dasharray="(size * sqrtTwo + 9) * 4"
+                    :stroke-width="5"
+                    :stroke-dashoffset="
+                        (size * sqrtTwo + 9) * 4 - progress * (size * sqrtTwo + 9) * 4
+                    "
+                    :stroke="progressColor"
+                />
+            </g>
+
+            <text :fill="titleColor" class="node-title">{{ title }}</text>
+        </g>
 
         <transition name="fade" appear>
             <text
@@ -386,11 +385,6 @@ export default defineComponent({
         opacity: 0.25;
     }
 }
-
-.grow-enter-from,
-.fade-leave-to {
-    transform: scale(0);
-}
 </style>
 
 <style>
@@ -398,4 +392,9 @@ export default defineComponent({
 .actions-leave-to .action {
     transform: translate(0, 0);
 }
+
+.grow-enter-from .node-container,
+.grow-leave-to .node-container {
+    transform: scale(0);
+}
 </style>

From 5c23976ec7ae6614493288f669da4492b869fdb0 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:29:27 -0500
Subject: [PATCH 35/49] Changed "assign" color to white

---
 src/data/layers/main.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 214ba73..46620b9 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -381,7 +381,7 @@ const resourceNodeType = {
                     selectedAction.value?.id;
                 return {
                     text: currentFocus ? "10%" : "X",
-                    color: currentFocus ? "green" : "black",
+                    color: currentFocus ? "green" : "white",
                     pulsing: true
                 };
             }
@@ -398,7 +398,7 @@ const resourceNodeType = {
                 }
                 let color;
                 if (change.assign) {
-                    color = "black";
+                    color = "white";
                 } else {
                     color = Decimal.gt(change.amount, 0) ? "green" : "red";
                 }
@@ -542,14 +542,14 @@ const actionNodeType = {
                                     (resources.focus.node.data as ResourceNodeData).currentFocus ===
                                     selectedAction.value?.id
                                         ? "green"
-                                        : "black",
+                                        : "white",
                                 "stroke-width": 4,
                                 pulsing: true
                             },
                             ...action.baseChanges.map(change => {
                                 let color;
                                 if (change.assign) {
-                                    color = "black";
+                                    color = "white";
                                 } else {
                                     color = Decimal.gt(change.amount, 0) ? "green" : "red";
                                 }

From b1d2bab36ffd11a6b9bc8bd3dac9cf632a3ffd15 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:44:38 -0500
Subject: [PATCH 36/49] Fixed labels not fading out properly

---
 src/components/board/BoardNode.vue | 36 ++++++++++++++++--------------
 1 file changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index e1baf43..b644b37 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -11,7 +11,7 @@
                     v-for="(action, index) in actions"
                     :key="action.id"
                     class="action"
-                    :class="{ selected: selectedAction === action }"
+                    :class="{ selected: selectedAction?.id === action.id }"
                     :transform="
                         `translate(
                             ${(-size - 30) *
@@ -34,7 +34,7 @@
                                 : fillColor
                         "
                         r="20"
-                        :stroke-width="selectedAction === action ? 4 : 0"
+                        :stroke-width="selectedAction?.id === action.id ? 4 : 0"
                         :stroke="outlineColor"
                     />
                     <text :fill="titleColor" class="material-icons">{{
@@ -88,8 +88,7 @@
                     :width="size * sqrtTwo + 16"
                     :height="size * sqrtTwo + 16"
                     :transform="
-                        `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) /
-                            2})`
+                        `translate(${-(size * sqrtTwo + 16) / 2}, ${-(size * sqrtTwo + 16) / 2})`
                     "
                     :fill="backgroundColor"
                     :stroke="receivingNode ? '#0F0' : '#0F03'"
@@ -110,8 +109,10 @@
                     :width="Math.max(size * sqrtTwo * progress - 2, 0)"
                     :height="Math.max(size * sqrtTwo * progress - 2, 0)"
                     :transform="
-                        `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) /
-                            2}, ${-Math.max(size * sqrtTwo * progress - 2, 0) / 2})`
+                        `translate(${-Math.max(size * sqrtTwo * progress - 2, 0) / 2}, ${-Math.max(
+                            size * sqrtTwo * progress - 2,
+                            0
+                        ) / 2})`
                     "
                     :fill="progressColor"
                 />
@@ -136,22 +137,23 @@
         </g>
 
         <transition name="fade" appear>
-            <text
-                v-if="label"
-                :fill="label.color || titleColor"
-                class="node-title"
-                :class="{ pulsing: label.pulsing }"
-                :y="-size - 20"
-                >{{ label.text }}</text
-            >
+            <g v-if="label">
+                <text
+                    :fill="label.color || titleColor"
+                    class="node-title"
+                    :class="{ pulsing: label.pulsing }"
+                    :y="-size - 20"
+                    >{{ label.text }}
+                </text>
+            </g>
         </transition>
 
         <transition name="fade" appear>
             <text
+                v-if="selected && selectedAction"
                 :fill="titleColor"
                 class="node-title"
                 :y="size + 75"
-                v-if="selected && selectedAction"
                 >Tap again to confirm</text
             >
         </transition>
@@ -308,13 +310,13 @@ export default defineComponent({
         performAction(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
             // If the onClick function made this action selected,
             // don't propagate the event (which will deselect everything)
-            if (action.onClick(this.node) || this.board.selectedAction === action) {
+            if (action.onClick(this.node) || this.board.selectedAction?.id === action.id) {
                 e.preventDefault();
                 e.stopPropagation();
             }
         },
         actionMouseUp(e: MouseEvent | TouchEvent, action: BoardNodeAction) {
-            if (this.board.selectedAction === action) {
+            if (this.board.selectedAction?.id === action.id) {
                 e.preventDefault();
                 e.stopPropagation();
             }

From 7890c1c298fed9624c24eebb01507c00ad376455 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:52:23 -0500
Subject: [PATCH 37/49] Made resource colors non-transparent

---
 src/data/layers/main.ts | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 46620b9..a55b03e 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -45,11 +45,11 @@ type Resource = {
 };
 
 const resources = {
-    time: createResource("time", "#3EB48933", 24 * 60 * 60, 24 * 60 * 60),
-    energy: createResource("energy", "#FFA50033", 100, 100),
-    social: createResource("social", "#80008033", 100, 100),
-    mental: createResource("mental", "#32CD3233", 100, 100),
-    focus: createResource("focus", "#0000FF33", 100, 0)
+    time: createResource("time", "#3EB489", 24 * 60 * 60, 24 * 60 * 60),
+    energy: createResource("energy", "#FFA500", 100, 100),
+    social: createResource("social", "#800080", 100, 100),
+    mental: createResource("mental", "#32CD32", 100, 100),
+    focus: createResource("focus", "#0000FF", 100, 0)
 } as Record<string, Resource>;
 
 function createResource(
@@ -486,7 +486,7 @@ const actionNodeType = {
     fillColor: "#000",
     draggable: true,
     shape: Shape.Diamond,
-    progressColor: "#0FF3",
+    progressColor: "#000",
     progressDisplay: ProgressDisplay.Outline,
     actions(node) {
         const actionNode = actionNodes[(node.data as ActionNodeData).actionType];

From 0e4cf7ebbce49a92f3d3f2ba8ab164ec03327c14 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 00:57:33 -0500
Subject: [PATCH 38/49] Moved links into resources objects

---
 src/data/layers/main.ts | 62 +++++++++++++++++++----------------------
 1 file changed, 29 insertions(+), 33 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index a55b03e..a47fb49 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -36,16 +36,33 @@ type ActionNodeData = {
     log: LogEntry[];
 };
 
+enum LinkType {
+    LossOnly,
+    GainOnly,
+    Both
+}
+
+// Links cause gain/loss of one resource to also affect other resources
+type ResourceLink = {
+    resource: string;
+    amount: DecimalSource;
+    linkType: LinkType;
+};
+
 type Resource = {
     readonly name: string;
     readonly color: string;
     readonly node: BoardNode;
+    readonly links?: ResourceLink[];
     readonly maxAmount: DecimalSource;
     amount: DecimalSource;
 };
 
 const resources = {
-    time: createResource("time", "#3EB489", 24 * 60 * 60, 24 * 60 * 60),
+    time: createResource("time", "#3EB489", 24 * 60 * 60, 24 * 60 * 60, [
+        { resource: "social", amount: 1 / (60 * 60), linkType: LinkType.LossOnly },
+        { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly }
+    ]),
     energy: createResource("energy", "#FFA500", 100, 100),
     social: createResource("social", "#800080", 100, 100),
     mental: createResource("mental", "#32CD32", 100, 100),
@@ -56,7 +73,8 @@ function createResource(
     name: string,
     color: string,
     maxAmount: DecimalSource,
-    defaultAmount: DecimalSource
+    defaultAmount: DecimalSource,
+    links?: ResourceLink[]
 ): Resource {
     const node = computed(() =>
         player.layers.main?.boards.main.nodes.find(
@@ -67,6 +85,7 @@ function createResource(
     return {
         name,
         color,
+        links,
         get node() {
             // Should only run once, but this tricks TS into knowing node.value exists
             while (node.value == null) {
@@ -319,37 +338,16 @@ function getRandomEvent(events: WeightedEvent[]): LogEntry | null {
     return null;
 }
 
-enum LinkType {
-    LossOnly,
-    GainOnly,
-    Both
-}
-
-// Links cause gain/loss of one resource to also affect other resources
-const links = {
-    time: [
-        { resource: "social", amount: 1 / (60 * 60), linkType: LinkType.LossOnly },
-        { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly }
-    ]
-} as Record<
-    string,
-    {
-        resource: string;
-        amount: DecimalSource;
-        linkType: LinkType;
-    }[]
->;
-
-for (const resource in links) {
-    const resourceLinks = links[resource];
+for (const id in resources) {
+    const resource = resources[id];
     watch(
-        () => resources[resource].amount,
+        () => resource.amount,
         (amount, oldAmount) => {
             if (amount == null || oldAmount == null) {
                 return;
             }
             const resourceGain = Decimal.sub(amount, oldAmount);
-            resourceLinks.forEach(link => {
+            resource.links?.forEach(link => {
                 if (link.linkType === LinkType.LossOnly && Decimal.gt(amount, oldAmount)) {
                     return;
                 }
@@ -412,10 +410,8 @@ const resourceNodeType = {
             selectedNode.value.type === "resource"
         ) {
             const selectedResource = getResource(selectedNode.value);
-            if (selectedResource.name in links) {
-                const link = links[selectedResource.name].find(
-                    link => link.resource === resource.name
-                );
+            if (selectedResource.links) {
+                const link = selectedResource.links.find(link => link.resource === resource.name);
                 if (link) {
                     let text;
                     if (resource.name === "time") {
@@ -691,8 +687,8 @@ export default {
                     }
                     if (selectedNode.value.type === "resource") {
                         const resource = getResource(selectedNode.value);
-                        if (resource.name in links) {
-                            return links[resource.name].map(link => {
+                        if (resource.links) {
+                            return resource.links.map(link => {
                                 const linkResource = resources[link.resource];
                                 let negativeLink = Decimal.lt(link.amount, 0);
                                 if (link.linkType === LinkType.LossOnly) {

From 116e6a47ec2f847e9027afd1c5bcecf8f63ecd16 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 18:15:05 -0500
Subject: [PATCH 39/49] Added nordic theme

---
 src/data/themes.ts | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/src/data/themes.ts b/src/data/themes.ts
index 35b8eaa..0e38c30 100644
--- a/src/data/themes.ts
+++ b/src/data/themes.ts
@@ -23,6 +23,7 @@ const defaultTheme: Theme = {
 export enum Themes {
     Classic = "classic",
     Paper = "paper",
+    Nordic = "nordic",
     Aqua = "aqua"
 }
 
@@ -44,6 +45,27 @@ export default {
         stackedInfoboxes: true,
         floatingTabs: false
     } as Theme,
+    // Based on https://www.nordtheme.com
+    nordic: {
+        ...defaultTheme,
+        variables: {
+            ...defaultTheme.variables,
+            "--color": "#D8DEE9",
+            "--points": "#E5E9F0",
+            "--background": "#2E3440",
+            "--secondary-background": "#3B4252",
+            "--locked": "#3B4252",
+            "--bought": "#8FBCBB",
+            "--link": "#88C0D0",
+            "--separator": "#3B4252",
+            "--border-radius": "4px",
+            "--danger": "#D08770",
+            "--modal-border": "solid 2px #3B4252",
+            "--feature-margin": "5px"
+        },
+        stackedInfoboxes: true,
+        floatingTabs: false
+    } as Theme,
     aqua: {
         ...defaultTheme,
         variables: {

From f516a3a09282c504a2c9ea9cef4515e4959aa5b4 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 22:01:56 -0500
Subject: [PATCH 40/49] Implemented force sleep and rest actions

---
 src/components/board/BoardNode.vue |  15 +-
 src/data/layers/main.ts            | 260 +++++++++++++++++++++--------
 2 files changed, 207 insertions(+), 68 deletions(-)

diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index b644b37..ac2d12e 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -1,6 +1,7 @@
 <template>
     <g
         class="boardnode"
+        :class="node.type"
         :style="{ opacity: dragging?.id === node.id && hasDragged ? 0.5 : 1 }"
         :transform="`translate(${position.x},${position.y})`"
     >
@@ -56,15 +57,23 @@
             <g v-if="shape === Shape.Circle">
                 <circle
                     v-if="canAccept"
+                    class="receiver"
                     :r="size + 8"
                     :fill="backgroundColor"
                     :stroke="receivingNode ? '#0F0' : '#0F03'"
                     :stroke-width="2"
                 />
 
-                <circle :r="size" :fill="fillColor" :stroke="outlineColor" :stroke-width="4" />
+                <circle
+                    class="body"
+                    :r="size"
+                    :fill="fillColor"
+                    :stroke="outlineColor"
+                    :stroke-width="4"
+                />
 
                 <circle
+                    class="progressFill"
                     v-if="progressDisplay === ProgressDisplay.Fill"
                     :r="Math.max(size * progress - 2, 0)"
                     :fill="progressColor"
@@ -85,6 +94,7 @@
             <g v-else-if="shape === Shape.Diamond" transform="rotate(45, 0, 0)">
                 <rect
                     v-if="canAccept"
+                    class="receiver"
                     :width="size * sqrtTwo + 16"
                     :height="size * sqrtTwo + 16"
                     :transform="
@@ -96,6 +106,7 @@
                 />
 
                 <rect
+                    class="body"
                     :width="size * sqrtTwo"
                     :height="size * sqrtTwo"
                     :transform="`translate(${(-size * sqrtTwo) / 2}, ${(-size * sqrtTwo) / 2})`"
@@ -106,6 +117,7 @@
 
                 <rect
                     v-if="progressDisplay === ProgressDisplay.Fill"
+                    class="progressFill"
                     :width="Math.max(size * sqrtTwo * progress - 2, 0)"
                     :height="Math.max(size * sqrtTwo * progress - 2, 0)"
                     :transform="
@@ -118,6 +130,7 @@
                 />
                 <rect
                     v-else
+                    class="progressDiamond"
                     :width="size * sqrtTwo + 9"
                     :height="size * sqrtTwo + 9"
                     :transform="
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index a47fb49..c682033 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -31,7 +31,7 @@ type ItemNodeData = {
     display: string;
 };
 
-type ActionNodeData = {
+export type ActionNodeData = {
     actionType: string;
     log: LogEntry[];
 };
@@ -147,14 +147,14 @@ function createItem(resource: string, amount: DecimalSource, display?: string) {
 }
 
 type Action = {
-    icon: string;
+    icon?: string;
     fillColor?: string;
     tooltip?: string;
-    events: Array<{
+    events?: Array<{
         event: () => LogEntry;
         weight: number;
     }>;
-    baseChanges: Array<{
+    baseChanges?: Array<{
         resource: string;
         amount: DecimalSource;
         assign?: boolean;
@@ -194,11 +194,15 @@ const actions = {
         tooltip: "Sleep",
         events: [
             {
-                event: () => ({ description: "You have a normal evening of undisturbed sleep" }),
+                event: () => {
+                    player.day = (player.day as Decimal).add(1);
+                    return { description: "You have a normal evening of undisturbed sleep" };
+                },
                 weight: 90
             },
             {
                 event: () => {
+                    player.day = (player.day as Decimal).add(1);
                     resources.energy.amount = 50;
                     return {
                         description: "You had a very restless sleep filled with nightmares :(",
@@ -211,6 +215,7 @@ const actions = {
             },
             {
                 event: () => {
+                    player.day = (player.day as Decimal).add(1);
                     createItem("energy", 25, "Refreshed");
                     return {
                         description:
@@ -224,10 +229,28 @@ const actions = {
             }
         ],
         baseChanges: [
-            { resource: "time", amount: -8 * 30 * 60 },
+            { resource: "time", amount: 16 * 60 * 60, assign: true },
             { resource: "energy", amount: 100, assign: true }
         ]
     },
+    forcedSleep: {
+        events: [
+            {
+                event: () => {
+                    const amount = resources.time.amount;
+                    resources.time.amount = 16 * 60 * 60;
+                    resources.energy.amount = Decimal.sub(100, Decimal.div(amount, 6 * 60));
+                    resources.mental.amount = Decimal.sub(resources.mental.amount, 10);
+                    player.day = (player.day as Decimal).add(1);
+                    return {
+                        description: `You passed out! That was <span style="font-style: italicize">not</span> a good night's sleep`
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [{ resource: "mental", amount: -10 }]
+    },
     rest: {
         icon: "chair",
         tooltip: "Rest",
@@ -259,18 +282,33 @@ const actions = {
                     return {
                         description:
                             "You take an incredible power nap and wake up significantly more refreshed",
-                        effectDescription: `+50% effectvie <span style="color: ${resources.energy.color};">Energy</span> restoration`
+                        effectDescription: `+50% effective <span style="color: ${resources.energy.color};">Energy</span> restoration`
                     };
                 },
                 weight: 5
             }
         ],
         baseChanges: [
-            { resource: "time", amount: -4 * 30 * 60 },
+            { resource: "time", amount: -4 * 60 * 60 },
             // 30 is the lowest it can be from any event
             // typically you'll get 40 though
             { resource: "energy", amount: 30 }
         ]
+    },
+    forcedRest: {
+        events: [
+            {
+                event: () => ({
+                    description: `You drag yourself to the couch before collapsing. You wake back up but still feel oddly drained`
+                }),
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -6 * 60 * 60 },
+            { resource: "energy", amount: 30 },
+            { resource: "mental", amount: -5 }
+        ]
     }
 } as Record<string, Action>;
 
@@ -378,21 +416,21 @@ const resourceNodeType = {
                     (resource.node.data as ResourceNodeData).currentFocus ===
                     selectedAction.value?.id;
                 return {
-                    text: currentFocus ? "10%" : "X",
+                    text: "10%",
                     color: currentFocus ? "green" : "white",
                     pulsing: true
                 };
             }
             const action = actions[selectedAction.value.id];
-            const change = action.baseChanges.find(change => change.resource === resource.name);
+            const change = action.baseChanges?.find(change => change.resource === resource.name);
             if (change != null) {
-                let text;
+                let text = Decimal.gt(change.amount, 0) ? "+" : "";
                 if (resource.name === "time") {
-                    text = formatTime(change.amount);
+                    text += formatTime(change.amount);
                 } else if (Decimal.eq(resource.maxAmount, 100)) {
-                    text = formatWhole(change.amount) + "%";
+                    text += formatWhole(change.amount) + "%";
                 } else {
-                    text = format(change.amount);
+                    text += format(change.amount);
                 }
                 let color;
                 if (change.assign) {
@@ -470,6 +508,44 @@ const resourceNodeType = {
     actions: [pinAction]
 } as RawFeature<NodeType>;
 
+function performAction(id: string, action: Action, node: BoardNode) {
+    if (
+        player.layers.main.forcedAction &&
+        (player.layers.main.forcedAction as { action: string }).action !== id
+    ) {
+        return;
+    }
+    const focusData = resources.focus.node.data as ResourceNodeData;
+    if (focusData.currentFocus === id) {
+        resources.focus.amount = Decimal.add(resources.focus.amount, 10);
+    } else {
+        focusData.currentFocus = id;
+        resources.focus.amount = 10;
+    }
+    if (action.baseChanges) {
+        for (const change of action.baseChanges) {
+            if (change.assign) {
+                resources[change.resource].amount = change.amount;
+            } else if (change.resource === "time") {
+                // Time isn't affected by focus multiplier
+                resources.time.amount = Decimal.add(resources.time.amount, change.amount);
+            } else {
+                resources[change.resource].amount = Decimal.add(
+                    resources[change.resource].amount,
+                    Decimal.times(change.amount, focusMult.value)
+                );
+            }
+        }
+    }
+    player.layers.main.boards.main.selectedAction = null;
+    if (action.events) {
+        const logEntry = getRandomEvent(action.events);
+        if (logEntry) {
+            (node.data as ActionNodeData).log.push(logEntry);
+        }
+    }
+}
+
 const actionNodeType = {
     title(node) {
         return actionNodes[(node.data as ActionNodeData).actionType].display;
@@ -484,6 +560,17 @@ const actionNodeType = {
     shape: Shape.Diamond,
     progressColor: "#000",
     progressDisplay: ProgressDisplay.Outline,
+    progress(node) {
+        const forcedAction = player.layers.main.forcedAction as {
+            resource: string;
+            node: number;
+            action: string;
+            progress: number;
+        } | null;
+        if (forcedAction && node.id === forcedAction.node) {
+            return forcedAction.progress;
+        }
+    },
     actions(node) {
         const actionNode = actionNodes[(node.data as ActionNodeData).actionType];
         return [
@@ -495,38 +582,11 @@ const actionNodeType = {
                     icon: action.icon,
                     tooltip: action.tooltip,
                     fillColor: action.fillColor,
-                    onClick(node) {
-                        if (selectedAction.value?.id === this.id) {
-                            const focusData = resources.focus.node.data as ResourceNodeData;
-                            if (focusData.currentFocus === id) {
-                                resources.focus.amount = Decimal.add(resources.focus.amount, 10);
-                            } else {
-                                focusData.currentFocus = id;
-                                resources.focus.amount = 10;
-                            }
-                            for (const change of action.baseChanges) {
-                                if (change.assign) {
-                                    resources[change.resource].amount = change.amount;
-                                } else if (change.resource === "time") {
-                                    // Time isn't affected by focus multiplier
-                                    resources.time.amount = Decimal.add(
-                                        resources.time.amount,
-                                        change.amount
-                                    );
-                                } else {
-                                    resources.time.amount = Decimal.add(
-                                        resources.time.amount,
-                                        Decimal.times(change.amount, focusMult.value)
-                                    );
-                                }
-                            }
-                            player.layers.main.boards.main.selectedAction = null;
-                            const logEntry = getRandomEvent(action.events);
-                            if (logEntry) {
-                                (node.data as ActionNodeData).log.push(logEntry);
-                            }
+                    onClick: node => {
+                        if (selectedAction.value?.id === id) {
+                            performAction(id, action, node);
                         } else {
-                            player.layers.main.boards.main.selectedAction = this.id;
+                            player.layers.main.boards.main.selectedAction = id;
                         }
                     },
                     links(node) {
@@ -542,7 +602,7 @@ const actionNodeType = {
                                 "stroke-width": 4,
                                 pulsing: true
                             },
-                            ...action.baseChanges.map(change => {
+                            ...(action.baseChanges || []).map(change => {
                                 let color;
                                 if (change.assign) {
                                     color = "white";
@@ -565,6 +625,32 @@ const actionNodeType = {
     }
 } as RawFeature<NodeType>;
 
+function registerResourceDepletedAction(resource: string, nodeID: string, action: string) {
+    watch(
+        () => ({
+            amount: resources[resource].amount,
+            forcedAction: player.layers.main?.forcedAction
+        }),
+        ({ amount, forcedAction }) => {
+            if (Decimal.eq(amount, 0) && forcedAction == null) {
+                player.layers.main.forcedAction = {
+                    resource,
+                    node: layers.main.boards!.data.main.nodes.find(
+                        node =>
+                            node.type === "action" &&
+                            (node.data as ActionNodeData).actionType === nodeID
+                    )!.id,
+                    action,
+                    progress: 0
+                };
+            }
+        }
+    );
+}
+
+registerResourceDepletedAction("time", "bed", "forcedSleep");
+registerResourceDepletedAction("energy", "bed", "forcedRest");
+
 export default {
     id: "main",
     display: Main,
@@ -572,10 +658,17 @@ export default {
     startData() {
         return {
             openNode: null,
-            showModal: false
+            showModal: false,
+            forcedAction: null
         } as {
             openNode: string | null;
             showModal: boolean;
+            forcedAction: {
+                resource: string;
+                node: number;
+                action: string;
+                progress: number;
+            } | null;
         };
     },
     minimizable: false,
@@ -586,6 +679,25 @@ export default {
             left: "0"
         }
     },
+    update(diff) {
+        const forcedAction = player.layers.main.forcedAction as {
+            resource: string;
+            node: number;
+            action: string;
+            progress: number;
+        } | null;
+        if (forcedAction) {
+            forcedAction.progress += new Decimal(diff).div(4).toNumber();
+            if (forcedAction.progress >= 1) {
+                performAction(
+                    forcedAction.action,
+                    actions[forcedAction.action],
+                    this.boards!.data.main.nodes.find(node => node.id === forcedAction.node)!
+                );
+                player.layers.main.forcedAction = null;
+            }
+        }
+    },
     boards: {
         data: {
             main: {
@@ -676,35 +788,49 @@ export default {
                     }
                 },
                 links(this: Board) {
+                    const links: BoardNodeLink[] = [];
+                    const forcedAction = player.layers.main.forcedAction as {
+                        resource: string;
+                        node: number;
+                        action: string;
+                        progress: number;
+                    } | null;
+                    if (forcedAction) {
+                        links.push({
+                            from: resources[forcedAction.resource].node,
+                            to: this.nodes.find(node => node.id === forcedAction.node)!,
+                            stroke: "black",
+                            "stroke-width": 4
+                        });
+                    }
                     if (this.selectedNode && this.selectedAction?.links) {
                         if (typeof this.selectedAction.links === "function") {
                             return this.selectedAction.links(this.selectedNode);
                         }
-                        return this.selectedAction.links;
+                        links.push(...this.selectedAction.links);
                     }
-                    if (selectedNode.value == null) {
-                        return null;
-                    }
-                    if (selectedNode.value.type === "resource") {
+                    if (selectedNode.value && selectedNode.value.type === "resource") {
                         const resource = getResource(selectedNode.value);
                         if (resource.links) {
-                            return resource.links.map(link => {
-                                const linkResource = resources[link.resource];
-                                let negativeLink = Decimal.lt(link.amount, 0);
-                                if (link.linkType === LinkType.LossOnly) {
-                                    negativeLink = !negativeLink;
-                                }
-                                return {
-                                    from: selectedNode.value,
-                                    to: linkResource.node,
-                                    stroke: negativeLink ? "red" : "green",
-                                    "stroke-width": 4,
-                                    pulsing: true
-                                };
-                            });
+                            links.push(
+                                ...resource.links.map(link => {
+                                    const linkResource = resources[link.resource];
+                                    let negativeLink = Decimal.lt(link.amount, 0);
+                                    if (link.linkType === LinkType.LossOnly) {
+                                        negativeLink = !negativeLink;
+                                    }
+                                    return {
+                                        from: selectedNode.value,
+                                        to: linkResource.node,
+                                        stroke: negativeLink ? "red" : "green",
+                                        "stroke-width": 4,
+                                        pulsing: true
+                                    } as BoardNodeLink;
+                                })
+                            );
                         }
                     }
-                    return null;
+                    return links;
                 }
             }
         }

From fdfccefb67461fdf0033d45a129d6cb0361e7b0e Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 22:24:56 -0500
Subject: [PATCH 41/49] Removed social resource and added make bed action plus
 some stuff I forgot to add in the last commit

---
 src/components/board/Board.vue     |  2 +-
 src/components/board/BoardNode.vue | 27 +++++++++++++++-----
 src/data/layers/Main.vue           |  9 ++++++-
 src/data/layers/main.ts            | 40 ++++++++++++++++++++----------
 src/data/mod.ts                    |  5 ++--
 src/typings/player.d.ts            |  2 +-
 6 files changed, 61 insertions(+), 24 deletions(-)

diff --git a/src/components/board/Board.vue b/src/components/board/Board.vue
index 5d60a07..445f014 100644
--- a/src/components/board/Board.vue
+++ b/src/components/board/Board.vue
@@ -16,7 +16,7 @@
         <svg class="stage" width="100%" height="100%">
             <g id="g1">
                 <transition-group name="link" appear>
-                    <g v-for="(link, index) in board.links || []" :key="index">
+                    <g v-for="link in board.links || []" :key="link">
                         <BoardLink :link="link" />
                     </g>
                 </transition-group>
diff --git a/src/components/board/BoardNode.vue b/src/components/board/BoardNode.vue
index ac2d12e..ea726a7 100644
--- a/src/components/board/BoardNode.vue
+++ b/src/components/board/BoardNode.vue
@@ -229,7 +229,22 @@ export default defineComponent({
             return this.board.selectedAction;
         },
         actions(): BoardNodeAction[] | null | undefined {
-            return getNodeTypeProperty(this.nodeType, this.node, "actions");
+            const actions = getNodeTypeProperty(this.nodeType, this.node, "actions") as
+                | BoardNodeAction[]
+                | null
+                | undefined;
+            if (actions) {
+                return actions.filter(action => {
+                    if (action.enabled == null) {
+                        return true;
+                    }
+                    if (typeof action.enabled === "function") {
+                        return action.enabled();
+                    }
+                    return action.enabled;
+                });
+            }
+            return null;
         },
         draggable(): boolean {
             return getNodeTypeProperty(this.nodeType, this.node, "draggable");
@@ -363,17 +378,17 @@ export default defineComponent({
     transform: rotate(-90deg);
 }
 
-.action:hover circle,
-.action.selected circle {
+.action:not(.boardnode):hover circle,
+.action:not(.boardnode).selected circle {
     r: 25;
 }
 
-.action:hover text,
-.action.selected text {
+.action:not(.boardnode):hover text,
+.action:not(.boardnode).selected text {
     font-size: 187.5%; /* 150% * 1.25 */
 }
 
-.action text {
+.action:not(.boardnode) text {
     text-anchor: middle;
     dominant-baseline: central;
 }
diff --git a/src/data/layers/Main.vue b/src/data/layers/Main.vue
index c27b588..045bf48 100644
--- a/src/data/layers/Main.vue
+++ b/src/data/layers/Main.vue
@@ -1,6 +1,7 @@
 <template>
     <div v-if="devSpeed === 0">Game Paused</div>
     <div v-else-if="devSpeed && devSpeed !== 1">Dev Speed: {{ formattedDevSpeed }}x</div>
+    <div>Day {{ day }}</div>
     <Board id="main" />
     <Modal :show="showModal" @close="closeModal">
         <template v-slot:header v-if="title">
@@ -70,7 +71,9 @@ export default defineComponent(function Main() {
     const devSpeed = computed(() => player.devSpeed);
     const formattedDevSpeed = computed(() => player.devSpeed && format(player.devSpeed));
 
-    return { title, body, footer, showModal, closeModal, devSpeed, formattedDevSpeed };
+    const day = computed(() => player.day);
+
+    return { title, body, footer, showModal, closeModal, devSpeed, formattedDevSpeed, day };
 });
 </script>
 
@@ -82,4 +85,8 @@ export default defineComponent(function Main() {
 .entry:not(:last-child) {
     border-bottom: solid 4px var(--separator);
 }
+
+.boardnode.action .progressDiamond {
+    transition-duration: 0s;
+}
 </style>
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index c682033..79d001a 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -60,11 +60,9 @@ type Resource = {
 
 const resources = {
     time: createResource("time", "#3EB489", 24 * 60 * 60, 24 * 60 * 60, [
-        { resource: "social", amount: 1 / (60 * 60), linkType: LinkType.LossOnly },
         { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly }
     ]),
     energy: createResource("energy", "#FFA500", 100, 100),
-    social: createResource("social", "#800080", 100, 100),
     mental: createResource("mental", "#32CD32", 100, 100),
     focus: createResource("focus", "#0000FF", 100, 0)
 } as Record<string, Resource>;
@@ -159,6 +157,7 @@ type Action = {
         amount: DecimalSource;
         assign?: boolean;
     }>;
+    enabled?: boolean | (() => boolean);
 };
 
 const actions = {
@@ -190,7 +189,7 @@ const actions = {
         ]
     },
     sleep: {
-        icon: "bed",
+        icon: "mode_night",
         tooltip: "Sleep",
         events: [
             {
@@ -309,6 +308,28 @@ const actions = {
             { resource: "energy", amount: 30 },
             { resource: "mental", amount: -5 }
         ]
+    },
+    makeBed: {
+        icon: "king_bed",
+        tooltip: "Make Bed",
+        enabled: () =>
+            Decimal.lt(player.lastDayBedMade as DecimalSource, player.day as DecimalSource),
+        events: [
+            {
+                event: () => {
+                    player.lastDayBedMade = player.day;
+                    return {
+                        description: `It's a small thing, but you feel better after making your bed`
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -10 * 60 },
+            { resource: "energy", amount: -5 },
+            { resource: "mental", amount: 5 }
+        ]
     }
 } as Record<string, Action>;
 
@@ -352,7 +373,7 @@ const actionNodes = {
         display: "Web"
     },
     bed: {
-        actions: ["sleep", "rest"],
+        actions: ["sleep", "rest", "makeBed"],
         display: "Bed"
     }
 } as Record<string, ActionNode>;
@@ -618,7 +639,8 @@ const actionNodeType = {
                                 } as BoardNodeLink;
                             })
                         ];
-                    }
+                    },
+                    enabled: action.enabled
                 } as BoardNodeAction;
             })
         ];
@@ -720,14 +742,6 @@ export default {
                                 amount: new Decimal(100)
                             } as ResourceNodeData
                         },
-                        {
-                            position: { x: 150, y: 0 },
-                            type: "resource",
-                            data: {
-                                resourceType: "social",
-                                amount: new Decimal(100)
-                            } as ResourceNodeData
-                        },
                         {
                             position: { x: -150, y: 0 },
                             type: "resource",
diff --git a/src/data/mod.ts b/src/data/mod.ts
index 3035aee..e6d3e97 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -1,7 +1,6 @@
 import { RawLayer } from "@/typings/layer";
 import { PlayerData } from "@/typings/player";
 import Decimal from "@/util/bignum";
-import { hardReset } from "@/util/save";
 import { computed } from "vue";
 import main from "./layers/main";
 
@@ -12,7 +11,9 @@ export const getInitialLayers = (
 
 export function getStartingData(): Record<string, unknown> {
     return {
-        points: new Decimal(10)
+        points: new Decimal(10),
+        day: new Decimal(1),
+        lastDayBedMade: new Decimal(0)
     };
 }
 
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
index c901591..71d6748 100644
--- a/src/typings/player.d.ts
+++ b/src/typings/player.d.ts
@@ -1,7 +1,7 @@
 import { Themes } from "@/data/themes";
 import { DecimalSource } from "@/lib/break_eternity";
 import Decimal from "@/util/bignum";
-import { BoardData, BoardNode } from "./features/board";
+import { BoardData } from "./features/board";
 import { MilestoneDisplay } from "./features/milestone";
 import { State } from "./state";
 

From 9313748f210a66d385b29fa05e661fe1fbf756dc Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Thu, 26 Aug 2021 23:47:53 -0500
Subject: [PATCH 42/49] Added develop action

---
 src/data/layers/main.ts | 67 ++++++++++++++++++++++++++++++++++++-----
 src/data/mod.ts         |  3 +-
 src/game/player.ts      |  2 +-
 src/util/save.ts        |  2 +-
 4 files changed, 63 insertions(+), 11 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 79d001a..b0c7dc3 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -160,7 +160,56 @@ type Action = {
     enabled?: boolean | (() => boolean);
 };
 
+const developSteps = [
+    "Spring break just started, and I've got no real obligations! Time to start working on a new project! Just gotta keep it scoped small enough this time so I can actually finish before school starts back up.",
+    "Created a new repo! I even added a README and LICENSE!",
+    "I created an index.html file, and a main.css and main.js",
+    "Thought about what the game should actually be about. Robots?",
+    `Actually had a better idea: <span style="font-style: italic">Ninjas</span>!`,
+    "Ok Ok that was a bit ridiculous. I'm pretty sure I'm actually going to create a game about... game development! It's a perfect and original idea!",
+    "Hmm, what if it involved starting in a garage then growing to become a AAA studio?",
+    "Or, what if it was more abstract. It could use different development-related features in a sort of tree structure, and the numbers get increasingly absurd!",
+    `No, that won't work. What if it got <span style="font-style: italic">too</span> ridiculous? Or got really boring towards the end? What would the end game even be? Probably something silly, that's what`,
+    `It could be self-documenting. A game about its own development process? Or maybe its narrated, and following the path of a developer over time. That could be something <a href="https://store.steampowered.com/app/303210/The_Beginners_Guide/">really special</href>.`,
+    "Maybe meta games are passé these days. How about I start with some first person shooter",
+    "You know what? I'll figure it out as I go along. Let's start with stuff any game would need",
+    "Made an options screen!",
+    "Made a credits screen! It's just me lol",
+    "Added a new option to the options screen! That's adding real value to the game!",
+    "Made a fancy title screen, minus the title itself!",
+    "I changed around some of the colors in the credits screen. Am I procrastinating?",
+    "Thought hard about a core mechanic. Decided it'd be better to just pick a theme that sounds cool and assume the mechanics will follow",
+    "Trying to come up with a theme, but I keep thinking of pokémon, and I'm pretty sure Nintendo will sue me if I make a fangame",
+    "Screw it I'm making pong",
+    "Added a paddle that you can control with the mouse!",
+    "Added a ball that bounces off the paddle",
+    "Added an enemy paddle - it doesn't move yet",
+    "Made the enemy paddle just move up and down in a loop",
+    "Added a number at the top that goes up every time your paddle hits the ball",
+    "Made the enemy paddle and ball move faster over time",
+    "Made the enemy paddle move in the direction of the ball",
+    `Thought of what to add next. I can't <span style="font-style: italic">just</span> make pong!`
+];
+
 const actions = {
+    develop: {
+        icon: "code",
+        tooltip: "Develop",
+        events: [
+            {
+                event() {
+                    const description = developSteps[player.devStep as number];
+                    (player.devStep as number)++;
+                    return { description };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -60 * 60 },
+            { resource: "energy", amount: -5 }
+        ]
+    },
     reddit: {
         icon: "reddit",
         tooltip: "Browse Reddit",
@@ -368,9 +417,9 @@ type ActionNode = {
 };
 
 const actionNodes = {
-    web: {
-        actions: ["reddit"],
-        display: "Web"
+    pc: {
+        actions: ["develop", "reddit"],
+        display: "PC"
     },
     bed: {
         actions: ["sleep", "rest", "makeBed"],
@@ -447,7 +496,7 @@ const resourceNodeType = {
             if (change != null) {
                 let text = Decimal.gt(change.amount, 0) ? "+" : "";
                 if (resource.name === "time") {
-                    text += formatTime(change.amount);
+                    text += formatTime(Decimal.div(change.amount, focusMult.value));
                 } else if (Decimal.eq(resource.maxAmount, 100)) {
                     text += formatWhole(change.amount) + "%";
                 } else {
@@ -548,12 +597,14 @@ function performAction(id: string, action: Action, node: BoardNode) {
             if (change.assign) {
                 resources[change.resource].amount = change.amount;
             } else if (change.resource === "time") {
-                // Time isn't affected by focus multiplier
-                resources.time.amount = Decimal.add(resources.time.amount, change.amount);
+                resources.time.amount = Decimal.add(
+                    resources.time.amount,
+                    Decimal.div(change.amount, focusMult.value)
+                );
             } else {
                 resources[change.resource].amount = Decimal.add(
                     resources[change.resource].amount,
-                    Decimal.times(change.amount, focusMult.value)
+                    change.amount
                 );
             }
         }
@@ -763,7 +814,7 @@ export default {
                             position: { x: -150, y: 150 },
                             type: "action",
                             data: {
-                                actionType: "web",
+                                actionType: "pc",
                                 log: []
                             } as ActionNodeData
                         },
diff --git a/src/data/mod.ts b/src/data/mod.ts
index e6d3e97..325fd57 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -13,7 +13,8 @@ export function getStartingData(): Record<string, unknown> {
     return {
         points: new Decimal(10),
         day: new Decimal(1),
-        lastDayBedMade: new Decimal(0)
+        lastDayBedMade: new Decimal(0),
+        devStep: 0
     };
 }
 
diff --git a/src/game/player.ts b/src/game/player.ts
index aabeb10..bf029cd 100644
--- a/src/game/player.ts
+++ b/src/game/player.ts
@@ -22,7 +22,7 @@ const state = reactive<PlayerData>({
     showTPS: true,
     msDisplay: MilestoneDisplay.All,
     hideChallenges: false,
-    theme: Themes.Paper,
+    theme: Themes.Nordic,
     subtabs: {},
     minimized: {},
     modID: "",
diff --git a/src/util/save.ts b/src/util/save.ts
index 0b14d81..f87b334 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -25,7 +25,7 @@ export function getInitialStore(playerData: Partial<PlayerData> = {}): PlayerDat
             showTPS: true,
             msDisplay: MilestoneDisplay.All,
             hideChallenges: false,
-            theme: Themes.Paper,
+            theme: Themes.Nordic,
             subtabs: {},
             minimized: {},
             modID: modInfo.id,

From 029cd534b179def3e478721d326d670db2b3c534 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 27 Aug 2021 00:18:34 -0500
Subject: [PATCH 43/49] Added notifications to actions

---
 package-lock.json       | 15 +++++++++++++++
 package.json            |  1 +
 src/components/index.ts |  9 ++++++---
 src/data/layers/main.ts | 36 +++++++++++++++++++++++-------------
 src/main.css            |  4 ++++
 5 files changed, 49 insertions(+), 16 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1871ec0..dbfe0eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
         "vue-panzoom": "^1.1.6",
         "vue-sortable": "github:Netbel/vue-sortable#master-fix",
         "vue-textarea-autosize": "^1.1.1",
+        "vue-toastification": "^2.0.0-rc.1",
         "vue-transition-expand": "^0.1.0"
       },
       "devDependencies": {
@@ -27325,6 +27326,14 @@
       "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.",
       "hasInstallScript": true
     },
+    "node_modules/vue-toastification": {
+      "version": "2.0.0-rc.1",
+      "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.1.tgz",
+      "integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==",
+      "peerDependencies": {
+        "vue": "^3.0.2"
+      }
+    },
     "node_modules/vue-transition-expand": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
@@ -41121,6 +41130,12 @@
         }
       }
     },
+    "vue-toastification": {
+      "version": "2.0.0-rc.1",
+      "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.1.tgz",
+      "integrity": "sha512-hjauv/FyesNZdwcr5m1SCyvu1JmlB+Ts5bTptDLDmsYYlj6Oqv8NYakiElpCF+Abwkn9J/AChh6FwkTL1HOb7Q==",
+      "requires": {}
+    },
     "vue-transition-expand": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/vue-transition-expand/-/vue-transition-expand-0.1.0.tgz",
diff --git a/package.json b/package.json
index f9fbcff..3100357 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "vue-panzoom": "^1.1.6",
     "vue-sortable": "github:Netbel/vue-sortable#master-fix",
     "vue-textarea-autosize": "^1.1.1",
+    "vue-toastification": "^2.0.0-rc.1",
     "vue-transition-expand": "^0.1.0"
   },
   "devDependencies": {
diff --git a/src/components/index.ts b/src/components/index.ts
index 408338f..bbc74f5 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -2,12 +2,14 @@
 // 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 { App } from "vue";
 import VueNextSelect from "vue-next-select";
 import "vue-next-select/dist/index.css";
 import panZoom from "vue-panzoom";
-import { App } from "vue";
+import Sortable from "vue-sortable";
+import VueTextareaAutosize from "vue-textarea-autosize";
+import Toast from "vue-toastification";
+import "vue-toastification/dist/index.css";
 
 export function registerComponents(vue: App): void {
     /* from files */
@@ -25,4 +27,5 @@ export function registerComponents(vue: App): void {
     vue.use(Sortable);
     vue.component("vue-select", VueNextSelect);
     vue.use(panZoom);
+    vue.use(Toast);
 }
diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index b0c7dc3..d404632 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -16,9 +16,12 @@ import { format, formatWhole } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { getUniqueNodeID } from "@/util/features";
 import { computed, watch } from "vue";
+import { useToast } from "vue-toastification";
 import themes from "../themes";
 import Main from "./Main.vue";
 
+const toast = useToast();
+
 type ResourceNodeData = {
     resourceType: string;
     amount: DecimalSource;
@@ -129,7 +132,7 @@ export type LogEntry = {
 
 export type WeightedEvent = {
     event: () => LogEntry;
-    weight: number;
+    weight: number | (() => number);
 };
 
 function createItem(resource: string, amount: DecimalSource, display?: string) {
@@ -148,10 +151,7 @@ type Action = {
     icon?: string;
     fillColor?: string;
     tooltip?: string;
-    events?: Array<{
-        event: () => LogEntry;
-        weight: number;
-    }>;
+    events?: Array<WeightedEvent>;
     baseChanges?: Array<{
         resource: string;
         amount: DecimalSource;
@@ -254,11 +254,11 @@ const actions = {
                     resources.energy.amount = 50;
                     return {
                         description: "You had a very restless sleep filled with nightmares :(",
-                        effectDescription: `50% <span style="color: ${resources.energy.color};">Energy</span>`
+                        effectDescription: `50% <span style="color: ${resources.energy.color};">Energy</span> `
                     };
                 },
                 weight() {
-                    return Decimal.sub(100, resources.mental.amount || 100);
+                    return Decimal.sub(100, resources.mental.amount);
                 }
             },
             {
@@ -272,7 +272,7 @@ const actions = {
                     };
                 },
                 weight() {
-                    return Decimal.sub(resources.mental.amount || 100, 75).max(5);
+                    return Decimal.sub(resources.mental.amount, 75).max(5);
                 }
             }
         ],
@@ -431,13 +431,22 @@ function getRandomEvent(events: WeightedEvent[]): LogEntry | null {
     if (events.length === 0) {
         return null;
     }
-    const totalWeight = events.reduce((acc, curr) => acc + curr.weight, 0);
-    const random = Math.random() * totalWeight;
+    const totalWeight = events.reduce((acc, curr) => {
+        let weight = curr.weight;
+        if (typeof weight === "function") {
+            weight = weight();
+        }
+        return Decimal.add(acc, weight);
+    }, new Decimal(0));
+    const random = Decimal.times(Math.random(), totalWeight);
 
-    let weight = 0;
+    let weight = new Decimal(0);
     for (const outcome of events) {
-        weight += outcome.weight;
-        if (random <= weight) {
+        weight = Decimal.add(
+            weight,
+            typeof outcome.weight === "function" ? outcome.weight() : outcome.weight
+        );
+        if (Decimal.lte(random, weight)) {
             return outcome.event();
         }
     }
@@ -613,6 +622,7 @@ function performAction(id: string, action: Action, node: BoardNode) {
     if (action.events) {
         const logEntry = getRandomEvent(action.events);
         if (logEntry) {
+            toast.info(logEntry.description);
             (node.data as ActionNodeData).log.push(logEntry);
         }
     }
diff --git a/src/main.css b/src/main.css
index 519e0e2..60188bd 100644
--- a/src/main.css
+++ b/src/main.css
@@ -62,3 +62,7 @@ a:hover,
 ul {
 	list-style-type: none;
 }
+
+.Vue-Toastification__toast {
+	margin: unset;
+}

From 6b5c94d62ea8ca54b7b5d5625ea9b99b0498a8c5 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Fri, 27 Aug 2021 00:22:26 -0500
Subject: [PATCH 44/49] Added warning notifications when resources deplete

---
 src/data/layers/main.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index d404632..efd70a1 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -15,6 +15,7 @@ import { formatTime } from "@/util/bignum";
 import { format, formatWhole } from "@/util/break_eternity";
 import { camelToTitle } from "@/util/common";
 import { getUniqueNodeID } from "@/util/features";
+import { coerceComponent } from "@/util/vue";
 import { computed, watch } from "vue";
 import { useToast } from "vue-toastification";
 import themes from "../themes";
@@ -291,7 +292,7 @@ const actions = {
                     resources.mental.amount = Decimal.sub(resources.mental.amount, 10);
                     player.day = (player.day as Decimal).add(1);
                     return {
-                        description: `You passed out! That was <span style="font-style: italicize">not</span> a good night's sleep`
+                        description: `You passed out! That was <span style="font-style: italic">not</span> a good night's sleep`
                     };
                 },
                 weight: 1
@@ -622,7 +623,7 @@ function performAction(id: string, action: Action, node: BoardNode) {
     if (action.events) {
         const logEntry = getRandomEvent(action.events);
         if (logEntry) {
-            toast.info(logEntry.description);
+            toast.info(coerceComponent(logEntry.description));
             (node.data as ActionNodeData).log.push(logEntry);
         }
     }
@@ -716,6 +717,7 @@ function registerResourceDepletedAction(resource: string, nodeID: string, action
         }),
         ({ amount, forcedAction }) => {
             if (Decimal.eq(amount, 0) && forcedAction == null) {
+                toast.error(coerceComponent(`${camelToTitle(resources[resource].name)} depleted!`));
                 player.layers.main.forcedAction = {
                     resource,
                     node: layers.main.boards!.data.main.nodes.find(

From 856809cade0c3f535b77c6d636295ab444b6b91d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 28 Aug 2021 07:42:38 -0500
Subject: [PATCH 45/49] Implemented hunger mechanic

---
 src/data/layers/main.ts | 207 +++++++++++++++++++++++++++++++++++-----
 src/data/mod.ts         |   1 +
 2 files changed, 183 insertions(+), 25 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index efd70a1..2a8fa3b 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -49,7 +49,7 @@ enum LinkType {
 // Links cause gain/loss of one resource to also affect other resources
 type ResourceLink = {
     resource: string;
-    amount: DecimalSource;
+    amount: DecimalSource | (() => DecimalSource);
     linkType: LinkType;
 };
 
@@ -64,11 +64,28 @@ type Resource = {
 
 const resources = {
     time: createResource("time", "#3EB489", 24 * 60 * 60, 24 * 60 * 60, [
-        { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly }
+        { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly },
+        { resource: "hunger", amount: -1 / (15 * 60), linkType: LinkType.LossOnly }
     ]),
     energy: createResource("energy", "#FFA500", 100, 100),
     mental: createResource("mental", "#32CD32", 100, 100),
-    focus: createResource("focus", "#0000FF", 100, 0)
+    focus: createResource("focus", "#0000FF", 100, 0),
+    hunger: createResource("hunger", "#FFFF00", 100, 0, [
+        {
+            resource: "focus",
+            amount() {
+                // the higher hunger goes, the more focus it consumes
+                // the idea being the amount lost is the area under the y=x line
+                // that's right, using calculus in a video game :sunglasses:
+                return new Decimal(resources.hunger.amount)
+                    .div(10)
+                    .pow(1.5)
+                    .div(1.5)
+                    .neg();
+            },
+            linkType: LinkType.GainOnly
+        }
+    ])
 } as Record<string, Resource>;
 
 function createResource(
@@ -279,7 +296,8 @@ const actions = {
         ],
         baseChanges: [
             { resource: "time", amount: 16 * 60 * 60, assign: true },
-            { resource: "energy", amount: 100, assign: true }
+            { resource: "energy", amount: 100, assign: true },
+            { resource: "hunger", amount: 20 }
         ]
     },
     forcedSleep: {
@@ -298,7 +316,10 @@ const actions = {
                 weight: 1
             }
         ],
-        baseChanges: [{ resource: "mental", amount: -10 }]
+        baseChanges: [
+            { resource: "mental", amount: -10 },
+            { resource: "hunger", amount: 20 }
+        ]
     },
     rest: {
         icon: "chair",
@@ -306,10 +327,7 @@ const actions = {
         events: [
             {
                 event: () => {
-                    resources.energy.amount = Decimal.sub(
-                        resources.energy.amount || 100,
-                        Decimal.times(10, focusMult.value)
-                    );
+                    resources.energy.amount = Decimal.add(resources.energy.amount, 10);
                     return { description: "You rest your eyes for a bit and wake up rejuvenated" };
                 },
                 weight: 90
@@ -324,10 +342,7 @@ const actions = {
             },
             {
                 event: () => {
-                    resources.energy.amount = Decimal.add(
-                        resources.energy.amount,
-                        Decimal.times(20, focusMult.value)
-                    );
+                    resources.energy.amount = Decimal.add(resources.energy.amount, 20);
                     return {
                         description:
                             "You take an incredible power nap and wake up significantly more refreshed",
@@ -380,6 +395,118 @@ const actions = {
             { resource: "energy", amount: -5 },
             { resource: "mental", amount: 5 }
         ]
+    },
+    eat: {
+        icon: "restaurant",
+        tooltip: "Eat meal",
+        events: [
+            {
+                event: () => ({ description: `You eat a delicious meal. Nice!` }),
+                weight: 8
+            },
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.add(resources.mental.amount, 10);
+                    return {
+                        description: `This is your favorite meal! Oh, what a treat!`,
+                        effectDescription: `+10% <span style="color: ${resources.mental.color}">Mental</span> `
+                    };
+                },
+                weight: 1
+            },
+            {
+                event: () => {
+                    resources.energy.amount = Decimal.sub(resources.energy.amount, 10);
+                    resources.hunger.amount = Decimal.add(resources.hunger.amount, 25);
+                    return {
+                        description: `Oh no, I'm not sure that food was still good`,
+                        effectDescription: `-10% <span style="color: ${resources.energy.color}">Energy</span>, -25% <span style="color: ${resources.hunger.color}">Hunger</span> depletion `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -30 * 60 },
+            { resource: "energy", amount: 10 },
+            { resource: "hunger", amount: -70 }
+        ]
+    },
+    snack: {
+        icon: "icecream",
+        tooltip: "Eat snack",
+        events: [
+            {
+                event: () => ({ description: `You have a nice, small snack. Nice!` }),
+                weight: 8
+            },
+            {
+                event: () => {
+                    resources.energy.amount = Decimal.add(resources.energy.amount, 10);
+                    return {
+                        description: `You chose a healthy, delicious snack. You feel really good!`,
+                        effectDescription: `+10% <span style="color: ${resources.energy.color}">Energy</span> `
+                    };
+                },
+                weight: 1
+            },
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.sub(resources.mental.amount, 5);
+                    resources.hunger.amount = Decimal.add(resources.hunger.amount, 10);
+                    return {
+                        description: `You gorge yourself on unhealthy foods, and don't feel so good`,
+                        effectDescription: `-5% <span style="color: ${resources.mental.color}">Mental</span>, -10% <span style="color: ${resources.hunger.color}">Hunger</span> depletion `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -20 * 60 },
+            { resource: "mental", amount: 2 },
+            { resource: "hunger", amount: -25 }
+        ]
+    },
+    forcedSnack: {
+        events: [
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.sub(resources.mental.amount, 15);
+                    return {
+                        description: `You scarf down anything and everything around you. That can't be good for you.`,
+                        effectDescription: `-15% <span style="color: ${resources.mental.color}">Mental</span> `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -20 * 60 },
+            { resource: "hunger", amount: -25 }
+        ]
+    },
+    brush: {
+        icon: "mood",
+        tooltip: "Brush Teeth",
+        enabled: () =>
+            Decimal.lt(player.lastDayBrushed as DecimalSource, player.day as DecimalSource),
+        events: [
+            {
+                event: () => {
+                    player.lastDayBrushed = player.day;
+                    return {
+                        description: `Brushing once a day is 33% of the way towards keeping the dentist at bay`
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -5 * 60 },
+            { resource: "energy", amount: -2 },
+            { resource: "mental", amount: 2 }
+        ]
     }
 } as Record<string, Action>;
 
@@ -425,6 +552,10 @@ const actionNodes = {
     bed: {
         actions: ["sleep", "rest", "makeBed"],
         display: "Bed"
+    },
+    food: {
+        actions: ["eat", "snack", "brush"],
+        display: "Food"
     }
 } as Record<string, ActionNode>;
 
@@ -474,9 +605,10 @@ for (const id in resources) {
                 }
                 const resource = resources[link.resource];
                 if (resource.amount != null) {
+                    const amount = typeof link.amount === "function" ? link.amount() : link.amount;
                     resource.amount = Decimal.add(
                         resource.amount,
-                        Decimal.times(link.amount, resourceGain)
+                        Decimal.times(amount, resourceGain)
                     );
                 }
             });
@@ -532,14 +664,17 @@ const resourceNodeType = {
                 const link = selectedResource.links.find(link => link.resource === resource.name);
                 if (link) {
                     let text;
+                    const amount = new Decimal(
+                        typeof link.amount === "function" ? link.amount() : link.amount
+                    ).neg();
                     if (resource.name === "time") {
-                        text = formatTime(link.amount);
+                        text = formatTime(amount);
                     } else if (Decimal.eq(resource.maxAmount, 100)) {
-                        text = formatWhole(link.amount) + "%";
+                        text = formatWhole(amount) + "%";
                     } else {
-                        text = format(link.amount);
+                        text = format(amount);
                     }
-                    let negativeLink = Decimal.lt(link.amount, 0);
+                    let negativeLink = Decimal.gt(amount, 0);
                     if (link.linkType === LinkType.LossOnly) {
                         negativeLink = !negativeLink;
                     }
@@ -709,14 +844,19 @@ const actionNodeType = {
     }
 } as RawFeature<NodeType>;
 
-function registerResourceDepletedAction(resource: string, nodeID: string, action: string) {
+function registerResourceDepletedAction(
+    resource: string,
+    nodeID: string,
+    action: string,
+    threshold: 0 | 100 = 0
+) {
     watch(
         () => ({
             amount: resources[resource].amount,
             forcedAction: player.layers.main?.forcedAction
         }),
         ({ amount, forcedAction }) => {
-            if (Decimal.eq(amount, 0) && forcedAction == null) {
+            if (Decimal.eq(amount, threshold) && forcedAction == null) {
                 toast.error(coerceComponent(`${camelToTitle(resources[resource].name)} depleted!`));
                 player.layers.main.forcedAction = {
                     resource,
@@ -735,6 +875,7 @@ function registerResourceDepletedAction(resource: string, nodeID: string, action
 
 registerResourceDepletedAction("time", "bed", "forcedSleep");
 registerResourceDepletedAction("energy", "bed", "forcedRest");
+registerResourceDepletedAction("hunger", "food", "forcedSnack", 100);
 
 export default {
     id: "main",
@@ -822,6 +963,14 @@ export default {
                                 amount: new Decimal(100)
                             } as ResourceNodeData
                         },
+                        {
+                            position: { x: 150, y: 0 },
+                            type: "resource",
+                            data: {
+                                resourceType: "hunger",
+                                amount: new Decimal(0)
+                            } as ResourceNodeData
+                        },
                         {
                             position: { x: -150, y: 150 },
                             type: "action",
@@ -837,6 +986,14 @@ export default {
                                 actionType: "bed",
                                 log: []
                             } as ActionNodeData
+                        },
+                        {
+                            position: { x: -300, y: 150 },
+                            type: "action",
+                            data: {
+                                actionType: "food",
+                                log: []
+                            } as ActionNodeData
                         }
                     ];
                 },
@@ -892,14 +1049,14 @@ export default {
                             links.push(
                                 ...resource.links.map(link => {
                                     const linkResource = resources[link.resource];
-                                    let negativeLink = Decimal.lt(link.amount, 0);
-                                    if (link.linkType === LinkType.LossOnly) {
-                                        negativeLink = !negativeLink;
-                                    }
+                                    const amount =
+                                        typeof link.amount === "function"
+                                            ? link.amount()
+                                            : link.amount;
                                     return {
                                         from: selectedNode.value,
                                         to: linkResource.node,
-                                        stroke: negativeLink ? "red" : "green",
+                                        stroke: Decimal.gt(amount, 0) ? "red" : "green",
                                         "stroke-width": 4,
                                         pulsing: true
                                     } as BoardNodeLink;
diff --git a/src/data/mod.ts b/src/data/mod.ts
index 325fd57..f8ad848 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -14,6 +14,7 @@ export function getStartingData(): Record<string, unknown> {
         points: new Decimal(10),
         day: new Decimal(1),
         lastDayBedMade: new Decimal(0),
+        lastDayBrushed: new Decimal(0),
         devStep: 0
     };
 }

From 2e06e8e4eaa842ba7603465ecf77c40af670759e Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 28 Aug 2021 11:20:12 -0500
Subject: [PATCH 46/49] Polish

---
 public/android-chrome-192x192.png | Bin 0 -> 8746 bytes
 public/android-chrome-512x512.png | Bin 0 -> 26679 bytes
 public/apple-touch-icon.png       | Bin 0 -> 8077 bytes
 public/favicon-16x16.png          | Bin 0 -> 711 bytes
 public/favicon-32x32.png          | Bin 0 -> 1293 bytes
 public/favicon.ico                | Bin 0 -> 15086 bytes
 public/index.html                 |   7 ++
 public/mstile-150x150.png         | Bin 0 -> 5716 bytes
 public/safari-pinned-tab.svg      | 158 ++++++++++++++++++++++++++++++
 public/site.webmanifest           |  19 ++++
 src/components/system/Info.vue    |   4 +-
 src/components/system/Nav.vue     |   8 +-
 src/data/modInfo.json             |   2 +-
 13 files changed, 193 insertions(+), 5 deletions(-)
 create mode 100644 public/android-chrome-192x192.png
 create mode 100644 public/android-chrome-512x512.png
 create mode 100644 public/apple-touch-icon.png
 create mode 100644 public/favicon-16x16.png
 create mode 100644 public/favicon-32x32.png
 create mode 100644 public/favicon.ico
 create mode 100644 public/mstile-150x150.png
 create mode 100644 public/safari-pinned-tab.svg
 create mode 100644 public/site.webmanifest

diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd0de301c24d6a0ae942c0e8302758e67efd6292
GIT binary patch
literal 8746
zcmV+_BGuiAP)<h;3K|Lk000e1NJLTq006)M006)U1ONa4_|>G000004XF*Lt006O%
z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002
zVoOIv0RM-N%)bBt00(qQO+^Rg0UQG!9T@WrSpWba&q+i<RCwC$op*p-RoRE1+1+e5
zy(1|Ql912?q4z{7f{OHBL=X@V5XJKOfeI*yfQ1jFqZ9=~Qz-^SAxH>;5HOemBnAki
z_iV~$cjx*3nAypcJF`>n-6WbHf3Uf8?>Xnb@44r^=dBPqMBK{jo#`4^IMMIAr5Jt3
zn*mAk@8MXRZ9$_iIwDLKh%tLO$R3*+n*|YfaFW^hm-~c6ED^hTiEldF@qXFZW)`GD
ztn>E<VZ7T~XRS5H%4da$+xvu#$yVokhE<yq3u4hWFC4_vbPM<?&UEI}z#3MuoW;yz
zHnW+_5}Jw~mQcl^+{o~>i;MXs&$65*MAor_hR9#D0z^780e;BUMc&@X3g$AMHyOvj
zSe|ci0(7JwwRGZ(T*ww#H^f=Ssw4v%n9dEnu`#nCLWtEqX6ab&t6t%d;PqVW;eF0=
zzRS|K_WYRScK@ZDv)IwGWiUWB?o5AZ|6J0E$GfhiwjQ#>#>#-I$lGsM=94i7^JD(V
zm#9uBo&ZBjZ1oTZM*N;-BWeaTv8sZnW9-QTe2eV)+p~ygn_0jz1R8jOIT62S*{F%Q
z{f3HPy$9FQi)&n+wvBda^{KbHi+|9W-RQ`ByvmA*-*e-Hh*y-k2c%=2*IH{!o3T76
zi}m=Mga1pn-E4@TbyWx~Smk*Ml8WseQ}Es;9=z$0Kk7XIK4ftSt#ssn**&=h_hs9H
z_fF=mO-JsJo&jrE6G}S+xq><Xj%0AbJ3nQ0<Zl%kqTYm|dl6e^aX5;@`7^L>*7RP^
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
Ubn-$ql>h($07*qoM6N<$g4*2$`2YX_

literal 0
HcmV?d00001

diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e22a44cd0248bbb39ebed4bce4450aec49fab9c
GIT binary patch
literal 26679
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
ARsaA1

literal 0
HcmV?d00001

diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..e16532eddcadabce5bc5a004ddb67e3b05947f29
GIT binary patch
literal 8077
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
bph_+P>K74o@<KJ000000NkvXXu0mjf=Sls(

literal 0
HcmV?d00001

diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..562afdb69f2469bd7b18ecd5a19986dab1fcb6a7
GIT binary patch
literal 711
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa
z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{Xiaj
ziKnkC`%^|4W;vPsFE-~H7#MRrT^vI+&ih{U&ERKXX#05o|KS@_8(U{)cpb_SH(fo|
za>dy#lb$F%Jr-mU#Ko>RXU>+Qg2Do!ys{bFGA2*nz^t~dSy$dzrT$In%}2KTZQGwS
zP7IYi>F}#a;rk`QjT?*>rcCdBwRLyi4WZPnHy`bc>{@V*^Xs!ICX&||h3@~IXSsTv
z$cnJk=B~+R)d|gd?Yi3nXS=6{>|AtwL*aXyz{Q^3b@p?O9;OOrZ^_<rz347mX-3iE
z)YBjDKHn4;o_o~z`kJrq{h!lhuRmC5y>IH<ZNV4w&G(vSwsyOHjY>PT;I`IMZsS{P
zMB`0wyxS?bdapqGo6jd7FEU)d*X-F^+gX1vtnu$RPrGtsVaTxwda2V(tR5|PmCD_G
zZpv4#X|uxb&eQiic6r|7ndbtQ8Bbp_DfwvS)6HoYR$Nxp*8LoEQ2u>wgo4PWxw}_B
z34b=JXPG&(<#b=o^UO*?(;upwT>Zvh@v7$E>|F(xGgkU6`~Ck;J!4e*!q5PpHF?0O
zP%UwdC`m~yNwrEYN(E93Mg~TPx(0^2MiwCk##Y7#Rz?=u28LD!25ssqLCFH4AvZrI
zGp!Q0hVNB^?|>S@t3o15f)dLW3X1a6GILTDN-7Id6*3D-k{K8(<~;ty!%-Nfp>fLp
z^cl~mK@7~w+<M8}!pg$llSP<?6<iuj4yQ0HZw^s7edEfBBWI4t9AQ7*;IY6<kKvWL
WV96&Z)2To!7(8A5T-G@yGywp110f0k

literal 0
HcmV?d00001

diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000000000000000000000000000000000000..039a8eb6810dd58064ad2b4371d7e97263a41b44
GIT binary patch
literal 1293
zcmV+o1@iidP)<h;3K|Lk000e1NJLTq001BW001Be1ONa4*>kdg00004XF*Lt006O%
z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002
zVoOIv0RM-N%)bBt00(qQO+^Rg0UQG!9B=eI&j0`evPnciR7l5_m04`nRS<{2yOg#p
z*2>nFf}sRcVzjtWLP87ylz>1~d~vIy5`{#As6-PJB$Ozk4;YHz22x@*^}z;s0K*al
zQgEv%B3neDQkE7FN}>0*pAY<d`)>;&&eJ)`{JxnpbIzOyArmqwr<e~o$bSAp1~Y>O
zw(~PrBHYl#e*U2*Z1jMynClj`)-nfp9ZkGLeCS3i5GT%17SN3*syIw3=^)q2Arg{D
zn854Y7lQ8MN#YEpkQ1x{AeX7c_?r4QX&8{rcihtf-^^27;5NGRJ4gAI(Y((Hpo-<h
zso@`phTJc*H?+G%6Wtg`Ev*b;5DD()6Gjpx%0pC9#cBq01SD9<j$r*Svv{5cifE*U
zp7de@3n@$;JcBr6c`yYKAdhV3^DzxT9UG~jivBdQlNf(f%a|_p>l*+B7|nV%v4T@P
zM+I4oKz0F9vMC_RR5EC#hE*I30H@j3-v6jyc-4}A>0qb%p_z{NCwn`=bH3&w&-spd
z;*!9#+?c7BwKqA&W90HOg-oHE9VB^zIDP2JY*y1s4#NY_wD$OThrifnP5$Qh?s1(x
z%yX>gJm7p6nP${t*Vz*IReO^lW;)A5w%c0!SmX*<nPG1?s4LAe%K}S1neu9OVcQX2
z;9b%~)jh%)&hQfHTxJFjaEi~UV=~Whk)>QFmwb|Jq>`2haeDD^2YD@P0V2&d15NB_
z2|I{ziseM<PBR-h!uixCw!0fb=_h<v#NLjwkC<jZ<Hj8CTdw!GNfXw)$!VsUD&cf5
zqyQ~0bG&n`aIpnOyjtR@{k_?~4);ATsd~XT0)!ZMu2(`?G`Q4pzUVi;8X$@+2vB!<
zH1Im_L}^3xa7{{n*b*nX-K2jyQH(jp>Hgr;#*Fw;C{SrT0Ef8Egr_a{P9L{U-R_Ve
z>H^E0XPq;Rdb^tg`9&WM00Cm4lPogLah_0hb|A-L?(=PzdDar~%>c01{1hNG6SGqh
z=GoVr08!>yXM50-&a*o7X7aiMCI(g32KPHx%(KL;>K4oFY>N#KRR9Xf3|84>aU8gp
zPe}ttQOz+1cJMBySF{tLp4LtyH!~PI>EyGulXnv>osW?3?of(5o#T(G-DV#NUAct&
zoZPhrIMOX%_FA^v9Ls|iE^>;4jCA>KM7+iOU1hQhpwhT`minzT&A8rA)rQFlrGC}+
zSS(`1SeO1^Gmp6)-e{%6ZYb-7xHWZU^mDWRmpsU!$SNDX<W~;7zWhJgMNi0o2O#eN
z001R)MObuXVRU6WV{&C-bY%cCFfuVMFflDMI8-n*Ix{djGB_(RF*-0Xh9{{~0000b
zbVXQnWMOn=I&E)cX=Zr<GB7eREif@HGB{K)GdeRcIx;vbFflqXF!y>9?f?J)S9(-f
zbW&k=AaHVTW@&6?Aar?fWguyAbYlPjc%0+%3<z;nC@9KL%gjmTQUC)Z3oc^|6C)E#
zlN19JAVh#P!(;;^vs56<*wo0x%)}hT2g`s+ph_+P>K74o@<KJ000000NkvXXu0mjf
DD}XUu

literal 0
HcmV?d00001

diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..851f35719e3d15037d77afc44f48db9c4a2e07ab
GIT binary patch
literal 15086
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
Vts(!%)qJD<mhZEC3oW4Se*uQ!=}iCt

literal 0
HcmV?d00001

diff --git a/public/index.html b/public/index.html
index 5a9799b..febe2c5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,6 +3,13 @@
 	<head>
 		<meta charset="utf-8">
 		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
+		<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
+		<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
+		<link rel="manifest" href="site.webmanifest">
+		<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
+		<meta name="msapplication-TileColor" content="#da532c">
+		<meta name="theme-color" content="#000000">
 		<meta name="viewport" content="width=device-width,initial-scale=1.0">
 
 		<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png
new file mode 100644
index 0000000000000000000000000000000000000000..e8525a7ba057c46f450e79849efddd839689b0ab
GIT binary patch
literal 5716
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

literal 0
HcmV?d00001

diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg
new file mode 100644
index 0000000..0466e63
--- /dev/null
+++ b/public/safari-pinned-tab.svg
@@ -0,0 +1,158 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="1707.000000pt" height="1707.000000pt" viewBox="0 0 1707.000000 1707.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+</metadata>
+<g transform="translate(0.000000,1707.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M4772 17059 c-158 -29 -342 -189 -424 -369 -15 -33 -40 -118 -53
+-185 -13 -66 -13 -254 1 -340 15 -99 14 -93 44 -220 17 -69 21 -111 18 -180
+-6 -112 -30 -168 -108 -245 -69 -69 -161 -123 -405 -240 -319 -153 -374 -186
+-459 -274 -118 -125 -162 -244 -153 -421 7 -142 38 -250 121 -420 146 -298
+351 -549 654 -802 288 -240 688 -474 1547 -907 556 -280 679 -358 880 -560
+142 -142 214 -256 249 -390 18 -67 18 -281 0 -356 -20 -86 -64 -190 -80 -190
+-37 0 -288 139 -449 249 -76 52 -234 169 -245 181 -3 3 -30 25 -60 50 -83 68
+-164 138 -250 216 -14 13 -34 31 -46 41 -12 10 -61 54 -109 98 -48 44 -93 85
+-99 90 -6 6 -52 46 -101 90 -50 44 -121 107 -160 140 -38 33 -76 66 -84 73
+-39 37 -267 216 -346 272 -151 106 -333 216 -480 290 -222 111 -400 152 -650
+153 -379 0 -717 -135 -948 -377 -218 -229 -362 -625 -295 -813 11 -32 22 -65
+23 -73 57 -246 53 -396 -15 -547 -50 -109 -141 -220 -245 -298 -22 -17 -42
+-32 -45 -35 -20 -20 -292 -190 -304 -190 -4 0 -135 -75 -176 -100 -8 -5 -33
+-19 -55 -31 -22 -11 -59 -34 -83 -50 -24 -16 -45 -29 -48 -29 -18 0 -204 -153
+-284 -232 -230 -231 -308 -339 -374 -523 -70 -194 -87 -442 -51 -725 19 -152
+75 -324 160 -496 66 -133 134 -220 224 -285 185 -134 386 -371 746 -882 351
+-499 489 -658 721 -834 152 -115 176 -124 354 -127 204 -4 449 26 595 71 204
+64 273 91 465 188 253 126 425 233 718 444 74 53 143 103 155 111 12 8 78 57
+147 108 69 51 136 101 149 110 13 9 51 38 85 63 33 25 86 63 116 86 55 40 271
+194 300 213 8 5 47 31 85 58 121 82 306 189 425 245 163 76 167 78 355 147
+400 147 588 232 666 300 61 53 69 83 32 122 -61 65 -222 129 -324 129 -92 1
+-349 -61 -464 -111 -11 -5 -84 -36 -163 -70 -151 -64 -239 -103 -372 -163
+-154 -70 -347 -139 -464 -167 -338 -79 -640 -18 -821 164 -132 133 -184 277
+-150 410 24 92 68 149 252 326 382 370 434 432 487 580 25 69 26 78 21 224
+-14 354 -5 444 62 606 67 163 189 279 312 298 50 8 234 7 305 -1 77 -9 213
+-56 361 -126 111 -52 177 -86 360 -186 77 -42 361 -213 467 -281 l67 -44 62
+33 c92 48 189 146 240 242 40 77 42 83 44 180 2 109 -13 219 -44 318 -19 62
+-73 202 -90 234 -5 10 -37 74 -72 143 -34 69 -85 166 -112 215 -28 50 -64 115
+-80 145 -17 30 -41 72 -54 92 -13 21 -23 40 -23 42 0 3 -15 31 -34 63 -110
+189 -216 402 -279 561 -25 62 -68 145 -95 185 -129 190 -163 302 -165 539 -1
+122 10 193 43 287 114 323 382 503 639 430 351 -101 631 -537 742 -1159 10
+-58 21 -116 23 -130 3 -14 12 -74 21 -135 9 -60 25 -166 36 -235 29 -183 26
+-163 55 -330 52 -301 158 -579 297 -778 l65 -93 27 87 c23 76 78 280 91 338 3
+12 6 26 8 31 2 6 6 28 11 50 4 22 10 49 12 60 4 19 28 144 37 195 6 32 26 175
+40 280 6 47 16 181 21 290 5 102 5 688 1 758 -8 120 -42 405 -62 512 -2 14 -7
+41 -10 60 -13 84 -65 324 -108 495 -25 102 -104 356 -126 410 -5 11 -20 53
+-34 93 -62 177 -185 445 -326 712 -34 63 -93 176 -132 250 -91 172 -166 297
+-229 380 -78 102 -270 285 -365 347 -11 7 -36 24 -57 38 -145 96 -417 208
+-611 250 -87 19 -219 45 -256 50 -25 3 -61 7 -80 10 -141 19 -190 23 -330 31
+-177 9 -228 12 -290 13 -52 1 -56 3 -80 39 -67 101 -226 239 -321 280 -96 41
+-240 58 -337 41z"/>
+<path d="M8963 16889 c-48 -9 -87 -28 -163 -79 -47 -32 -185 -137 -242 -185
+-31 -26 -92 -61 -118 -69 -14 -4 -70 -9 -125 -12 -142 -6 -158 -10 -198 -49
+-56 -54 -67 -107 -82 -380 -10 -188 -19 -233 -58 -309 -45 -87 -30 -190 40
+-260 44 -43 92 -58 181 -55 82 3 160 23 317 84 152 59 186 71 262 88 70 16
+168 12 216 -8 126 -53 431 -368 518 -535 19 -37 24 -62 26 -145 3 -152 -24
+-194 -131 -200 -67 -4 -108 14 -196 90 -147 126 -199 165 -231 178 -131 53
+-308 -81 -309 -232 0 -39 5 -49 43 -86 45 -43 113 -71 196 -81 24 -3 139 -7
+255 -8 116 -2 229 -7 251 -11 113 -21 252 -143 320 -282 33 -68 37 -85 37
+-150 -2 -118 -27 -175 -102 -226 -36 -25 -50 -22 -105 28 -28 25 -63 52 -80
+60 -16 9 -37 20 -45 25 -51 31 -188 78 -255 87 -129 17 -212 -3 -276 -67 -108
+-108 -82 -229 61 -289 58 -25 88 -27 388 -32 162 -3 271 -17 320 -42 36 -20
+89 -87 99 -126 12 -48 4 -344 -12 -442 -2 -13 -6 -45 -9 -71 -3 -27 -9 -69
+-12 -95 -36 -290 -168 -779 -285 -1058 -7 -16 -13 -32 -14 -35 -2 -6 -9 -23
+-43 -105 -117 -286 -425 -893 -568 -1118 -27 -42 -117 -173 -200 -290 -259
+-365 -318 -468 -386 -671 -47 -140 -51 -201 -19 -287 62 -165 207 -289 406
+-346 64 -18 245 -25 310 -11 102 22 256 151 455 384 179 207 397 562 507 824
+8 19 23 53 33 75 155 349 287 878 335 1340 31 303 35 640 9 865 -12 106 -14
+126 -29 197 -35 170 -23 300 40 428 51 106 124 173 240 222 60 25 106 32 285
+39 74 2 146 7 158 9 88 15 144 89 138 179 -9 125 -112 219 -241 219 -106 1
+-127 -10 -230 -121 -128 -137 -232 -202 -324 -202 -130 0 -233 124 -233 281
+-1 172 145 293 547 454 230 93 271 123 283 207 10 76 -43 135 -157 173 -89 29
+-187 24 -278 -14 -69 -30 -106 -73 -133 -154 -42 -128 -124 -212 -217 -221
+-140 -14 -314 201 -301 373 12 175 192 352 446 441 194 68 275 177 216 291
+-17 33 -84 92 -133 115 -49 25 -193 26 -236 2 -66 -36 -111 -98 -120 -165 -16
+-112 -64 -191 -140 -227 -45 -22 -134 -22 -173 -2 -84 43 -232 304 -277 486
+-26 103 -26 257 0 325 50 133 128 206 274 256 67 23 91 23 161 0 54 -18 174
+-22 227 -8 45 12 91 39 123 72 28 29 32 40 32 81 -1 40 -9 60 -48 117 -26 38
+-68 91 -93 117 -67 70 -105 79 -381 84 -124 3 -238 8 -254 11 -50 10 -99 42
+-167 107 -94 90 -123 115 -156 132 -36 18 -114 25 -170 15z"/>
+<path d="M13290 15222 c-77 -10 -338 -111 -504 -194 -183 -91 -431 -255 -580
+-383 -245 -211 -484 -467 -649 -695 -69 -96 -131 -188 -182 -270 -32 -52 -65
+-102 -71 -110 -16 -19 -236 -453 -291 -575 -110 -240 -237 -591 -323 -895 -23
+-80 -43 -153 -45 -163 -2 -11 -20 -83 -39 -160 -19 -78 -37 -154 -40 -169 -28
+-138 -66 -334 -72 -373 -3 -22 -7 -42 -9 -45 -2 -3 -7 -33 -10 -65 -4 -33 -9
+-62 -10 -65 -9 -14 -19 -145 -20 -235 0 -115 22 -199 63 -240 37 -37 65 -40
+117 -15 49 24 175 142 241 225 21 27 44 55 49 61 6 6 60 79 120 164 287 398
+400 548 481 636 27 29 72 59 361 245 65 42 208 136 318 209 178 117 243 157
+385 236 71 40 224 101 300 120 103 26 283 26 360 -1 187 -63 297 -154 324
+-268 20 -83 26 -313 11 -407 -11 -70 -53 -189 -91 -257 -74 -133 -229 -310
+-379 -433 -65 -53 -320 -230 -332 -230 -3 0 -17 -8 -31 -19 -15 -10 -47 -29
+-72 -43 -261 -146 -362 -209 -475 -294 -92 -69 -203 -132 -410 -233 -653 -319
+-678 -334 -828 -484 -84 -83 -116 -149 -121 -245 -2 -37 1 -81 6 -99 l9 -33
+67 26 c333 129 361 139 657 229 176 54 458 122 620 150 17 2 39 7 50 9 11 2
+45 7 75 11 30 4 62 8 70 10 158 27 630 46 765 31 128 -14 154 -18 205 -26 30
+-5 78 -12 105 -15 28 -3 61 -8 75 -10 14 -3 45 -8 70 -11 25 -3 56 -7 70 -9
+25 -4 238 -31 318 -40 23 -3 61 -7 85 -10 248 -31 575 -14 788 40 306 79 507
+203 826 509 125 120 158 164 202 269 58 142 61 258 12 540 -6 33 -13 67 -16
+77 -3 10 -8 28 -10 39 -3 12 -7 29 -9 39 -81 343 -96 436 -97 572 0 206 42
+309 227 547 201 258 277 491 269 828 -7 344 -110 638 -321 910 -201 261 -495
+426 -779 439 -78 3 -132 -8 -238 -53 -225 -93 -417 -40 -697 193 -47 39 -90
+75 -96 81 -6 6 -35 31 -64 56 -29 24 -58 49 -64 54 -37 35 -162 130 -229 175
+-150 100 -251 135 -412 143 -33 1 -71 1 -85 -1z"/>
+<path d="M12517 9234 c-1 -1 -54 -5 -117 -8 -174 -9 -277 -20 -463 -46 -94
+-14 -116 -17 -182 -30 -27 -5 -66 -12 -85 -15 -19 -3 -42 -8 -50 -10 -8 -2
+-28 -7 -45 -9 -60 -11 -90 -17 -115 -26 -14 -4 -68 -18 -120 -30 -314 -73
+-921 -278 -1080 -364 -32 -18 -90 -112 -90 -148 0 -23 5 -28 35 -34 36 -6 107
+0 178 16 23 5 53 11 67 14 14 2 93 21 175 41 217 52 287 63 405 59 127 -4 237
+-18 390 -49 14 -3 41 -7 60 -10 19 -3 46 -7 60 -10 14 -3 43 -7 65 -10 22 -3
+47 -7 55 -10 8 -2 33 -6 55 -10 211 -30 501 -116 680 -201 290 -136 471 -355
+498 -601 l5 -42 -61 -29 c-142 -67 -345 -250 -402 -361 -45 -89 -56 -224 -23
+-286 41 -77 131 -142 298 -215 114 -50 155 -80 225 -162 22 -26 45 -54 52 -61
+6 -8 27 -46 47 -85 128 -253 100 -572 -70 -798 -95 -127 -195 -179 -344 -178
+-143 2 -241 70 -455 319 -38 44 -74 85 -80 91 -16 17 -199 216 -210 229 -162
+184 -684 718 -840 860 -150 137 -413 335 -443 335 -6 0 -12 4 -14 9 -9 24
+-458 235 -458 215 0 -3 -8 0 -18 5 -22 12 -151 39 -223 46 -30 4 -66 8 -81 10
+-35 4 -36 -8 -7 -101 53 -173 165 -345 379 -584 60 -66 188 -205 231 -251 381
+-398 523 -584 660 -864 43 -88 111 -216 150 -285 94 -163 216 -406 308 -615
+12 -27 29 -63 36 -80 16 -33 57 -127 60 -135 1 -3 6 -16 12 -30 67 -158 153
+-366 208 -500 350 -855 534 -1230 705 -1435 59 -72 192 -194 260 -241 150
+-104 330 -187 491 -229 72 -19 359 -27 467 -12 115 15 311 99 427 182 138 99
+296 284 376 439 40 79 43 82 78 82 47 1 237 14 276 19 17 2 53 7 80 10 94 10
+144 18 210 30 521 102 895 308 1082 598 80 125 146 295 167 427 7 42 8 350 2
+400 -3 19 -7 62 -11 95 -8 73 -23 145 -68 330 -44 175 -87 281 -149 365 -66
+88 -324 326 -497 458 -131 99 -259 221 -308 293 -45 65 -63 127 -59 201 5 101
+50 183 166 305 92 97 116 129 151 199 81 166 109 428 68 629 -45 219 -147 431
+-310 647 -138 182 -351 392 -549 543 -302 229 -590 382 -943 501 -124 42 -157
+52 -304 88 -158 39 -335 69 -473 82 -41 3 -78 8 -81 10 -16 9 -563 26 -572 18z"/>
+<path d="M7388 7875 c-1 -2 -27 -6 -56 -9 -43 -5 -128 -21 -212 -41 -377 -87
+-791 -280 -1210 -563 -470 -318 -783 -601 -1111 -1003 -243 -298 -439 -617
+-582 -949 -111 -258 -198 -653 -199 -908 0 -79 8 -186 18 -237 3 -11 7 -35 10
+-54 10 -57 12 -67 34 -148 28 -100 100 -282 146 -370 44 -83 169 -272 221
+-333 77 -91 199 -212 267 -268 39 -31 77 -63 85 -70 35 -34 248 -166 346 -215
+227 -115 490 -196 715 -222 88 -10 123 -22 189 -67 67 -45 183 -163 234 -237
+17 -25 34 -48 37 -51 11 -8 86 -140 124 -220 35 -71 69 -151 268 -630 45 -107
+104 -238 131 -290 27 -52 54 -105 61 -117 6 -13 14 -23 19 -23 4 0 7 -5 7 -12
+0 -21 113 -162 191 -238 65 -64 343 -268 459 -337 285 -171 648 -264 1000
+-257 80 2 161 5 180 8 82 12 98 15 165 30 98 23 93 21 181 51 134 46 340 156
+438 235 166 132 259 240 382 440 97 159 198 246 649 567 378 269 473 343 611
+474 284 271 426 516 471 814 9 56 8 387 -1 595 -7 178 -10 213 -21 310 -4 30
+-9 71 -11 90 -2 19 -6 46 -9 60 -3 14 -8 42 -11 64 -3 21 -8 48 -10 60 -3 11
+-7 32 -10 46 -97 475 -223 799 -500 1282 -71 124 -280 457 -294 468 -4 3 -21
+28 -40 55 -34 51 -250 343 -281 380 -9 11 -63 76 -119 145 -124 152 -192 230
+-360 415 -82 90 -605 609 -661 655 l-48 41 -15 -23 c-22 -34 -34 -116 -35
+-248 -3 -165 20 -250 120 -455 65 -134 85 -169 188 -338 286 -472 350 -653
+317 -902 -11 -84 -67 -224 -107 -266 -49 -51 -97 -59 -385 -64 -131 -2 -256
+-7 -276 -9 -70 -9 -113 -45 -156 -131 -24 -46 -26 -60 -26 -190 0 -147 3 -187
+29 -325 39 -211 1 -419 -108 -595 -51 -82 -182 -205 -271 -254 -122 -67 -219
+-91 -346 -87 -164 6 -264 49 -381 167 -84 83 -159 232 -184 362 -3 10 -6 26
+-9 35 -3 9 -8 37 -11 62 -4 24 -9 48 -11 52 -10 15 -12 234 -3 288 5 30 11 74
+14 97 3 23 19 104 35 180 37 169 37 169 49 218 30 124 61 317 61 383 0 115
+-38 158 -139 156 -49 -1 -196 -42 -226 -64 -19 -13 -130 -50 -199 -65 -38 -9
+-105 -15 -152 -14 -298 6 -462 227 -462 620 1 119 6 162 34 299 50 233 160
+445 313 600 84 85 157 135 264 183 171 75 286 155 440 306 175 170 253 355
+198 464 -46 91 -174 143 -353 145 -53 0 -98 -1 -100 -3z"/>
+</g>
+</svg>
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..b20abb7
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,19 @@
+{
+    "name": "",
+    "short_name": "",
+    "icons": [
+        {
+            "src": "/android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "/android-chrome-512x512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}
diff --git a/src/components/system/Info.vue b/src/components/system/Info.vue
index 455c111..0896597 100644
--- a/src/components/system/Info.vue
+++ b/src/components/system/Info.vue
@@ -17,7 +17,7 @@
                     Aarex
                 </div>
                 <br />
-                <div class="link" @click="$emit('openDialog', 'Changelog')">
+                <div v-if="false" class="link" @click="$emit('openDialog', 'Changelog')">
                     Changelog
                 </div>
                 <br />
@@ -51,7 +51,7 @@
                 </div>
                 <br />
                 <div>Time Played: {{ timePlayed }}</div>
-                <div v-if="hotkeys">
+                <div v-if="hotkeys.length > 0">
                     <br />
                     <h4>Hotkeys</h4>
                     <div v-for="key in hotkeys" :key="key.key">
diff --git a/src/components/system/Nav.vue b/src/components/system/Nav.vue
index 93a61b3..4b4a302 100644
--- a/src/components/system/Nav.vue
+++ b/src/components/system/Nav.vue
@@ -2,14 +2,18 @@
     <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">
+        <div
+            @click="openDialog('Changelog')"
+            class="version-container"
+            style="pointer-events: none"
+        >
             <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')" />
+            <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>
diff --git a/src/data/modInfo.json b/src/data/modInfo.json
index 42c9ed0..b0bda0f 100644
--- a/src/data/modInfo.json
+++ b/src/data/modInfo.json
@@ -13,7 +13,7 @@
 	"defaultDecimalsShown": 2,
 	"useHeader": true,
 	"banner": null,
-	"logo": null,
+	"logo": "android-chrome-512x512.png",
 	"initialTabs": [ "main" ],
 
 	"maxTickLength": 3600,

From 97fe313558ecf11fd7713af513a4d4b47f132032 Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 28 Aug 2021 11:20:17 -0500
Subject: [PATCH 47/49] Implemented chapter 2

---
 src/data/layers/main.ts | 361 ++++++++++++++++++++++++++++++++++------
 src/data/mod.ts         |   6 +-
 src/util/vue.ts         |   9 +-
 3 files changed, 318 insertions(+), 58 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index 2a8fa3b..a19960f 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -67,8 +67,8 @@ const resources = {
         { resource: "mental", amount: 1 / (120 * 60), linkType: LinkType.LossOnly },
         { resource: "hunger", amount: -1 / (15 * 60), linkType: LinkType.LossOnly }
     ]),
-    energy: createResource("energy", "#FFA500", 100, 100),
-    mental: createResource("mental", "#32CD32", 100, 100),
+    energy: createResource("energy", "#FFA500", 100),
+    mental: createResource("mental", "#800080", 100),
     focus: createResource("focus", "#0000FF", 100, 0),
     hunger: createResource("hunger", "#FFFF00", 100, 0, [
         {
@@ -85,14 +85,15 @@ const resources = {
             },
             linkType: LinkType.GainOnly
         }
-    ])
+    ]),
+    money: createResource("money", "#32CD32", 1000, 0)
 } as Record<string, Resource>;
 
 function createResource(
     name: string,
     color: string,
     maxAmount: DecimalSource,
-    defaultAmount: DecimalSource,
+    defaultAmount?: DecimalSource,
     links?: ResourceLink[]
 ): Resource {
     const node = computed(() =>
@@ -121,7 +122,10 @@ function createResource(
             return node.value;
         },
         get amount() {
-            return node.value ? (node.value.data as ResourceNodeData).amount : defaultAmount;
+            if (node.value) {
+                return (node.value.data as ResourceNodeData).amount;
+            }
+            return defaultAmount == null ? maxAmount : defaultAmount;
         },
         set amount(amount: DecimalSource) {
             (this.node.data as ResourceNodeData).amount = Decimal.clamp(amount, 0, maxAmount);
@@ -142,6 +146,7 @@ function getResource(node: BoardNode): Resource {
 const selectedNode = computed(() => layers.main?.boards?.data.main.selectedNode);
 const selectedAction = computed(() => layers.main?.boards?.data.main.selectedAction);
 const focusMult = computed(() => Decimal.div(resources.focus.amount, 100).add(1));
+const chapter = computed(() => ((player.devSteps as number) >= chapterOne.length ? 2 : 1));
 
 export type LogEntry = {
     description: string;
@@ -170,15 +175,21 @@ type Action = {
     fillColor?: string;
     tooltip?: string;
     events?: Array<WeightedEvent>;
-    baseChanges?: Array<{
-        resource: string;
-        amount: DecimalSource;
-        assign?: boolean;
-    }>;
+    baseChanges?:
+        | Array<{
+              resource: string;
+              amount: DecimalSource;
+              assign?: boolean;
+          }>
+        | (() => Array<{
+              resource: string;
+              amount: DecimalSource;
+              assign?: boolean;
+          }>);
     enabled?: boolean | (() => boolean);
 };
 
-const developSteps = [
+const chapterOne = [
     "Spring break just started, and I've got no real obligations! Time to start working on a new project! Just gotta keep it scoped small enough this time so I can actually finish before school starts back up.",
     "Created a new repo! I even added a README and LICENSE!",
     "I created an index.html file, and a main.css and main.js",
@@ -188,7 +199,7 @@ const developSteps = [
     "Hmm, what if it involved starting in a garage then growing to become a AAA studio?",
     "Or, what if it was more abstract. It could use different development-related features in a sort of tree structure, and the numbers get increasingly absurd!",
     `No, that won't work. What if it got <span style="font-style: italic">too</span> ridiculous? Or got really boring towards the end? What would the end game even be? Probably something silly, that's what`,
-    `It could be self-documenting. A game about its own development process? Or maybe its narrated, and following the path of a developer over time. That could be something <a href="https://store.steampowered.com/app/303210/The_Beginners_Guide/">really special</href>.`,
+    `It could be self-documenting. A game about its own development process? Or maybe its narrated, and following the path of a developer over time. That could be something <a href="https://store.steampowered.com/app/303210/The_Beginners_Guide/">really special</a>.`,
     "Maybe meta games are passé these days. How about I start with some first person shooter",
     "You know what? I'll figure it out as I go along. Let's start with stuff any game would need",
     "Made an options screen!",
@@ -209,6 +220,43 @@ const developSteps = [
     `Thought of what to add next. I can't <span style="font-style: italic">just</span> make pong!`
 ];
 
+// Y'all ready for a stream of conciousness?
+const chapterTwo = [
+    "Aw geeze. Break ended... but I don't want to stop developing! I think it'll be okay if I just continue working on it a bit after school... Otherwise, how would I be able to add all the new features I currently and have always intended on adding!",
+    "So I have to level with you. Dev to player. This was intended to be the chapter where you unlock a school node with actions about going to class. It'd take tons of time but give you experience, and not doing it by EOD would cause a fight with your parents, causing a mental health penalty",
+    "Buuuut, I don't think the game is really hard enough right now. The experience was supposed to be a buff and overall you wouldn't get nerfed that hard",
+    "The plan for a chapter 3 was having your parents tell you to start paying for food and rent, so you'd have to start working as well. That was when the difficulty would ramp up: you'd have to take fewer classes per day and sleeping and food would cost money now",
+    "Due to a lack of time, I've cut the school mechanic and adapted the money mechanic so it could be implemented in a reasonable amount of time",
+    "Unfortunately I don't have a good way of making the story work, and I've never been good at writing anyways, so I'm making this in-universe devlog a meta one instead",
+    "This also means I don't have to spend time explaining what got cut due to time separately in the game description or something :sunglasses:",
+    "I know the sunglasses emoji didn't appear in the last message, I just like typing stuff like that. I do it in my markdown notes a lot",
+    "I spent way too long working on the board itself. Which is fine - I really like the board, and will use it in other projects",
+    "Like Kronos. I know its taking a long time to make but it'll be worth it, trust me :)",
+    "Let's just hope my writing skills improve in that game :sweat_smile:",
+    "Does anyone else not like the name of :sweat_smile:? It's a good emoji but man typing it out is kinda gross",
+    "I think it's ironic that I keep wanting to make more narratively driven games, despite how slow I am at writing, and my insecurities about any story I've written. But I love narratively driven games, so its only natural to want to make one myself, right?",
+    "Anyways. I don't actually have many regrets on how I spent my time these last two weeks. I've been incredibly productive, I actually think. The project was a bit ambitious",
+    "And let me tell you: the work does not stop here! After this jam I need to massively refactor this project, or at least how boards work",
+    "Did you know most of the code for this is in a single 1300+ LoC file? It's a mess to find the section of code I need. And all the actions are in one massive object!",
+    'Speaking of, eventually I wanted to have a "refactor" action on the PC node.',
+    `I'm wondering if I'll need some sort of dependency injection to get around cyclical dependencies, which sounds like a whole mess that'll just make it <span style="font-style: italic">more</span> complicated, but I'd love to have each mechanic in its own file`,
+    "e.g. a file could register the energy resource, bed node, and its various actions. That would be really nice, and avoid this massive nest of a file where every mechanic appears in several spread out parts",
+    "kinda like how the new Composition API in Vue 3 is supposed to make code relating to a specific thing able to be kept close to itself, versus the Options API where it all gets spread out",
+    "I actually used the composition API in this game, although TMT-X itself mainly uses the Options API still. That'll probably change at some point, I really like the Composition API",
+    "I think my stream of conciousness is just about running out of things to say, so I guess the game should end soon",
+    "heh, I said the theme of the jam in that last message. If you didn't notice, you can read it by opening the log in the PC action. I maybe should have mentioned that earlier",
+    "Ah well. It's not like I have a tutorial anyways",
+    "Before I go, I have to say one last thing: If I had a bit more time, I would've liked to restyle these notifications",
+    `I used a nice library that was super easy to implement and use, but the notifications look <span style="font-style: italic">so</span> out of place`,
+    "I also wanted to add more random events, especially to the browsing the web action. Imagine finding a plant store page that adds an action to buy said plant, which you could then care for each day. Stuff like that.",
+    "Anyways. I guess that's it. I hope you enjoyed this experience. Once TMT-X is closer to public release I'll probably use it to finish/continue this game.",
+    "Or not. Historically I don't really return to my game jam games.",
+    `Although the mod loader made for Lit (<a href="https://qq1010903229.github.io/lit/">Lit+</a>) is fantastic, and makes me want to work on that game more. It's probably my favorite TMT mod, apart from Kronos.`,
+    "I actually hadn't told anyone about this publicly yet, but since you've been reading thse I guess it won't hurt:",
+    "Loader gave me permission to use his content if/when I port Lit to TMT. Lit+ will become part of the base game! I'm very excited to do that. :D",
+    `So yeah. Thanks for playing. See you in the remake of this game. <span style="font-style: italic">oh god I'll have to rewrite all of this</span> `
+];
+
 const actions = {
     develop: {
         icon: "code",
@@ -216,7 +264,42 @@ const actions = {
         events: [
             {
                 event() {
-                    const description = developSteps[player.devStep as number];
+                    const step = player.devStep as number;
+                    let description;
+                    if (step < chapterOne.length) {
+                        description = chapterOne[step];
+                    } else {
+                        if (step === chapterOne.length) {
+                            player.layers.main.boards.main.nodes.push({
+                                id: getUniqueNodeID(layers.main.boards!.data.main),
+                                position: { x: 0, y: 0 },
+                                type: "resource",
+                                data: {
+                                    resourceType: "money",
+                                    amount: 10
+                                } as ResourceNodeData
+                            });
+                            player.layers.main.boards.main.nodes.push({
+                                id: getUniqueNodeID(layers.main.boards!.data.main),
+                                position: { x: -300, y: 150 },
+                                type: "action",
+                                data: {
+                                    actionType: "job",
+                                    log: []
+                                } as ActionNodeData
+                            });
+                            player.layers.main.boards.main.nodes.push({
+                                id: getUniqueNodeID(layers.main.boards!.data.main),
+                                position: { x: 300, y: 150 },
+                                type: "action",
+                                data: {
+                                    actionType: "home",
+                                    log: []
+                                } as ActionNodeData
+                            });
+                        }
+                        description = chapterTwo[step - chapterOne.length];
+                    }
                     (player.devStep as number)++;
                     return { description };
                 },
@@ -294,11 +377,22 @@ const actions = {
                 }
             }
         ],
-        baseChanges: [
-            { resource: "time", amount: 16 * 60 * 60, assign: true },
-            { resource: "energy", amount: 100, assign: true },
-            { resource: "hunger", amount: 20 }
-        ]
+        baseChanges: () => {
+            if (chapter.value === 1) {
+                return [
+                    { resource: "time", amount: 16 * 60 * 60, assign: true },
+                    { resource: "energy", amount: 100, assign: true },
+                    { resource: "hunger", amount: 20 }
+                ];
+            } else {
+                return [
+                    { resource: "time", amount: 16 * 60 * 60, assign: true },
+                    { resource: "energy", amount: 100, assign: true },
+                    { resource: "hunger", amount: 20 },
+                    { resource: "money", amount: -25 }
+                ];
+            }
+        }
     },
     forcedSleep: {
         events: [
@@ -316,10 +410,20 @@ const actions = {
                 weight: 1
             }
         ],
-        baseChanges: [
-            { resource: "mental", amount: -10 },
-            { resource: "hunger", amount: 20 }
-        ]
+        baseChanges: () => {
+            if (chapter.value === 1) {
+                return [
+                    { resource: "mental", amount: -10 },
+                    { resource: "hunger", amount: 20 }
+                ];
+            } else {
+                return [
+                    { resource: "mental", amount: -10 },
+                    { resource: "hunger", amount: 20 },
+                    { resource: "money", amount: -25 }
+                ];
+            }
+        }
     },
     rest: {
         icon: "chair",
@@ -426,11 +530,22 @@ const actions = {
                 weight: 1
             }
         ],
-        baseChanges: [
-            { resource: "time", amount: -30 * 60 },
-            { resource: "energy", amount: 10 },
-            { resource: "hunger", amount: -70 }
-        ]
+        baseChanges: () => {
+            if (chapter.value === 1) {
+                return [
+                    { resource: "time", amount: -30 * 60 },
+                    { resource: "energy", amount: 10 },
+                    { resource: "hunger", amount: -70 }
+                ];
+            } else {
+                return [
+                    { resource: "time", amount: -30 * 60 },
+                    { resource: "energy", amount: 10 },
+                    { resource: "hunger", amount: -70 },
+                    { resource: "money", amount: 10 }
+                ];
+            }
+        }
     },
     snack: {
         icon: "icecream",
@@ -462,11 +577,22 @@ const actions = {
                 weight: 1
             }
         ],
-        baseChanges: [
-            { resource: "time", amount: -20 * 60 },
-            { resource: "mental", amount: 2 },
-            { resource: "hunger", amount: -25 }
-        ]
+        baseChanges: () => {
+            if (chapter.value === 1) {
+                return [
+                    { resource: "time", amount: -20 * 60 },
+                    { resource: "mental", amount: 2 },
+                    { resource: "hunger", amount: -25 }
+                ];
+            } else {
+                return [
+                    { resource: "time", amount: -20 * 60 },
+                    { resource: "mental", amount: 2 },
+                    { resource: "hunger", amount: -25 },
+                    { resource: "money", amount: 5 }
+                ];
+            }
+        }
     },
     forcedSnack: {
         events: [
@@ -481,10 +607,20 @@ const actions = {
                 weight: 1
             }
         ],
-        baseChanges: [
-            { resource: "time", amount: -20 * 60 },
-            { resource: "hunger", amount: -25 }
-        ]
+        baseChanges: () => {
+            if (chapter.value === 1) {
+                return [
+                    { resource: "time", amount: -20 * 60 },
+                    { resource: "hunger", amount: -25 }
+                ];
+            } else {
+                return [
+                    { resource: "time", amount: -20 * 60 },
+                    { resource: "hunger", amount: -25 },
+                    { resource: "money", amount: 5 }
+                ];
+            }
+        }
     },
     brush: {
         icon: "mood",
@@ -507,6 +643,90 @@ const actions = {
             { resource: "energy", amount: -2 },
             { resource: "mental", amount: 2 }
         ]
+    },
+    work: {
+        icon: "work",
+        tooltip: "Work",
+        events: [
+            {
+                event: () => ({ description: "Work was... tolerable... today" }),
+                weight: 8
+            },
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.sub(resources.mental.amount, 12);
+                    return {
+                        description: `Work was a nightmare today. You're pretty sure the shades of red on your boss' angry face aren't healthy`,
+                        effectDescription: `-12% <span style="color: ${resources.mental.color}">Mental</span> `
+                    };
+                },
+                weight: 1
+            },
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.add(resources.mental.amount, 4);
+                    resources.money.amount = Decimal.add(resources.money.amount, 20);
+                    return {
+                        description:
+                            "You were so productive today! The boss even handed you a below-the-table bonus at the end of the day. Wow!",
+                        effectDescription: `+4% <span style="color: ${resources.mental.color}">Mental</span>, +$20 <span style="color: ${resources.money.color}">Money</span> `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -9 * 60 * 60 },
+            { resource: "hunger", amount: -50 },
+            { resource: "money", amount: 40 },
+            { resource: "energy", amount: -70 }
+        ]
+    },
+    promote: {
+        icon: "badge",
+        tooltip: "Ask for promotion",
+        events: [
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.sub(resources.mental.amount, 10);
+                    return {
+                        description:
+                            "I didn't get promoted :( This could be bad luck or the result of the develop not having enough time to implement experience lol",
+                        effectDescription: `-10% <span style="color: ${resources.mental.color}">Mental</span> `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [
+            { resource: "time", amount: -60 * 60 },
+            { resource: "energy", amount: -20 }
+        ]
+    },
+    money: {
+        icon: "attach_money",
+        tooltip: "Ask parents for money",
+        events: [
+            {
+                event: () => {
+                    resources.mental.amount = Decimal.sub(
+                        resources.mental.amount,
+                        Decimal.times(5, Decimal.add(player.moneyRequests as DecimalSource, 1))
+                    );
+                    player.moneyRequests = Decimal.add(player.moneyRequests as DecimalSource, 1);
+                    return {
+                        description:
+                            "I asked my parents for money and got $100. That should help, but every time I ask I feel guiltier",
+                        effectDescription: `-${Decimal.times(
+                            5,
+                            player.moneyRequests as DecimalSource
+                        )}% <span style="color: ${resources.mental.color}">Mental</span> `
+                    };
+                },
+                weight: 1
+            }
+        ],
+        baseChanges: [{ resource: "money", amount: 100 }]
     }
 } as Record<string, Action>;
 
@@ -556,6 +776,14 @@ const actionNodes = {
     food: {
         actions: ["eat", "snack", "brush"],
         display: "Food"
+    },
+    job: {
+        actions: ["work", "promote"],
+        display: "Job"
+    },
+    home: {
+        actions: ["money"],
+        display: "Home"
     }
 } as Record<string, ActionNode>;
 
@@ -634,11 +862,19 @@ const resourceNodeType = {
                 };
             }
             const action = actions[selectedAction.value.id];
-            const change = action.baseChanges?.find(change => change.resource === resource.name);
+            const baseChanges =
+                action.baseChanges &&
+                (typeof action.baseChanges === "function"
+                    ? action.baseChanges()
+                    : action.baseChanges);
+            const change = baseChanges?.find(change => change.resource === resource.name);
             if (change != null) {
                 let text = Decimal.gt(change.amount, 0) ? "+" : "";
                 if (resource.name === "time") {
+                    // TODO only apply focusMult if focus is on this action
                     text += formatTime(Decimal.div(change.amount, focusMult.value));
+                } else if (resource.name === "money") {
+                    text = "$" + formatWhole(change.amount);
                 } else if (Decimal.eq(resource.maxAmount, 100)) {
                     text += formatWhole(change.amount) + "%";
                 } else {
@@ -669,6 +905,8 @@ const resourceNodeType = {
                     ).neg();
                     if (resource.name === "time") {
                         text = formatTime(amount);
+                    } else if (resource.name === "money") {
+                        text = "$" + formatWhole(resource.amount);
                     } else if (Decimal.eq(resource.maxAmount, 100)) {
                         text = formatWhole(amount) + "%";
                     } else {
@@ -688,6 +926,9 @@ const resourceNodeType = {
             if (data.resourceType === "time") {
                 return { text: formatTime(data.amount), color: resource.color };
             }
+            if (resource.name === "money") {
+                return { text: "$" + formatWhole(resource.amount), color: resource.color };
+            }
             if (Decimal.eq(resource.maxAmount, 100)) {
                 return {
                     text: formatWhole(data.amount) + "%",
@@ -738,7 +979,9 @@ function performAction(id: string, action: Action, node: BoardNode) {
         resources.focus.amount = 10;
     }
     if (action.baseChanges) {
-        for (const change of action.baseChanges) {
+        const baseChanges =
+            typeof action.baseChanges === "function" ? action.baseChanges() : action.baseChanges;
+        for (const change of baseChanges) {
             if (change.assign) {
                 resources[change.resource].amount = change.amount;
             } else if (change.resource === "time") {
@@ -808,6 +1051,11 @@ const actionNodeType = {
                         }
                     },
                     links(node) {
+                        const baseChanges =
+                            action.baseChanges &&
+                            (typeof action.baseChanges === "function"
+                                ? action.baseChanges()
+                                : action.baseChanges);
                         return [
                             {
                                 from: resources.focus.node,
@@ -820,7 +1068,7 @@ const actionNodeType = {
                                 "stroke-width": 4,
                                 pulsing: true
                             },
-                            ...(action.baseChanges || []).map(change => {
+                            ...(baseChanges || []).map(change => {
                                 let color;
                                 if (change.assign) {
                                     color = "white";
@@ -853,18 +1101,24 @@ function registerResourceDepletedAction(
     watch(
         () => ({
             amount: resources[resource].amount,
-            forcedAction: player.layers.main?.forcedAction
+            forcedAction: player.layers.main?.forcedAction,
+            node: layers.main?.boards?.data.main.nodes.find(
+                node =>
+                    node.type === "action" && (node.data as ActionNodeData).actionType === nodeID
+            )
         }),
-        ({ amount, forcedAction }) => {
-            if (Decimal.eq(amount, threshold) && forcedAction == null) {
-                toast.error(coerceComponent(`${camelToTitle(resources[resource].name)} depleted!`));
+        ({ amount, forcedAction, node }) => {
+            if (Decimal.eq(amount, threshold) && forcedAction == null && node != null) {
+                toast.error(
+                    coerceComponent(
+                        `${camelToTitle(resources[resource].name)} depleted!`,
+                        "span",
+                        false
+                    )
+                );
                 player.layers.main.forcedAction = {
                     resource,
-                    node: layers.main.boards!.data.main.nodes.find(
-                        node =>
-                            node.type === "action" &&
-                            (node.data as ActionNodeData).actionType === nodeID
-                    )!.id,
+                    node: node.id,
                     action,
                     progress: 0
                 };
@@ -876,6 +1130,7 @@ function registerResourceDepletedAction(
 registerResourceDepletedAction("time", "bed", "forcedSleep");
 registerResourceDepletedAction("energy", "bed", "forcedRest");
 registerResourceDepletedAction("hunger", "food", "forcedSnack", 100);
+registerResourceDepletedAction("money", "home", "money");
 
 export default {
     id: "main",
@@ -931,7 +1186,7 @@ export default {
                 startNodes() {
                     return [
                         {
-                            position: { x: 0, y: 0 },
+                            position: { x: 0, y: -150 },
                             type: "resource",
                             data: {
                                 resourceType: "time",
@@ -980,7 +1235,7 @@ export default {
                             } as ActionNodeData
                         },
                         {
-                            position: { x: 150, y: 150 },
+                            position: { x: 0, y: 150 },
                             type: "action",
                             data: {
                                 actionType: "bed",
@@ -988,7 +1243,7 @@ export default {
                             } as ActionNodeData
                         },
                         {
-                            position: { x: -300, y: 150 },
+                            position: { x: 150, y: 150 },
                             type: "action",
                             data: {
                                 actionType: "food",
@@ -1009,8 +1264,10 @@ export default {
                                 const data = node.data as ItemNodeData;
                                 const resource = resources[data.resource];
                                 let text;
-                                if (data.resource === "time") {
+                                if (resource.name === "time") {
                                     text = formatTime(data.amount);
+                                } else if (resource.name === "money") {
+                                    text = "$" + formatWhole(resource.amount);
                                 } else if (Decimal.eq(100, resource.maxAmount)) {
                                     text = format(data.amount) + "%";
                                 }
diff --git a/src/data/mod.ts b/src/data/mod.ts
index f8ad848..4b9051c 100644
--- a/src/data/mod.ts
+++ b/src/data/mod.ts
@@ -1,3 +1,4 @@
+import player from "@/game/player";
 import { RawLayer } from "@/typings/layer";
 import { PlayerData } from "@/typings/player";
 import Decimal from "@/util/bignum";
@@ -15,12 +16,13 @@ export function getStartingData(): Record<string, unknown> {
         day: new Decimal(1),
         lastDayBedMade: new Decimal(0),
         lastDayBrushed: new Decimal(0),
-        devStep: 0
+        devStep: 0,
+        moneyRequests: new Decimal(0)
     };
 }
 
 export const hasWon = computed(() => {
-    return false;
+    return (player.devSpeed as number) >= 61;
 });
 
 export const pointGain = computed(() => {
diff --git a/src/util/vue.ts b/src/util/vue.ts
index b3d953a..a530e0a 100644
--- a/src/util/vue.ts
+++ b/src/util/vue.ts
@@ -36,11 +36,12 @@ const data = function(): Record<string, unknown> {
 };
 export function coerceComponent(
     component: string | ComponentOptions | Component,
-    defaultWrapper = "span"
+    defaultWrapper = "span",
+    allowComponentNames = true
 ): Component | string {
     if (typeof component === "string") {
         component = component.trim();
-        if (!(component in vue._context.components)) {
+        if (!allowComponentNames || !(component in vue._context.components)) {
             if (component.charAt(0) !== "<") {
                 component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
             }
@@ -48,7 +49,7 @@ export function coerceComponent(
             return defineComponent({
                 template: component,
                 data,
-                inject: ["tab"],
+                mixins: [InjectLayerMixin],
                 methods: {
                     hasUpgrade,
                     hasMilestone,
@@ -107,7 +108,7 @@ export const InjectLayerMixin = {
         layer: {
             type: String,
             default(): string {
-                return (inject("tab") as { layer: string }).layer;
+                return (inject("tab", { layer: "" }) as { layer: string }).layer;
             }
         }
     }

From 72e73db0484a778183a0baa46f7e9d9bdd63cf1a Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 28 Aug 2021 11:35:25 -0500
Subject: [PATCH 48/49] Fixed things happening on save load

---
 src/data/layers/main.ts | 2 +-
 src/game/gameLoop.ts    | 2 ++
 src/game/player.ts      | 1 +
 src/typings/player.d.ts | 1 +
 src/util/save.ts        | 6 ++++--
 5 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/data/layers/main.ts b/src/data/layers/main.ts
index a19960f..d3a8610 100644
--- a/src/data/layers/main.ts
+++ b/src/data/layers/main.ts
@@ -820,7 +820,7 @@ for (const id in resources) {
     watch(
         () => resource.amount,
         (amount, oldAmount) => {
-            if (amount == null || oldAmount == null) {
+            if (amount == null || oldAmount == null || player.justLoaded) {
                 return;
             }
             const resourceGain = Decimal.sub(amount, oldAmount);
diff --git a/src/game/gameLoop.ts b/src/game/gameLoop.ts
index d6c3f52..c1f9968 100644
--- a/src/game/gameLoop.ts
+++ b/src/game/gameLoop.ts
@@ -172,6 +172,8 @@ function update() {
     modUpdate(diff);
     updateOOMPS(trueDiff);
     updateLayers(diff);
+
+    player.justLoaded = false;
 }
 
 export default function startGameLoop(): void {
diff --git a/src/game/player.ts b/src/game/player.ts
index bf029cd..bcee7a1 100644
--- a/src/game/player.ts
+++ b/src/game/player.ts
@@ -27,6 +27,7 @@ const state = reactive<PlayerData>({
     minimized: {},
     modID: "",
     modVersion: "",
+    justLoaded: false,
     hasNaN: false,
     NaNPath: [],
     NaNReceiver: null,
diff --git a/src/typings/player.d.ts b/src/typings/player.d.ts
index 71d6748..deaef27 100644
--- a/src/typings/player.d.ts
+++ b/src/typings/player.d.ts
@@ -38,6 +38,7 @@ export interface PlayerData {
     minimized: Record<string, boolean>;
     modID: string;
     modVersion: string;
+    justLoaded: boolean;
     hasNaN: boolean;
     NaNPath?: Array<string>;
     NaNReceiver?: Record<string, unknown> | null;
diff --git a/src/util/save.ts b/src/util/save.ts
index f87b334..5684a4e 100644
--- a/src/util/save.ts
+++ b/src/util/save.ts
@@ -31,6 +31,7 @@ export function getInitialStore(playerData: Partial<PlayerData> = {}): PlayerDat
             modID: modInfo.id,
             modVersion: modInfo.versionNumber,
             layers: {},
+            justLoaded: false,
             ...getStartingData(),
 
             // Values that don't get loaded/saved
@@ -148,6 +149,7 @@ export async function loadSave(playerData: Partial<PlayerData>): Promise<void> {
             delete player.layers[prop];
         }
     }
+    player.justLoaded = true;
 }
 
 export function applyPlayerData<T extends Record<string, any>>(
@@ -187,9 +189,9 @@ window.onbeforeunload = () => {
     }
 };
 window.save = save;
-export const hardReset = window.hardReset = async () => {
+export const hardReset = (window.hardReset = async () => {
     await loadSave(newSave());
     const modData = JSON.parse(decodeURIComponent(escape(atob(localStorage.getItem(modInfo.id)!))));
     modData.active = player.id;
     localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(modData)))));
-};
+});

From 8da668b8d6c919199731d1ba3dd245b1d487e00b Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sat, 28 Aug 2021 12:08:14 -0500
Subject: [PATCH 49/49] Fixed build

---
 vue.config.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vue.config.js b/vue.config.js
index 34b03af..2482099 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -1,5 +1,5 @@
 module.exports = {
-    publicPath: process.env.NODE_ENV === "production" ? "/The-Modding-Tree-X" : "/",
+    publicPath: process.env.NODE_ENV === "production" ? "./" : "/",
     runtimeCompiler: true,
     chainWebpack(config) {
         config.resolve.alias.delete("@");