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("@");