generated from nhcarrigan/template
feat: initial prototype — core game systems (#30)
## Summary This PR represents the full v1 prototype, implementing the core game systems for Elysium. - Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests - Adventurer hiring with batch size selector and progressive tier cost scaling - Prestige, transcendence, and apotheosis systems with auto-prestige support - Character sheet, titles, leaderboards, companion system, and daily login bonuses - Auto-quest and auto-boss toggles - Discord webhook notifications on prestige/transcendence/apotheosis - Discord role awarded on apotheosis - Responsive design and overarching story/lore system - In-game sound effects and browser notifications for key events - Support link button in the resource bar - Full test coverage (100% on `apps/api` and `packages/types`) - CI pipeline: lint → build → test ## Closes Closes #1 Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Closes #7 Closes #8 Closes #9 Closes #10 Closes #11 Closes #12 Closes #13 Closes #14 Closes #16 Closes #19 Closes #20 Closes #21 Closes #22 Closes #23 Closes #24 Closes #25 Closes #26 Closes #27 Closes #29 ✨ This issue was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #30 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* @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 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<CodexEntry["sourceType"], string> = {
|
||||
adventurer: "👥",
|
||||
boss: "⚔️",
|
||||
equipment: "🛡️",
|
||||
exploration: "🧭",
|
||||
prestige: "🔮",
|
||||
quest: "📜",
|
||||
recipe: "⚗️",
|
||||
upgrade: "🔧",
|
||||
zone: "🗺️",
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<string | null>(null);
|
||||
|
||||
if (state === null) {
|
||||
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((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 (
|
||||
<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: `${String(Math.round(progressPercent))}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entriesByZone.map(({ zoneId, zoneName, entries, unlockedEntries }) => {
|
||||
return (
|
||||
<div className="codex-zone" key={zoneId}>
|
||||
<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 className="codex-entry locked" key={entry.id}>
|
||||
<div className="codex-entry-header">
|
||||
<span className="codex-lock">{"🔒"}</span>
|
||||
<span className="codex-entry-title">{"???"}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handleExpand(): void {
|
||||
setExpandedId(isExpanded
|
||||
? null
|
||||
: entry.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`codex-entry unlocked ${
|
||||
isExpanded
|
||||
? "expanded"
|
||||
: ""
|
||||
}`}
|
||||
key={entry.id}
|
||||
onClick={handleExpand}
|
||||
>
|
||||
<div className="codex-entry-header">
|
||||
<span className="codex-source-badge">
|
||||
{sourceBadge[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>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { CodexPanel };
|
||||
Reference in New Issue
Block a user