Files
elysium/apps/web/src/components/game/vampireZonesPanel.tsx
T
hikari 3e34701d32 feat: vampire zones, quests, and achievements panels
Implements the three read-only vampire expansion display panels:
- VampireZonesPanel: zone grid with lock state, boss/quest unlock requirements
- VampireQuestsPanel: zone-filtered quest list with duration, rewards, progress badge
- VampireAchievementsPanel: achievement cards with progress bars and ichor/soul shard rewards
Wires all three into GameLayout, replacing the corresponding tab placeholders.
2026-04-16 10:15:03 -07:00

155 lines
4.2 KiB
TypeScript

/**
* @file Vampire Zones panel β€” read-only view of all vampire realms.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Complex panel with zone grid rendering */
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
import { useGame } from "../../context/gameContext.js";
import type { VampireZone } from "@elysium/types";
import type { JSX } from "react";
interface ZoneCardProperties {
readonly zone: VampireZone;
readonly isLocked: boolean;
readonly unlockBossName: string | undefined;
readonly unlockQuestName: string | undefined;
}
/**
* Renders a single vampire zone card.
* @param props - The zone card properties.
* @param props.zone - The zone data.
* @param props.isLocked - Whether this zone is currently locked.
* @param props.unlockBossName - Name of the boss required to unlock, if any.
* @param props.unlockQuestName - Name of the quest required to unlock, if any.
* @returns The JSX element.
*/
const VampireZoneCard = ({
zone,
isLocked,
unlockBossName,
unlockQuestName,
}: ZoneCardProperties): JSX.Element => {
return (
<div className={`zone-card${isLocked
? " locked"
: ""}`}>
<div className="zone-card-header">
<span aria-hidden="true" className="zone-emoji">{zone.emoji}</span>
<h3 className="zone-name">{zone.name}</h3>
{isLocked
? <span aria-label="Locked" className="zone-lock-icon">{"πŸ”’"}</span>
: null}
</div>
<p className="zone-description">{zone.description}</p>
{isLocked
&& (unlockBossName !== undefined || unlockQuestName !== undefined)
? <div className="zone-unlock-requirements">
<p className="zone-unlock-label">{"Unlock requirements:"}</p>
{unlockBossName === undefined
? null
: <p className="zone-unlock-item">
{"🩸 Defeat: "}
{unlockBossName}
</p>
}
{unlockQuestName === undefined
? null
: <p className="zone-unlock-item">
{"πŸ“œ Complete: "}
{unlockQuestName}
</p>
}
</div>
: null}
{isLocked
? null
: <span className="zone-badge unlocked">{"🩸 Unlocked"}</span>
}
</div>
);
};
/**
* Renders the Vampire Zones panel showing all 18 vampire realms.
* @returns The JSX element.
*/
const VampireZonesPanel = (): JSX.Element => {
const { state } = useGame();
if (state === null) {
return (
<section className="panel">
<p>{"Loading..."}</p>
</section>
);
}
const { vampire } = state;
if (vampire === undefined) {
return (
<section className="panel">
<p>{"The Vampire expansion is not yet unlocked."}</p>
</section>
);
}
const { bosses: vampireBosses, quests: vampireQuests, zones } = vampire;
const defeatedBossIds = new Set(
vampireBosses.
filter((boss) => {
return boss.status === "defeated";
}).
map((boss) => {
return boss.id;
}),
);
return (
<section className="panel vampire-zones-panel">
<div className="panel-header">
<h2>{"πŸ—ΊοΈ Vampire Zones"}</h2>
</div>
<div className="zone-grid">
{zones.map((zone) => {
const isLocked = zone.unlockBossId !== null
&& !defeatedBossIds.has(zone.unlockBossId);
const unlockBoss = zone.unlockBossId === null
? undefined
: vampireBosses.find((boss) => {
return boss.id === zone.unlockBossId;
});
const unlockQuest = zone.unlockQuestId === null
? undefined
: vampireQuests.find((quest) => {
return quest.id === zone.unlockQuestId;
});
return (
<VampireZoneCard
isLocked={isLocked}
key={zone.id}
unlockBossName={unlockBoss?.name}
unlockQuestName={unlockQuest?.name}
zone={zone}
/>
);
})}
</div>
</section>
);
};
export { VampireZonesPanel };