From 8a332dc9cef0c734e5fdcb965af649c887c26689 Mon Sep 17 00:00:00 2001 From: Hikari Date: Wed, 25 Mar 2026 17:13:00 -0700 Subject: [PATCH] fix: show effective post-multiplier stats on adventurer cards (#154) Adds computeEffectiveAdventurerStats to tick.ts to calculate per-unit gold/s, essence/s, and combat power with all active multipliers applied (upgrades, prestige, equipment, echo, crafted, companions). Updates AdventurerCard to display these effective values so players can see the true contribution of each adventurer rather than raw base stats. --- .../src/components/game/adventurerPanel.tsx | 30 +++-- apps/web/src/engine/tick.ts | 112 ++++++++++++++++++ 2 files changed, 134 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/game/adventurerPanel.tsx b/apps/web/src/components/game/adventurerPanel.tsx index d0f9a95..94ff206 100644 --- a/apps/web/src/components/game/adventurerPanel.tsx +++ b/apps/web/src/components/game/adventurerPanel.tsx @@ -9,6 +9,7 @@ /* eslint-disable complexity -- Complex component with many render paths */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; +import { computeEffectiveAdventurerStats } from "../../engine/tick.js"; import { cdnImage } from "../../utils/cdn.js"; import { LockToggle } from "../ui/lockToggle.js"; import type { Adventurer } from "@elysium/types"; @@ -76,12 +77,19 @@ const computeMaxAffordable = (adventurer: Adventurer, gold: number): number => { return quantity; }; +interface EffectiveAdventurerStats { + readonly combatPower: number; + readonly essencePerSecond: number; + readonly goldPerSecond: number; +} + interface AdventurerCardProperties { - readonly adventurer: Adventurer; - readonly currentGold: number; - readonly batchSize: BatchSize; - readonly unlockHint: string | undefined; - readonly formatNumber: (n: number)=> string; + readonly adventurer: Adventurer; + readonly currentGold: number; + readonly batchSize: BatchSize; + readonly unlockHint: string | undefined; + readonly formatNumber: (n: number)=> string; + readonly effectiveStats: EffectiveAdventurerStats; } /** @@ -92,6 +100,7 @@ interface AdventurerCardProperties { * @param props.batchSize - The selected batch size. * @param props.unlockHint - Optional quest name that unlocks this adventurer. * @param props.formatNumber - The number formatting utility function. + * @param props.effectiveStats - The post-multiplier per-unit stats. * @returns The JSX element. */ const AdventurerCard = ({ @@ -100,6 +109,7 @@ const AdventurerCard = ({ batchSize, unlockHint, formatNumber, + effectiveStats, }: AdventurerCardProperties): JSX.Element => { const { buyAdventurer } = useGame(); @@ -134,17 +144,17 @@ const AdventurerCard = ({

{adventurer.name}

- {formatNumber(adventurer.goldPerSecond)} + {formatNumber(effectiveStats.goldPerSecond)} {" gold/s each"}

{adventurer.essencePerSecond > 0 &&

- {formatNumber(adventurer.essencePerSecond)} + {formatNumber(effectiveStats.essencePerSecond)} {" essence/s each"}

}

- {formatNumber(adventurer.combatPower)} + {formatNumber(effectiveStats.combatPower)} {" combat power each"}

@@ -280,6 +290,10 @@ const AdventurerPanel = (): JSX.Element => { adventurer={adventurer} batchSize={batchSize} currentGold={state.resources.gold} + effectiveStats={computeEffectiveAdventurerStats( + state, + adventurer.id, + )} formatNumber={formatNumber} key={adventurer.id} unlockHint={adventurerUnlockHints.get(adventurer.id)} diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index 240edab..145e56b 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -243,6 +243,118 @@ export const computeEssencePerSecond = (state: GameState): number => { return essencePerSecond; }; +/** + * Computes the effective per-unit stats for a single adventurer type, + * applying all active multipliers (upgrades, prestige, equipment, echo, + * crafted, companion). The returned values represent what a single + * adventurer of this type currently contributes per second, matching the + * per-unit contribution used by computeGoldPerSecond and + * computeEssencePerSecond. + * @param state - The current game state. + * @param adventurerId - The ID of the adventurer to compute stats for. + * @returns Effective per-unit goldPerSecond, essencePerSecond, and combatPower. + */ +export const computeEffectiveAdventurerStats = ( + state: GameState, + adventurerId: string, +): { combatPower: number; essencePerSecond: number; goldPerSecond: number } => { + const adventurer = state.adventurers.find((a) => { + return a.id === adventurerId; + }); + + /* V8 ignore next 3 -- @preserve */ + if (adventurer === undefined) { + return { combatPower: 0, essencePerSecond: 0, goldPerSecond: 0 }; + } + + const upgradeMultiplier = state.upgrades. + filter((upgrade) => { + const isGlobal = upgrade.target === "global"; + const isThisAdventurer + = upgrade.target === "adventurer" + && upgrade.adventurerId === adventurerId; + return upgrade.purchased && (isGlobal || isThisAdventurer); + }). + reduce((mult, upgrade) => { + return mult * upgrade.multiplier; + }, 1); + + const equippedItems = state.equipment.filter((item) => { + return item.equipped; + }); + const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => { + return mult * (item.bonus.goldMultiplier ?? 1); + }, 1); + const equipmentCombatMultiplier = equippedItems.reduce((mult, item) => { + return mult * (item.bonus.combatMultiplier ?? 1); + }, 1); + const equippedItemIds = equippedItems.map((item) => { + return item.id; + }); + const setBonuses = computeSetBonuses(equippedItemIds, EQUIPMENT_SETS); + + const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; + const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; + // eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear + const prestigeCombatMultiplier = 1 + state.prestige.count * 0.1; + const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1; + const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1; + const craftedGoldMultiplier + = state.exploration?.craftedGoldMultiplier ?? 1; + const craftedEssenceMultiplier + = state.exploration?.craftedEssenceMultiplier ?? 1; + const craftedCombatMultiplier + = state.exploration?.craftedCombatMultiplier ?? 1; + + const companionBonus = getActiveCompanionBonus( + state.companions?.activeCompanionId, + state.companions?.unlockedCompanionIds ?? [], + ); + const companionGoldMult + = companionBonus?.type === "passiveGold" + ? 1 + companionBonus.value + : 1; + const companionEssenceMult + = companionBonus?.type === "essenceIncome" + ? 1 + companionBonus.value + : 1; + const companionCombatMult + = companionBonus?.type === "bossDamage" + ? 1 + companionBonus.value + : 1; + + const goldPerSecond + = adventurer.goldPerSecond + * upgradeMultiplier + * state.prestige.productionMultiplier + * runestonesIncome + * echoIncome + * equipmentGoldMultiplier + * setBonuses.goldMultiplier + * craftedGoldMultiplier + * companionGoldMult; + + const essencePerSecond + = adventurer.essencePerSecond + * upgradeMultiplier + * state.prestige.productionMultiplier + * runestonesEssence + * craftedEssenceMultiplier + * companionEssenceMult; + + const combatPower + = adventurer.combatPower + * upgradeMultiplier + * prestigeCombatMultiplier + * equipmentCombatMultiplier + * setBonuses.combatMultiplier + * echoCombatMultiplier + * craftedCombatMultiplier + * companionCombatMult; + + return { combatPower, essencePerSecond, goldPerSecond }; +}; + /** * Computes the party's total combat power, applying all active multipliers * (upgrades, prestige, equipment, set bonuses, echo, crafted, companion).