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)} /> ); })}