generated from nhcarrigan/template
feat: add zone system to bosses and quests
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -1107,6 +1107,73 @@ body {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ββ Zone Selector βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
||||
|
||||
.zone-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.zone-tab {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(147, 51, 234, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--colour-text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
gap: 0.2rem;
|
||||
padding: 0.6rem 1rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.zone-tab:hover:not(:disabled) {
|
||||
background: rgba(147, 51, 234, 0.15);
|
||||
border-color: rgba(147, 51, 234, 0.6);
|
||||
color: var(--colour-text);
|
||||
}
|
||||
|
||||
.zone-tab-active {
|
||||
background: rgba(147, 51, 234, 0.25);
|
||||
border-color: var(--colour-primary);
|
||||
color: var(--colour-text);
|
||||
}
|
||||
|
||||
.zone-tab-locked {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.zone-emoji {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.zone-name {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.zone-lock {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.empty-zone {
|
||||
color: var(--colour-text-muted);
|
||||
font-style: italic;
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ββ Loading / Error screens βββββββββββββββββββββββββββββββββββββββββββββ */
|
||||
|
||||
.loading-screen,
|
||||
.error-screen {
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user