generated from nhcarrigan/template
fd286cd29f
Implements a full in-game Codex panel that tracks lore discovery across seven categories: bosses (72), quests (95), zones (18), equipment (65), adventurer tiers (32), upgrades (57), and prestige upgrades (25). Lore entries unlock automatically as players progress — existing completions are retroactively and silently added on first load. New discoveries trigger a toast notification and badge counter on the Codex tab.
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import { useState } from "react";
|
|
import type { CodexEntry } from "@elysium/types";
|
|
import { CODEX_ENTRIES, ZONE_LABELS } from "../../data/codex.js";
|
|
import { useGame } from "../../context/GameContext.js";
|
|
|
|
const SOURCE_BADGE: Record<CodexEntry["sourceType"], string> = {
|
|
boss: "⚔️",
|
|
quest: "📜",
|
|
equipment: "🛡️",
|
|
adventurer: "👥",
|
|
upgrade: "🔧",
|
|
prestige: "🔮",
|
|
zone: "🗺️",
|
|
};
|
|
|
|
export const CodexPanel = (): React.JSX.Element => {
|
|
const { state } = useGame();
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
|
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
|
|
|
const unlockedIds = new Set(state.codex?.unlockedEntryIds ?? []);
|
|
const totalEntries = CODEX_ENTRIES.length;
|
|
const unlockedCount = CODEX_ENTRIES.filter((e) => unlockedIds.has(e.id)).length;
|
|
const progressPercent = totalEntries > 0 ? (unlockedCount / totalEntries) * 100 : 0;
|
|
|
|
const entriesByZone = Object.entries(ZONE_LABELS).map(([zoneId, zoneName]) => {
|
|
const zoneEntries = CODEX_ENTRIES.filter((e) => e.zoneId === zoneId);
|
|
const unlockedZoneEntries = zoneEntries.filter((e) => unlockedIds.has(e.id));
|
|
return { zoneId, zoneName, entries: zoneEntries, unlockedEntries: unlockedZoneEntries };
|
|
}).filter(({ entries }) => entries.length > 0);
|
|
|
|
return (
|
|
<section className="panel codex-panel">
|
|
<h2>📖 Codex</h2>
|
|
|
|
<div className="codex-progress">
|
|
<p className="codex-progress-text">
|
|
Lore discovered: <strong>{unlockedCount} / {totalEntries}</strong>
|
|
</p>
|
|
<div className="codex-progress-bar">
|
|
<div className="codex-progress-fill" style={{ width: `${progressPercent}%` }} />
|
|
</div>
|
|
</div>
|
|
|
|
{entriesByZone.map(({ zoneId, zoneName, entries, unlockedEntries }) => (
|
|
<div key={zoneId} className="codex-zone">
|
|
<h3 className="codex-zone-header">
|
|
{zoneName}
|
|
<span className="codex-zone-count">
|
|
{unlockedEntries.length}/{entries.length}
|
|
</span>
|
|
</h3>
|
|
|
|
<div className="codex-entries">
|
|
{entries.map((entry) => {
|
|
const isUnlocked = unlockedIds.has(entry.id);
|
|
const isExpanded = expandedId === entry.id;
|
|
|
|
if (!isUnlocked) {
|
|
return (
|
|
<div key={entry.id} className="codex-entry locked">
|
|
<div className="codex-entry-header">
|
|
<span className="codex-lock">🔒</span>
|
|
<span className="codex-entry-title">???</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={entry.id}
|
|
className={`codex-entry unlocked ${isExpanded ? "expanded" : ""}`}
|
|
onClick={() => { setExpandedId(isExpanded ? null : entry.id); }}
|
|
>
|
|
<div className="codex-entry-header">
|
|
<span className="codex-source-badge">
|
|
{SOURCE_BADGE[entry.sourceType]}
|
|
</span>
|
|
<span className="codex-entry-title">{entry.title}</span>
|
|
<span className="codex-chevron">{isExpanded ? "▲" : "▼"}</span>
|
|
</div>
|
|
{isExpanded && (
|
|
<p className="codex-entry-content">{entry.content}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</section>
|
|
);
|
|
};
|