From d723656743a756991d485724b88467f6dab4d354 Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 19 Mar 2026 11:45:36 -0700 Subject: [PATCH] feat: show progress toward unlock conditions on achievement cards (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds a `getCurrentProgress` helper that mirrors the tick engine's achievement-checking logic to compute the player's current progress for each condition type - Locked achievement cards now display a `` bar and a numeric `{current} / {target}` label so players can see exactly how close they are to each achievement - Unlocked achievements are unaffected — no progress bar shown once earned ## Test plan - [ ] Verify locked achievement cards display a progress bar and numeric label - [ ] Verify the progress values match what the tick engine uses for unlock checking - [ ] Verify unlocked achievement cards show no progress bar - [ ] Confirm lint, build, and tests all pass Closes #57 Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/elysium/pulls/71 Co-authored-by: Hikari Co-committed-by: Hikari --- .../src/components/game/achievementPanel.tsx | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/game/achievementPanel.tsx b/apps/web/src/components/game/achievementPanel.tsx index 6bf9cab..8cb5d53 100644 --- a/apps/web/src/components/game/achievementPanel.tsx +++ b/apps/web/src/components/game/achievementPanel.tsx @@ -9,7 +9,7 @@ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { cdnImage } from "../../utils/cdn.js"; import { LockToggle } from "../ui/lockToggle.js"; -import type { Achievement } from "@elysium/types"; +import type { Achievement, GameState } from "@elysium/types"; /** * Returns the plural form of a word based on a count. @@ -54,9 +54,50 @@ const conditionDescription = ( } }; +/** + * Returns the player's current progress value toward an achievement's unlock condition, + * mirroring the logic used by the tick engine's checkAchievements function. + * @param achievement - The achievement to evaluate progress for. + * @param state - The current game state. + * @returns The current numeric progress toward the achievement condition. + */ +const getCurrentProgress = ( + achievement: Achievement, + state: GameState, +): number => { + const { condition } = achievement; + switch (condition.type) { + case "totalGoldEarned": + return state.player.totalGoldEarned; + case "totalClicks": + return state.player.totalClicks; + case "bossesDefeated": + return state.bosses.filter((boss) => { + return boss.status === "defeated"; + }).length; + case "questsCompleted": + return state.quests.filter((quest) => { + return quest.status === "completed"; + }).length; + case "adventurerTotal": + return state.adventurers.reduce((sum, adventurer) => { + return sum + adventurer.count; + }, 0); + case "prestigeCount": + return state.prestige.count; + case "equipmentOwned": + return state.equipment.filter((item) => { + return item.owned; + }).length; + default: + return 0; + } +}; + interface AchievementCardProperties { - readonly achievement: Achievement; - readonly formatNumber: (n: number)=> string; + readonly achievement: Achievement; + readonly formatNumber: (n: number)=> string; + readonly progressValue: number; } /** @@ -64,14 +105,18 @@ interface AchievementCardProperties { * @param props - The achievement card properties. * @param props.achievement - The achievement to display. * @param props.formatNumber - The number formatting utility function. + * @param props.progressValue - The player's current progress toward the unlock condition. * @returns The JSX element. */ +// eslint-disable-next-line max-lines-per-function -- Progress bar adds necessary lines for locked state const AchievementCard = ({ achievement, formatNumber, + progressValue, }: AchievementCardProperties): JSX.Element => { const isUnlocked = achievement.unlockedAt !== null; const crystals = achievement.reward?.crystals; + const cappedProgress = Math.min(progressValue, achievement.condition.amount); return (
{conditionDescription(achievement, formatNumber)}

+ {!isUnlocked + &&
+ + + {formatNumber(progressValue)} + {" / "} + {formatNumber(achievement.condition.amount)} + +
+ } {crystals !== undefined &&

{"💎 +"} @@ -163,6 +221,7 @@ const AchievementPanel = (): JSX.Element => { achievement={achievement} formatNumber={formatNumber} key={achievement.id} + progressValue={getCurrentProgress(achievement, state)} /> ); })}