diff --git a/apps/api/src/routes/debug.ts b/apps/api/src/routes/debug.ts index 080e5f5..b50318f 100644 --- a/apps/api/src/routes/debug.ts +++ b/apps/api/src/routes/debug.ts @@ -642,6 +642,14 @@ const patchAdventurerStats = (state: GameState): number => { if (defaultAdventurer === undefined) { continue; } + const hasChanged + = savedAdventurer.baseCost !== defaultAdventurer.baseCost + || savedAdventurer.class !== defaultAdventurer.class + || savedAdventurer.combatPower !== defaultAdventurer.combatPower + || savedAdventurer.essencePerSecond !== defaultAdventurer.essencePerSecond + || savedAdventurer.goldPerSecond !== defaultAdventurer.goldPerSecond + || savedAdventurer.level !== defaultAdventurer.level + || savedAdventurer.name !== defaultAdventurer.name; savedAdventurer.baseCost = defaultAdventurer.baseCost; savedAdventurer.class = defaultAdventurer.class; savedAdventurer.combatPower = defaultAdventurer.combatPower; @@ -649,7 +657,9 @@ const patchAdventurerStats = (state: GameState): number => { savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond; savedAdventurer.level = defaultAdventurer.level; savedAdventurer.name = defaultAdventurer.name; - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -670,6 +680,15 @@ const patchQuestStats = (state: GameState): number => { if (defaultQuest === undefined) { continue; } + const savedPrereqs = JSON.stringify(savedQuest.prerequisiteIds); + const defaultPrereqs = JSON.stringify(defaultQuest.prerequisiteIds); + const hasChanged + = savedQuest.name !== defaultQuest.name + || savedQuest.description !== defaultQuest.description + || savedQuest.durationSeconds !== defaultQuest.durationSeconds + || savedPrereqs !== defaultPrereqs + || savedQuest.zoneId !== defaultQuest.zoneId + || savedQuest.combatPowerRequired !== defaultQuest.combatPowerRequired; savedQuest.name = defaultQuest.name; savedQuest.description = defaultQuest.description; savedQuest.durationSeconds = defaultQuest.durationSeconds; @@ -678,7 +697,9 @@ const patchQuestStats = (state: GameState): number => { if (defaultQuest.combatPowerRequired !== undefined) { savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired; } - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -689,6 +710,7 @@ const patchQuestStats = (state: GameState): number => { * @param state - The player's current game state (mutated in place). * @returns The number of boss entries whose stats were updated. */ +/* eslint-disable-next-line complexity, max-statements -- Comparing many boss stat fields for change detection */ const patchBossStats = (state: GameState): number => { const defaultBossMap = new Map(defaultBosses.map((boss) => { return [ boss.id, boss ] as const; @@ -699,6 +721,20 @@ const patchBossStats = (state: GameState): number => { if (defaultBoss === undefined) { continue; } + const savedRewards = JSON.stringify(savedBoss.equipmentRewards); + const defaultRewards = JSON.stringify(defaultBoss.equipmentRewards); + const hasChanged + = savedBoss.name !== defaultBoss.name + || savedBoss.description !== defaultBoss.description + || savedBoss.maxHp !== defaultBoss.maxHp + || savedBoss.damagePerSecond !== defaultBoss.damagePerSecond + || savedBoss.goldReward !== defaultBoss.goldReward + || savedBoss.essenceReward !== defaultBoss.essenceReward + || savedBoss.crystalReward !== defaultBoss.crystalReward + || savedRewards !== defaultRewards + || savedBoss.prestigeRequirement !== defaultBoss.prestigeRequirement + || savedBoss.zoneId !== defaultBoss.zoneId + || savedBoss.bountyRunestones !== defaultBoss.bountyRunestones; savedBoss.name = defaultBoss.name; savedBoss.description = defaultBoss.description; savedBoss.maxHp = defaultBoss.maxHp; @@ -710,7 +746,9 @@ const patchBossStats = (state: GameState): number => { savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement; savedBoss.zoneId = defaultBoss.zoneId; savedBoss.bountyRunestones = defaultBoss.bountyRunestones; - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -731,12 +769,20 @@ const patchZoneStats = (state: GameState): number => { if (defaultZone === undefined) { continue; } + const hasChanged + = savedZone.name !== defaultZone.name + || savedZone.description !== defaultZone.description + || savedZone.emoji !== defaultZone.emoji + || savedZone.unlockBossId !== defaultZone.unlockBossId + || savedZone.unlockQuestId !== defaultZone.unlockQuestId; savedZone.name = defaultZone.name; savedZone.description = defaultZone.description; savedZone.emoji = defaultZone.emoji; savedZone.unlockBossId = defaultZone.unlockBossId; savedZone.unlockQuestId = defaultZone.unlockQuestId; - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -747,6 +793,7 @@ const patchZoneStats = (state: GameState): number => { * @param state - The player's current game state (mutated in place). * @returns The number of upgrade entries whose stats were updated. */ +/* eslint-disable-next-line complexity -- Comparing many upgrade stat fields for change detection */ const patchUpgradeStats = (state: GameState): number => { const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => { return [ upgrade.id, upgrade ] as const; @@ -757,6 +804,15 @@ const patchUpgradeStats = (state: GameState): number => { if (defaultUpgrade === undefined) { continue; } + const hasChanged + = savedUpgrade.name !== defaultUpgrade.name + || savedUpgrade.description !== defaultUpgrade.description + || savedUpgrade.target !== defaultUpgrade.target + || savedUpgrade.adventurerId !== defaultUpgrade.adventurerId + || savedUpgrade.multiplier !== defaultUpgrade.multiplier + || savedUpgrade.costGold !== defaultUpgrade.costGold + || savedUpgrade.costEssence !== defaultUpgrade.costEssence + || savedUpgrade.costCrystals !== defaultUpgrade.costCrystals; savedUpgrade.name = defaultUpgrade.name; savedUpgrade.description = defaultUpgrade.description; savedUpgrade.target = defaultUpgrade.target; @@ -767,7 +823,9 @@ const patchUpgradeStats = (state: GameState): number => { savedUpgrade.costGold = defaultUpgrade.costGold; savedUpgrade.costEssence = defaultUpgrade.costEssence; savedUpgrade.costCrystals = defaultUpgrade.costCrystals; - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -778,6 +836,7 @@ const patchUpgradeStats = (state: GameState): number => { * @param state - The player's current game state (mutated in place). * @returns The number of equipment entries whose stats were updated. */ +/* eslint-disable-next-line complexity, max-statements -- Comparing many equipment stat fields for change detection */ const patchEquipmentStats = (state: GameState): number => { const defaultEquipmentMap = new Map(defaultEquipment.map((item) => { return [ item.id, item ] as const; @@ -788,6 +847,18 @@ const patchEquipmentStats = (state: GameState): number => { if (defaultItem === undefined) { continue; } + const savedBonus = JSON.stringify(savedItem.bonus); + const defaultBonus = JSON.stringify(defaultItem.bonus); + const savedCost = JSON.stringify(savedItem.cost); + const defaultCost = JSON.stringify(defaultItem.cost); + const hasChanged + = savedItem.name !== defaultItem.name + || savedItem.description !== defaultItem.description + || savedItem.type !== defaultItem.type + || savedItem.rarity !== defaultItem.rarity + || savedBonus !== defaultBonus + || savedCost !== defaultCost + || savedItem.setId !== defaultItem.setId; savedItem.name = defaultItem.name; savedItem.description = defaultItem.description; savedItem.type = defaultItem.type; @@ -799,7 +870,9 @@ const patchEquipmentStats = (state: GameState): number => { if (defaultItem.setId !== undefined) { savedItem.setId = defaultItem.setId; } - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; @@ -820,6 +893,16 @@ const patchAchievementStats = (state: GameState): number => { if (defaultAchievement === undefined) { continue; } + const savedCondition = JSON.stringify(savedAchievement.condition); + const defaultCondition = JSON.stringify(defaultAchievement.condition); + const savedReward = JSON.stringify(savedAchievement.reward); + const defaultReward = JSON.stringify(defaultAchievement.reward); + const hasChanged + = savedAchievement.name !== defaultAchievement.name + || savedAchievement.description !== defaultAchievement.description + || savedAchievement.icon !== defaultAchievement.icon + || savedCondition !== defaultCondition + || savedReward !== defaultReward; savedAchievement.name = defaultAchievement.name; savedAchievement.description = defaultAchievement.description; savedAchievement.icon = defaultAchievement.icon; @@ -827,7 +910,9 @@ const patchAchievementStats = (state: GameState): number => { if (defaultAchievement.reward !== undefined) { savedAchievement.reward = { ...defaultAchievement.reward }; } - patched = patched + 1; + if (hasChanged) { + patched = patched + 1; + } } return patched; }; diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index cb76ba0..ec4d8a2 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -10,7 +10,11 @@ /* eslint-disable complexity -- Many conditional resource and badge render paths */ import { useState, type FocusEvent, type JSX } from "react"; import { useGame } from "../../context/gameContext.js"; -import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js"; +import { + RESOURCE_CAP, + computeEssencePerSecond, + computeGoldPerSecond, +} from "../../engine/tick.js"; import type { Resource } from "@elysium/types"; interface ResourceBarProperties { @@ -83,12 +87,14 @@ const ResourceBar = ({ const { gold, essence, crystals } = resources; let partyCombatPower = 0; let goldPerSecond = 0; + let essencePerSecond = 0; if (state !== null) { for (const adventurer of state.adventurers) { const contribution = adventurer.combatPower * adventurer.count; partyCombatPower = partyCombatPower + contribution; } goldPerSecond = computeGoldPerSecond(state); + essencePerSecond = computeEssencePerSecond(state); } let avatarUrl: string | null = null; @@ -182,6 +188,13 @@ const ResourceBar = ({ {"Gold/s"} +
+ {"⚡"} + + {formatNumber(essencePerSecond)} + + {"Essence/s"} +
diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index fd89b84..24762bb 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -1127,7 +1127,7 @@ export const GameProvider = ({ return adventurer.unlocked && next.resources.gold >= cost; }). sort((adventurerA, adventurerB) => { - return adventurerB.combatPower - adventurerA.combatPower; + return adventurerB.level - adventurerA.level; }); if (bestAdventurer !== undefined) { const purchaseCost diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index c80f8e5..7b77cb0 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -195,6 +195,54 @@ export const computeGoldPerSecond = (state: GameState): number => { return goldPerSecond; }; +/** + * Computes the current essence per second for the given game state, + * applying all relevant multipliers (upgrades, prestige, echo, crafted, companion). + * @param state - The current game state. + * @returns The total essence per second. + */ +export const computeEssencePerSecond = (state: GameState): number => { + const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; + const craftedEssenceMultiplier + = state.exploration?.craftedEssenceMultiplier ?? 1; + const companionBonus = getActiveCompanionBonus( + state.companions?.activeCompanionId, + state.companions?.unlockedCompanionIds ?? [], + ); + const companionEssenceMult + = companionBonus?.type === "essenceIncome" + ? 1 + companionBonus.value + : 1; + + let essencePerSecond = 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.essencePerSecond + * adventurer.count + * upgradeMultiplier + * state.prestige.productionMultiplier + * runestonesEssence + * craftedEssenceMultiplier + * companionEssenceMult; + essencePerSecond = essencePerSecond + contribution; + } + return essencePerSecond; +}; + /** * Pure function — applies one game tick to the state. * DeltaSeconds: time elapsed since last tick.