/** * @file Codex panel component displaying discovered lore entries. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex component with zone and entry rendering */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { CODEX_ENTRIES, ZONE_LABELS } from "../../data/codex.js"; import { cdnImage } from "../../utils/cdn.js"; import type { CodexEntry } from "@elysium/types"; /** * Converts a fraction to a percentage value. * @param numerator - The numerator value. * @param denominator - The denominator value. * @returns The percentage as a number between 0 and 100. */ const toPercent = (numerator: number, denominator: number): number => { if (denominator === 0) { return 0; } const scaled = numerator * 100; return scaled / denominator; }; const sourceBadge: Record = { adventurer: "👥", boss: "⚔️", equipment: "🛡️", exploration: "🧭", prestige: "🔮", quest: "📜", recipe: "⚗️", upgrade: "🔧", zone: "🗺️", }; const sourceTypeFolder: Record = { adventurer: "adventurers", boss: "bosses", equipment: "equipment", exploration: "explorations", prestige: "prestige-upgrades", quest: "quests", recipe: "recipes", upgrade: "upgrades", zone: "zones", }; /** * Converts a snake_case ID to a Title Case display name. * @param id - The snake_case identifier to format. * @returns The formatted display name. */ const formatId = (id: string): string => { return id.split("_"). map((word) => { return word.charAt(0).toUpperCase() + word.slice(1); }). join(" "); }; /** * Generates a human-readable unlock hint for a locked codex entry. * @param entry - The locked codex entry. * @returns A string describing how to unlock the entry. */ const buildUnlockHint = (entry: CodexEntry): string => { const name = formatId(entry.sourceId); switch (entry.sourceType) { case "boss": return `Defeat ${name}`; case "quest": return `Complete: ${name}`; case "equipment": return `Obtain: ${name}`; case "adventurer": return `Recruit a ${name}`; case "upgrade": return `Purchase: ${name}`; case "prestige": return `Purchase runestone upgrade: ${name}`; case "zone": return `Explore: ${name}`; case "exploration": return `Discover: ${name}`; case "recipe": return `Craft: ${name}`; default: return "Keep playing to unlock"; } }; /** * Renders the codex panel with lore entries grouped by zone. * @returns The JSX element. */ const CodexPanel = (): JSX.Element => { const { state } = useGame(); const [ expandedId, setExpandedId ] = useState(null); if (state === null) { return (

{"Loading..."}

); } const unlockedIds = new Set(state.codex?.unlockedEntryIds ?? []); const totalEntries = CODEX_ENTRIES.length; const unlockedCount = CODEX_ENTRIES.filter((entry) => { return unlockedIds.has(entry.id); }).length; const progressPercent = toPercent(unlockedCount, totalEntries); const entriesByZone = Object.entries(ZONE_LABELS). map(([ zoneId, zoneName ]) => { const entries = CODEX_ENTRIES.filter((entry) => { return entry.zoneId === zoneId; }); const unlockedEntries = entries.filter((entry) => { return unlockedIds.has(entry.id); }); return { entries, unlockedEntries, zoneId, zoneName, }; }). filter(({ entries }) => { return entries.length > 0; }); return (

{"📖 Codex"}

{"Lore discovered: "} {unlockedCount} {" / "} {totalEntries}

{entriesByZone.map(({ zoneId, zoneName, entries, unlockedEntries }) => { return (

{zoneName} {unlockedEntries.length} {"/"} {entries.length}

{entries.map((entry) => { const isUnlocked = unlockedIds.has(entry.id); const isExpanded = expandedId === entry.id; if (!isUnlocked) { return (
{"🔒"} {"???"}

{buildUnlockHint(entry)}

); } function handleExpand(): void { setExpandedId(isExpanded ? null : entry.id); } return (
{sourceBadge[entry.sourceType]} {entry.title} {isExpanded ? "▲" : "▼"}
{isExpanded ? <> {entry.title}

{entry.content}

: null}
); })}
); })}
); }; export { CodexPanel };