/** * @file Exploration panel component for exploring areas and collecting materials. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable complexity -- Complex component with many conditional render paths */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { EXPLORATION_AREAS } from "../../data/explorations.js"; import { cdnImage } from "../../utils/cdn.js"; import { ZoneSelector } from "./zoneSelector.js"; import type { ExploreCollectResponse } from "@elysium/types"; /** * Formats a duration in seconds to a human-readable string. * @param seconds - The total number of seconds to format. * @returns The formatted duration string. */ const formatDuration = (seconds: number): string => { const secondsPerDay = 86_400; const secondsPerHour = 3600; const secondsPerMinute = 60; if (seconds >= secondsPerDay) { const days = Math.floor(seconds / secondsPerDay); const remainingAfterDays = seconds % secondsPerDay; const hours = Math.floor(remainingAfterDays / secondsPerHour); return hours > 0 ? `${String(days)}d ${String(hours)}h` : `${String(days)}d`; } if (seconds >= secondsPerHour) { const hours = Math.floor(seconds / secondsPerHour); const remainingAfterHours = seconds % secondsPerHour; const minutes = Math.floor(remainingAfterHours / secondsPerMinute); return `${String(hours)}h ${String(minutes)}m`; } if (seconds >= secondsPerMinute) { const minutes = Math.floor(seconds / secondsPerMinute); const secs = seconds % secondsPerMinute; return `${String(minutes)}m ${String(secs)}s`; } return `${String(seconds)}s`; }; /** * Computes the time remaining for an exploration in progress. * @param startedAt - The timestamp when exploration started. * @param durationSeconds - The total duration in seconds. * @returns The remaining seconds. */ const timeRemaining = (startedAt: number, durationSeconds: number): number => { const elapsed = (Date.now() - startedAt) / 1000; return Math.max(0, durationSeconds - elapsed); }; interface CollectResult { areaId: string; response: ExploreCollectResponse; } /** * Renders the exploration panel for managing area explorations. * @returns The JSX element. */ const ExplorationPanel = (): JSX.Element => { const { state, startExploration, collectExploration, formatNumber } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState("verdant_vale"); const [ pendingAreaId, setPendingAreaId ] = useState(null); const [ lastResult, setLastResult ] = useState(null); if (state === null) { return (

{"Loading..."}

); } const { zones, exploration: explorationState } = state; const zoneAreas = EXPLORATION_AREAS.filter((area) => { return area.zoneId === activeZoneId; }); const hasActiveExploration = explorationState?.areas.some((area) => { return area.status === "in_progress"; }) ?? false; async function handleStart(areaId: string): Promise { setPendingAreaId(areaId); try { await startExploration(areaId); } finally { setPendingAreaId(null); } } async function handleCollect(areaId: string): Promise { setPendingAreaId(areaId); try { const result = await collectExploration(areaId); setLastResult({ areaId: areaId, response: result }); } finally { setPendingAreaId(null); } } function handleDismissResult(): void { setLastResult(null); } function handleZoneSelect(id: string): void { setActiveZoneId(id); setLastResult(null); } const goldChange = lastResult?.response.event?.goldChange ?? 0; const essenceChange = lastResult?.response.event?.essenceChange ?? 0; return (

{"πŸ—ΊοΈ Exploration"}

{lastResult === null ? null :
{lastResult.response.foundNothing ?

{lastResult.response.nothingMessage}

: <> {lastResult.response.event === null ? null :

{lastResult.response.event.text}

}
{goldChange !== 0 && 0 ? "" : "negative"}`} > {"πŸͺ™ "} {goldChange > 0 ? "+" : ""} {formatNumber(goldChange)} {" gold"} } {essenceChange > 0 && {"✨ +"} {formatNumber(essenceChange)} {" essence"} } {lastResult.response.event?.materialGained !== null && lastResult.response.event?.materialGained !== undefined ? {"πŸ“¦ +"} {lastResult.response.event.materialGained.quantity}{" "} {/* eslint-disable-next-line stylistic/max-len -- long property chain cannot be shortened */} {lastResult.response.event.materialGained.materialId.replaceAll( "_", " ", )} {" (event)"} : null} {lastResult.response.materialsFound.map((foundMaterial) => { return ( {"πŸ“¦ +"} {foundMaterial.quantity}{" "} {foundMaterial.materialId.replaceAll("_", " ")} ); })}
}
}
{zoneAreas.map((area) => { const areaState = explorationState?.areas.find((explorationArea) => { return explorationArea.id === area.id; }); const status = areaState?.status ?? "locked"; const startedAt = areaState?.startedAt ?? 0; const isReady = status === "in_progress" && timeRemaining(startedAt, area.durationSeconds) <= 0; const isPending = pendingAreaId === area.id; function handleStartClick(): void { void handleStart(area.id); } function handleCollectClick(): void { void handleCollect(area.id); } return (
{area.name}

{area.name} {areaState?.completedOnce === true ? {" πŸ“–"} : null}

{area.description}

{"⏱️ "} {formatDuration(area.durationSeconds)}
{status === "locked" && {"πŸ”’ Locked"} } {status === "available" && } {status === "in_progress" && !isReady && {"⏳ "} {formatDuration( Math.ceil(timeRemaining(startedAt, area.durationSeconds)), )} {" remaining"} } {status === "in_progress" && isReady ? : null}
); })} {zoneAreas.length === 0 &&

{"No exploration areas in this zone."}

}
); }; export { ExplorationPanel };