generated from nhcarrigan/template
fix: resolve sync count inflation, add essence/s display, sort auto-buy by level
Closes #147: patch functions now detect actual changes before incrementing the patched counter, preventing inflated sync reports. Closes #149: computeEssencePerSecond exported from tick engine and shown in the resource bar dropdown alongside Gold/s. Closes #150: auto-buy now sorts adventurers by level descending for semantic clarity, ensuring highest-tier units are purchased first.
This commit is contained in:
@@ -642,6 +642,14 @@ const patchAdventurerStats = (state: GameState): number => {
|
|||||||
if (defaultAdventurer === undefined) {
|
if (defaultAdventurer === undefined) {
|
||||||
continue;
|
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.baseCost = defaultAdventurer.baseCost;
|
||||||
savedAdventurer.class = defaultAdventurer.class;
|
savedAdventurer.class = defaultAdventurer.class;
|
||||||
savedAdventurer.combatPower = defaultAdventurer.combatPower;
|
savedAdventurer.combatPower = defaultAdventurer.combatPower;
|
||||||
@@ -649,8 +657,10 @@ const patchAdventurerStats = (state: GameState): number => {
|
|||||||
savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond;
|
savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond;
|
||||||
savedAdventurer.level = defaultAdventurer.level;
|
savedAdventurer.level = defaultAdventurer.level;
|
||||||
savedAdventurer.name = defaultAdventurer.name;
|
savedAdventurer.name = defaultAdventurer.name;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -670,6 +680,15 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
if (defaultQuest === undefined) {
|
if (defaultQuest === undefined) {
|
||||||
continue;
|
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.name = defaultQuest.name;
|
||||||
savedQuest.description = defaultQuest.description;
|
savedQuest.description = defaultQuest.description;
|
||||||
savedQuest.durationSeconds = defaultQuest.durationSeconds;
|
savedQuest.durationSeconds = defaultQuest.durationSeconds;
|
||||||
@@ -678,8 +697,10 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
if (defaultQuest.combatPowerRequired !== undefined) {
|
if (defaultQuest.combatPowerRequired !== undefined) {
|
||||||
savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired;
|
savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired;
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -689,6 +710,7 @@ const patchQuestStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of boss entries whose stats were updated.
|
* @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 patchBossStats = (state: GameState): number => {
|
||||||
const defaultBossMap = new Map(defaultBosses.map((boss) => {
|
const defaultBossMap = new Map(defaultBosses.map((boss) => {
|
||||||
return [ boss.id, boss ] as const;
|
return [ boss.id, boss ] as const;
|
||||||
@@ -699,6 +721,20 @@ const patchBossStats = (state: GameState): number => {
|
|||||||
if (defaultBoss === undefined) {
|
if (defaultBoss === undefined) {
|
||||||
continue;
|
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.name = defaultBoss.name;
|
||||||
savedBoss.description = defaultBoss.description;
|
savedBoss.description = defaultBoss.description;
|
||||||
savedBoss.maxHp = defaultBoss.maxHp;
|
savedBoss.maxHp = defaultBoss.maxHp;
|
||||||
@@ -710,8 +746,10 @@ const patchBossStats = (state: GameState): number => {
|
|||||||
savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement;
|
savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement;
|
||||||
savedBoss.zoneId = defaultBoss.zoneId;
|
savedBoss.zoneId = defaultBoss.zoneId;
|
||||||
savedBoss.bountyRunestones = defaultBoss.bountyRunestones;
|
savedBoss.bountyRunestones = defaultBoss.bountyRunestones;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -731,13 +769,21 @@ const patchZoneStats = (state: GameState): number => {
|
|||||||
if (defaultZone === undefined) {
|
if (defaultZone === undefined) {
|
||||||
continue;
|
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.name = defaultZone.name;
|
||||||
savedZone.description = defaultZone.description;
|
savedZone.description = defaultZone.description;
|
||||||
savedZone.emoji = defaultZone.emoji;
|
savedZone.emoji = defaultZone.emoji;
|
||||||
savedZone.unlockBossId = defaultZone.unlockBossId;
|
savedZone.unlockBossId = defaultZone.unlockBossId;
|
||||||
savedZone.unlockQuestId = defaultZone.unlockQuestId;
|
savedZone.unlockQuestId = defaultZone.unlockQuestId;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -747,6 +793,7 @@ const patchZoneStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of upgrade entries whose stats were updated.
|
* @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 patchUpgradeStats = (state: GameState): number => {
|
||||||
const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => {
|
const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => {
|
||||||
return [ upgrade.id, upgrade ] as const;
|
return [ upgrade.id, upgrade ] as const;
|
||||||
@@ -757,6 +804,15 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
if (defaultUpgrade === undefined) {
|
if (defaultUpgrade === undefined) {
|
||||||
continue;
|
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.name = defaultUpgrade.name;
|
||||||
savedUpgrade.description = defaultUpgrade.description;
|
savedUpgrade.description = defaultUpgrade.description;
|
||||||
savedUpgrade.target = defaultUpgrade.target;
|
savedUpgrade.target = defaultUpgrade.target;
|
||||||
@@ -767,8 +823,10 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
savedUpgrade.costGold = defaultUpgrade.costGold;
|
savedUpgrade.costGold = defaultUpgrade.costGold;
|
||||||
savedUpgrade.costEssence = defaultUpgrade.costEssence;
|
savedUpgrade.costEssence = defaultUpgrade.costEssence;
|
||||||
savedUpgrade.costCrystals = defaultUpgrade.costCrystals;
|
savedUpgrade.costCrystals = defaultUpgrade.costCrystals;
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -778,6 +836,7 @@ const patchUpgradeStats = (state: GameState): number => {
|
|||||||
* @param state - The player's current game state (mutated in place).
|
* @param state - The player's current game state (mutated in place).
|
||||||
* @returns The number of equipment entries whose stats were updated.
|
* @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 patchEquipmentStats = (state: GameState): number => {
|
||||||
const defaultEquipmentMap = new Map(defaultEquipment.map((item) => {
|
const defaultEquipmentMap = new Map(defaultEquipment.map((item) => {
|
||||||
return [ item.id, item ] as const;
|
return [ item.id, item ] as const;
|
||||||
@@ -788,6 +847,18 @@ const patchEquipmentStats = (state: GameState): number => {
|
|||||||
if (defaultItem === undefined) {
|
if (defaultItem === undefined) {
|
||||||
continue;
|
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.name = defaultItem.name;
|
||||||
savedItem.description = defaultItem.description;
|
savedItem.description = defaultItem.description;
|
||||||
savedItem.type = defaultItem.type;
|
savedItem.type = defaultItem.type;
|
||||||
@@ -799,8 +870,10 @@ const patchEquipmentStats = (state: GameState): number => {
|
|||||||
if (defaultItem.setId !== undefined) {
|
if (defaultItem.setId !== undefined) {
|
||||||
savedItem.setId = defaultItem.setId;
|
savedItem.setId = defaultItem.setId;
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -820,6 +893,16 @@ const patchAchievementStats = (state: GameState): number => {
|
|||||||
if (defaultAchievement === undefined) {
|
if (defaultAchievement === undefined) {
|
||||||
continue;
|
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.name = defaultAchievement.name;
|
||||||
savedAchievement.description = defaultAchievement.description;
|
savedAchievement.description = defaultAchievement.description;
|
||||||
savedAchievement.icon = defaultAchievement.icon;
|
savedAchievement.icon = defaultAchievement.icon;
|
||||||
@@ -827,8 +910,10 @@ const patchAchievementStats = (state: GameState): number => {
|
|||||||
if (defaultAchievement.reward !== undefined) {
|
if (defaultAchievement.reward !== undefined) {
|
||||||
savedAchievement.reward = { ...defaultAchievement.reward };
|
savedAchievement.reward = { ...defaultAchievement.reward };
|
||||||
}
|
}
|
||||||
|
if (hasChanged) {
|
||||||
patched = patched + 1;
|
patched = patched + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return patched;
|
return patched;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
/* eslint-disable complexity -- Many conditional resource and badge render paths */
|
/* eslint-disable complexity -- Many conditional resource and badge render paths */
|
||||||
import { useState, type FocusEvent, type JSX } from "react";
|
import { useState, type FocusEvent, type JSX } from "react";
|
||||||
import { useGame } from "../../context/gameContext.js";
|
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";
|
import type { Resource } from "@elysium/types";
|
||||||
|
|
||||||
interface ResourceBarProperties {
|
interface ResourceBarProperties {
|
||||||
@@ -83,12 +87,14 @@ const ResourceBar = ({
|
|||||||
const { gold, essence, crystals } = resources;
|
const { gold, essence, crystals } = resources;
|
||||||
let partyCombatPower = 0;
|
let partyCombatPower = 0;
|
||||||
let goldPerSecond = 0;
|
let goldPerSecond = 0;
|
||||||
|
let essencePerSecond = 0;
|
||||||
if (state !== null) {
|
if (state !== null) {
|
||||||
for (const adventurer of state.adventurers) {
|
for (const adventurer of state.adventurers) {
|
||||||
const contribution = adventurer.combatPower * adventurer.count;
|
const contribution = adventurer.combatPower * adventurer.count;
|
||||||
partyCombatPower = partyCombatPower + contribution;
|
partyCombatPower = partyCombatPower + contribution;
|
||||||
}
|
}
|
||||||
goldPerSecond = computeGoldPerSecond(state);
|
goldPerSecond = computeGoldPerSecond(state);
|
||||||
|
essencePerSecond = computeEssencePerSecond(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarUrl: string | null = null;
|
let avatarUrl: string | null = null;
|
||||||
@@ -182,6 +188,13 @@ const ResourceBar = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className="resource-label">{"Gold/s"}</span>
|
<span className="resource-label">{"Gold/s"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="resource">
|
||||||
|
<span className="resource-icon">{"⚡"}</span>
|
||||||
|
<span className="resource-value">
|
||||||
|
{formatNumber(essencePerSecond)}
|
||||||
|
</span>
|
||||||
|
<span className="resource-label">{"Essence/s"}</span>
|
||||||
|
</div>
|
||||||
<div className={`resource${essenceFull
|
<div className={`resource${essenceFull
|
||||||
? " resource-full"
|
? " resource-full"
|
||||||
: ""}`}>
|
: ""}`}>
|
||||||
|
|||||||
@@ -1127,7 +1127,7 @@ export const GameProvider = ({
|
|||||||
return adventurer.unlocked && next.resources.gold >= cost;
|
return adventurer.unlocked && next.resources.gold >= cost;
|
||||||
}).
|
}).
|
||||||
sort((adventurerA, adventurerB) => {
|
sort((adventurerA, adventurerB) => {
|
||||||
return adventurerB.combatPower - adventurerA.combatPower;
|
return adventurerB.level - adventurerA.level;
|
||||||
});
|
});
|
||||||
if (bestAdventurer !== undefined) {
|
if (bestAdventurer !== undefined) {
|
||||||
const purchaseCost
|
const purchaseCost
|
||||||
|
|||||||
@@ -195,6 +195,54 @@ export const computeGoldPerSecond = (state: GameState): number => {
|
|||||||
return goldPerSecond;
|
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.
|
* Pure function — applies one game tick to the state.
|
||||||
* DeltaSeconds: time elapsed since last tick.
|
* DeltaSeconds: time elapsed since last tick.
|
||||||
|
|||||||
Reference in New Issue
Block a user