feat: add zone system to bosses and quests

This commit is contained in:
2026-03-06 13:42:40 -08:00
committed by Naomi Carrigan
parent e9e0df31fd
commit 897eba5f64
15 changed files with 239 additions and 4 deletions
+17 -3
View File
@@ -2,6 +2,7 @@ import type { Boss } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { formatNumber } from "../../utils/format.js";
import { ZoneSelector } from "./ZoneSelector.js";
interface BossCardProps {
boss: Boss;
@@ -17,7 +18,7 @@ const BossCard = ({
isChallenging,
}: BossCardProps): React.JSX.Element => {
const hpPercent = (boss.currentHp / boss.maxHp) * 100;
const isLocked = boss.prestigeRequirement > prestigeCount;
const isPrestigeLocked = boss.prestigeRequirement > prestigeCount;
const canChallenge =
(boss.status === "available" || boss.status === "in_progress") && !isChallenging;
@@ -26,7 +27,7 @@ const BossCard = ({
<div className="boss-info">
<h3>{boss.name}</h3>
<p>{boss.description}</p>
{isLocked && boss.status === "locked" && (
{isPrestigeLocked && boss.status === "locked" && (
<p className="prestige-lock">
🔒 Requires Prestige {boss.prestigeRequirement}
</p>
@@ -87,6 +88,7 @@ const BossCard = ({
export const BossPanel = (): React.JSX.Element => {
const { state, challengeBoss } = useGame();
const [challengingBossId, setChallengingBossId] = useState<string | null>(null);
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
if (!state) return <section className="panel"><p>Loading...</p></section>;
@@ -135,10 +137,19 @@ export const BossPanel = (): React.JSX.Element => {
}
};
const zones = state.zones ?? [];
const zoneBosses = state.bosses.filter((b) => b.zoneId === activeZoneId);
return (
<section className="panel boss-panel">
<h2>Boss Encounters</h2>
<ZoneSelector
activeZoneId={activeZoneId}
zones={zones}
onSelectZone={setActiveZoneId}
/>
<div className="party-combat-stats">
<div className="combat-stat">
<span className="stat-label"> Party DPS</span>
@@ -151,7 +162,7 @@ export const BossPanel = (): React.JSX.Element => {
</div>
<div className="boss-list">
{state.bosses.map((boss) => (
{zoneBosses.map((boss) => (
<BossCard
key={boss.id}
boss={boss}
@@ -162,6 +173,9 @@ export const BossPanel = (): React.JSX.Element => {
}}
/>
))}
{zoneBosses.length === 0 && (
<p className="empty-zone">No bosses in this zone yet.</p>
)}
</div>
</section>
);
+17 -1
View File
@@ -1,5 +1,7 @@
import type { Quest } from "@elysium/types";
import { useState } from "react";
import { useGame } from "../../context/GameContext.js";
import { ZoneSelector } from "./ZoneSelector.js";
const formatDuration = (seconds: number): string => {
if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
@@ -62,16 +64,30 @@ const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
export const QuestPanel = (): React.JSX.Element => {
const { state } = useGame();
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
if (!state) return <section className="panel"><p>Loading...</p></section>;
const zones = state.zones ?? [];
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
return (
<section className="panel quest-panel">
<h2>Quests</h2>
<ZoneSelector
activeZoneId={activeZoneId}
zones={zones}
onSelectZone={setActiveZoneId}
/>
<div className="quest-list">
{state.quests.map((quest) => (
{zoneQuests.map((quest) => (
<QuestCard key={quest.id} quest={quest} />
))}
{zoneQuests.length === 0 && (
<p className="empty-zone">No quests in this zone yet.</p>
)}
</div>
</section>
);
@@ -0,0 +1,32 @@
import type { Zone } from "@elysium/types";
interface ZoneSelectorProps {
zones: Zone[];
activeZoneId: string;
onSelectZone: (zoneId: string) => void;
}
export const ZoneSelector = ({
zones,
activeZoneId,
onSelectZone,
}: ZoneSelectorProps): React.JSX.Element => (
<div className="zone-selector">
{zones.map((zone) => (
<button
key={zone.id}
className={`zone-tab ${zone.id === activeZoneId ? "zone-tab-active" : ""} ${zone.status === "locked" ? "zone-tab-locked" : ""}`}
disabled={zone.status === "locked"}
onClick={() => {
onSelectZone(zone.id);
}}
title={zone.status === "locked" ? `Unlock by defeating ${zone.unlockBossId?.replace(/_/g, " ") ?? "the previous boss"}` : zone.description}
type="button"
>
<span className="zone-emoji">{zone.emoji}</span>
<span className="zone-name">{zone.name}</span>
{zone.status === "locked" && <span className="zone-lock">🔒</span>}
</button>
))}
</div>
);