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:
2026-03-25 16:16:21 -07:00
committed by Naomi Carrigan
parent 9926e7f639
commit ad4fcc2811
4 changed files with 155 additions and 9 deletions
+14 -1
View File
@@ -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 = ({
</span>
<span className="resource-label">{"Gold/s"}</span>
</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
? " resource-full"
: ""}`}>
+1 -1
View File
@@ -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
+48
View File
@@ -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.