From 06c80e186a7854ecf3ebe29371ebac15f331c785 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Mar 2026 13:52:02 -0700 Subject: [PATCH] feat: gold/sec display with multipliers (#100) and peasant late-game upgrades (#101) --- apps/api/src/data/quests.ts | 2 + apps/api/src/data/upgrades.ts | 28 +++++++++ apps/web/src/components/ui/resourceBar.tsx | 9 ++- apps/web/src/engine/tick.ts | 72 ++++++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/apps/api/src/data/quests.ts b/apps/api/src/data/quests.ts index 94def83..b33b3a0 100644 --- a/apps/api/src/data/quests.ts +++ b/apps/api/src/data/quests.ts @@ -183,6 +183,7 @@ export const defaultQuests: Array = [ { amount: 1500, type: "essence" }, { amount: 75, type: "crystals" }, { targetId: "knight_1", type: "upgrade" }, + { targetId: "peasant_2", type: "upgrade" }, ], status: "locked", zoneId: "shadow_marshes", @@ -282,6 +283,7 @@ export const defaultQuests: Array = [ { amount: 40_000_000, type: "gold" }, { amount: 12_000, type: "essence" }, { amount: 300, type: "crystals" }, + { targetId: "peasant_3", type: "upgrade" }, ], status: "locked", zoneId: "volcanic_depths", diff --git a/apps/api/src/data/upgrades.ts b/apps/api/src/data/upgrades.ts index 3352ed9..1f06343 100644 --- a/apps/api/src/data/upgrades.ts +++ b/apps/api/src/data/upgrades.ts @@ -162,6 +162,34 @@ export const defaultUpgrades: Array = [ target: "adventurer", unlocked: false, }, + { + adventurerId: "peasant", + costCrystals: 0, + costEssence: 20, + costGold: 0, + description: + "Organised labour guilds and proper scheduling make peasants ten times more productive.", + id: "peasant_2", + multiplier: 10, + name: "Guild Organisation", + purchased: false, + target: "adventurer", + unlocked: false, + }, + { + adventurerId: "peasant", + costCrystals: 50, + costEssence: 0, + costGold: 0, + description: + "Magical augmentation through crystalline resonance supercharges even the humblest worker.", + id: "peasant_3", + multiplier: 50, + name: "Crystal Augmentation", + purchased: false, + target: "adventurer", + unlocked: false, + }, { adventurerId: "militia", costCrystals: 0, diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index 0a20d27..a7599cd 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -7,7 +7,7 @@ /* eslint-disable max-lines-per-function -- Large header with many resource and action elements */ /* eslint-disable complexity -- Many conditional resource and badge render paths */ import { useGame } from "../../context/gameContext.js"; -import { RESOURCE_CAP } from "../../engine/tick.js"; +import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js"; import type { Resource } from "@elysium/types"; import type { JSX } from "react"; @@ -80,11 +80,13 @@ const ResourceBar = ({ const { formatNumber, syncError, state } = useGame(); const { gold, essence, crystals } = resources; let partyCombatPower = 0; + let goldPerSecond = 0; if (state !== null) { for (const adventurer of state.adventurers) { const contribution = adventurer.combatPower * adventurer.count; partyCombatPower = partyCombatPower + contribution; } + goldPerSecond = computeGoldPerSecond(state); } const resourceValues = [ gold, essence, crystals ]; const anyFull = resourceValues.some((v) => { @@ -113,6 +115,11 @@ const ResourceBar = ({ : null} +
+ {"📈"} + {formatNumber(goldPerSecond)} + {"Gold/s"} +
diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index 4bf4acf..c80f8e5 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -123,6 +123,78 @@ const capResource = (value: number): number => { return Math.min(value, RESOURCE_CAP); }; +/** + * Pure function — applies one game tick to the state. + * DeltaSeconds: time elapsed since last tick. + * Returns a new GameState (does not mutate the original). + * @param state - The current game state. + * @param deltaSeconds - Time elapsed since last tick in seconds. + * @returns A new GameState with the tick applied. + */ +/** + * Computes the effective gold earned per second across all adventurers, + * including all active multipliers (upgrades, prestige, equipment, etc.). + * @param state - The current game state. + * @returns Gold per second as a number. + */ +export const computeGoldPerSecond = (state: GameState): number => { + const equippedItems: Array = state.equipment.filter((item) => { + return item.equipped; + }); + const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => { + return mult * (item.bonus.goldMultiplier ?? 1); + }, 1); + const setGoldMultiplier = computeSetBonuses( + equippedItems.map((item) => { + return item.id; + }), + EQUIPMENT_SETS, + ).goldMultiplier; + + const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; + const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1; + const craftedGoldMultiplier = state.exploration?.craftedGoldMultiplier ?? 1; + const companionBonus = getActiveCompanionBonus( + state.companions?.activeCompanionId, + state.companions?.unlockedCompanionIds ?? [], + ); + const companionGoldMult + = companionBonus?.type === "passiveGold" + ? 1 + companionBonus.value + : 1; + + let goldPerSecond = 0; + for (const adventurer of state.adventurers) { + if (!adventurer.unlocked || adventurer.count === 0) { + continue; + } + const upgradeMultiplier = state.upgrades. + filter((upgrade) => { + const isGlobal = upgrade.target === "global"; + const isThisAdventurer + = upgrade.target === "adventurer" + && upgrade.adventurerId === adventurer.id; + return upgrade.purchased && (isGlobal || isThisAdventurer); + }). + reduce((mult, upgrade) => { + return mult * upgrade.multiplier; + }, 1); + const contribution + = adventurer.goldPerSecond + * adventurer.count + * upgradeMultiplier + * state.prestige.productionMultiplier + * runestonesIncome + * echoIncome + * equipmentGoldMultiplier + * setGoldMultiplier + * craftedGoldMultiplier + * companionGoldMult; + goldPerSecond = goldPerSecond + contribution; + } + return goldPerSecond; +}; + /** * Pure function — applies one game tick to the state. * DeltaSeconds: time elapsed since last tick.