generated from nhcarrigan/template
108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
import type { Quest } from "@elysium/types";
|
|
import { useState } from "react";
|
|
import { useGame } from "../../context/GameContext.js";
|
|
import { LockToggle } from "../ui/LockToggle.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`;
|
|
if (seconds >= 60) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
return `${seconds}s`;
|
|
};
|
|
|
|
const questTimeRemaining = (quest: Quest): number => {
|
|
if (quest.status !== "active" || quest.startedAt == null) return 0;
|
|
const elapsed = (Date.now() - quest.startedAt) / 1000;
|
|
return Math.max(0, quest.durationSeconds - elapsed);
|
|
};
|
|
|
|
interface QuestCardProps {
|
|
quest: Quest;
|
|
}
|
|
|
|
const QuestCard = ({ quest }: QuestCardProps): React.JSX.Element => {
|
|
const { startQuest } = useGame();
|
|
|
|
return (
|
|
<div className={`quest-card quest-${quest.status}`}>
|
|
<div className="quest-info">
|
|
<h3>{quest.name}</h3>
|
|
<p>{quest.description}</p>
|
|
<div className="quest-rewards">
|
|
{quest.rewards.map((reward, index) => (
|
|
// eslint-disable-next-line react/no-array-index-key -- rewards have no unique id
|
|
<span key={index} className="reward-tag">
|
|
{reward.type === "gold" && `🪙 ${reward.amount?.toLocaleString()}`}
|
|
{reward.type === "essence" && `✨ ${reward.amount?.toLocaleString()}`}
|
|
{reward.type === "crystals" && `💎 ${reward.amount?.toLocaleString()}`}
|
|
{reward.type === "upgrade" && "🔓 Upgrade"}
|
|
{reward.type === "adventurer" && "👥 New Adventurer"}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="quest-action">
|
|
{quest.status === "locked" && <span className="quest-badge locked">🔒 Locked</span>}
|
|
{quest.status === "available" && (
|
|
<button
|
|
className="start-quest-button"
|
|
onClick={() => { startQuest(quest.id); }}
|
|
type="button"
|
|
>
|
|
Send Party ({formatDuration(quest.durationSeconds)})
|
|
</button>
|
|
)}
|
|
{quest.status === "active" && (
|
|
<span className="quest-badge active">
|
|
⏳ {formatDuration(Math.ceil(questTimeRemaining(quest)))} remaining
|
|
</span>
|
|
)}
|
|
{quest.status === "completed" && <span className="quest-badge completed">✅ Complete</span>}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const QuestPanel = (): React.JSX.Element => {
|
|
const { state } = useGame();
|
|
const [activeZoneId, setActiveZoneId] = useState("verdant_vale");
|
|
const [showLocked, setShowLocked] = useState(true);
|
|
|
|
if (!state) return <section className="panel"><p>Loading...</p></section>;
|
|
|
|
const zones = state.zones ?? [];
|
|
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
|
|
const lockedCount = zoneQuests.filter((q) => q.status === "locked").length;
|
|
const visibleQuests = showLocked
|
|
? zoneQuests
|
|
: zoneQuests.filter((q) => q.status !== "locked");
|
|
|
|
return (
|
|
<section className="panel quest-panel">
|
|
<div className="panel-header">
|
|
<h2>Quests</h2>
|
|
<LockToggle
|
|
lockedCount={lockedCount}
|
|
showLocked={showLocked}
|
|
onToggle={() => { setShowLocked((v) => !v); }}
|
|
/>
|
|
</div>
|
|
|
|
<ZoneSelector
|
|
activeZoneId={activeZoneId}
|
|
zones={zones}
|
|
onSelectZone={setActiveZoneId}
|
|
/>
|
|
|
|
<div className="quest-list">
|
|
{visibleQuests.map((quest) => (
|
|
<QuestCard key={quest.id} quest={quest} />
|
|
))}
|
|
{visibleQuests.length === 0 && (
|
|
<p className="empty-zone">No quests to show in this zone.</p>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|