generated from nhcarrigan/template
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.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @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 };
|
||||
Reference in New Issue
Block a user