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).