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