3 Commits

Author SHA1 Message Date
hikari 35e4d71d98 fix: preserve boss first-kill state across prestige
CI / Lint, Build & Test (pull_request) Successful in 1m10s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m11s
Fixes #39. Added bountyRunestonesClaimed?: boolean to the Boss type.
The first-kill bounty runestones are now only awarded once across all
prestige resets — the boss route checks the flag before awarding, sets
it on first defeat, and buildPostPrestigeState carries the flag forward
through fresh boss state on prestige. The boss panel badge no longer
shows for bosses whose bounty has already been claimed.
2026-03-09 21:35:03 -07:00
hikari f2d82d58fc fix: preserve achievements across prestige
Fixes #38. buildPostPrestigeState was using structuredClone(defaultAchievements)
via the freshState, which reset all achievements on every prestige. Achievements
are now carried forward from currentState.achievements instead, ensuring unlocked
achievements are never lost across prestige resets.
2026-03-09 21:31:06 -07:00
hikari 062e5b59a6 fix: preserve lifetime player stats across prestige
Fixes #37. After prestige, the GameState's player.lifetime* fields were
stale — they did not include the current run's contributions. The Prisma
Player record was updated correctly, but the saved GameState had old values,
so the UI showed stale all-time totals on reload.

buildPostPrestigeState now computes run-stat contributions and folds them
into the fresh player object before writing the prestige state, ensuring
the GameState is always consistent with the DB Player record.
2026-03-09 21:28:16 -07:00
6 changed files with 11 additions and 63 deletions
@@ -16,31 +16,6 @@ import type { Adventurer } from "@elysium/types";
type BatchSize = 1 | 5 | 10 | 25 | 100 | "max";
const batchOptions: Array<BatchSize> = [ 1, 5, 10, 25, 100, "max" ];
/**
* Parses a localStorage string back into a valid BatchSize, defaulting to 1.
* @param stored - The raw string from localStorage (or null if absent).
* @returns A valid BatchSize value.
*/
const parseBatchSize = (stored: string | null): BatchSize => {
if (stored === "max") {
return "max";
}
const numeric = Number(stored);
if (numeric === 5) {
return 5;
}
if (numeric === 10) {
return 10;
}
if (numeric === 25) {
return 25;
}
if (numeric === 100) {
return 100;
}
return 1;
};
/**
* Computes the total cost to buy a batch of adventurers.
* @param adventurer - The adventurer to buy.
@@ -173,9 +148,7 @@ const AdventurerCard = ({
const AdventurerPanel = (): JSX.Element => {
const { state, formatNumber } = useGame();
const [ showLocked, setShowLocked ] = useState(true);
const [ batchSize, setBatchSize ] = useState<BatchSize>(() => {
return parseBatchSize(localStorage.getItem("elysium_batch_size"));
});
const [ batchSize, setBatchSize ] = useState<BatchSize>(1);
if (state === null) {
return (
@@ -223,7 +196,6 @@ const AdventurerPanel = (): JSX.Element => {
{batchOptions.map((option) => {
function handleBatchSelect(): void {
setBatchSize(option);
localStorage.setItem("elysium_batch_size", String(option));
}
return (
<button
+2 -9
View File
@@ -239,9 +239,7 @@ const BossPanel = (): JSX.Element => {
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
null,
);
const [ activeZoneId, setActiveZoneId ] = useState(() => {
return sessionStorage.getItem("elysium_boss_zone") ?? "verdant_vale";
});
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
const [ showLocked, setShowLocked ] = useState(true);
if (state === null) {
@@ -319,11 +317,6 @@ const BossPanel = (): JSX.Element => {
}
}
function handleZoneSelect(zoneId: string): void {
setActiveZoneId(zoneId);
sessionStorage.setItem("elysium_boss_zone", zoneId);
}
function handleToggle(): void {
setShowLocked((current) => {
return !current;
@@ -381,7 +374,7 @@ const BossPanel = (): JSX.Element => {
<ZoneSelector
activeZoneId={activeZoneId}
onSelectZone={handleZoneSelect}
onSelectZone={setActiveZoneId}
zones={zones}
/>
@@ -243,7 +243,7 @@ const CharacterPage = ({ discordId }: CharacterPageProperties): JSX.Element => {
return (
<div
className="character-page-equipment-item"
key={item.name}
key={item.type}
>
<div className="character-page-equipment-header">
<span className="character-page-equipment-slot">
@@ -26,9 +26,7 @@ const bonusLabel: Record<string, string> = {
*/
const CraftingPanel = (): JSX.Element => {
const { state, craftRecipe, formatNumber } = useGame();
const [ activeZoneId, setActiveZoneId ] = useState(() => {
return sessionStorage.getItem("elysium_craft_zone") ?? "verdant_vale";
});
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
const [ pendingRecipeId, setPendingRecipeId ] = useState<string | null>(null);
if (state === null) {
@@ -70,11 +68,6 @@ const CraftingPanel = (): JSX.Element => {
});
}
function handleZoneSelect(zoneId: string): void {
setActiveZoneId(zoneId);
sessionStorage.setItem("elysium_craft_zone", zoneId);
}
async function handleCraft(recipeId: string): Promise<void> {
setPendingRecipeId(recipeId);
try {
@@ -92,7 +85,7 @@ const CraftingPanel = (): JSX.Element => {
<ZoneSelector
activeZoneId={activeZoneId}
onSelectZone={handleZoneSelect}
onSelectZone={setActiveZoneId}
zones={zones}
/>
@@ -67,9 +67,7 @@ interface CollectResult {
const ExplorationPanel = (): JSX.Element => {
const { state, startExploration, collectExploration, formatNumber }
= useGame();
const [ activeZoneId, setActiveZoneId ] = useState(() => {
return sessionStorage.getItem("elysium_explore_zone") ?? "verdant_vale";
});
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
const [ pendingAreaId, setPendingAreaId ] = useState<string | null>(null);
const [ lastResult, setLastResult ] = useState<CollectResult | null>(null);
@@ -118,7 +116,6 @@ const ExplorationPanel = (): JSX.Element => {
function handleZoneSelect(id: string): void {
setActiveZoneId(id);
setLastResult(null);
sessionStorage.setItem("elysium_explore_zone", id);
}
const goldChange = lastResult?.response.event?.goldChange ?? 0;
+4 -11
View File
@@ -108,9 +108,9 @@ const QuestCard = ({
</p>
}
<div className="quest-rewards">
{quest.rewards.map((reward, rewardIndex) => {
{quest.rewards.map((reward) => {
return (
<span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}>
<span className="reward-tag" key={`${reward.type}-${String(reward.amount ?? "")}`}>
{reward.type === "gold"
&& `🪙 ${formatNumber(reward.amount ?? 0)}`}
{reward.type === "essence"
@@ -184,9 +184,7 @@ const QuestCard = ({
*/
const QuestPanel = (): JSX.Element => {
const { state, toggleAutoQuest } = useGame();
const [ activeZoneId, setActiveZoneId ] = useState(() => {
return sessionStorage.getItem("elysium_quest_zone") ?? "verdant_vale";
});
const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale");
const [ showLocked, setShowLocked ] = useState(true);
if (state === null) {
@@ -245,11 +243,6 @@ const QuestPanel = (): JSX.Element => {
}
}
function handleZoneSelect(zoneId: string): void {
setActiveZoneId(zoneId);
sessionStorage.setItem("elysium_quest_zone", zoneId);
}
function handleToggle(): void {
setShowLocked((current) => {
return !current;
@@ -292,7 +285,7 @@ const QuestPanel = (): JSX.Element => {
<ZoneSelector
activeZoneId={activeZoneId}
onSelectZone={handleZoneSelect}
onSelectZone={setActiveZoneId}
zones={zones}
/>