'use client' import { useCallback, useEffect, useState } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { TeamBuilder } from '@/components/team-builder' import type { CrewState, ExploreResult, SystemState } from '@/lib/game/save' import type { FoundItem } from '@/lib/game/world' // LOCAL SYSTEM tab — the bodies of the current system (planets + staggered moons) with an // EXPLORE button per unexplored row. A button click ARMS the planet (#44): the crew team // builder mounts at the top of the column and its DISPATCH POSTs /api/explore with the team. // The parent (MissionControl) owns the /api/system state so the right column's CARGO HOLD // mirrors scans immediately; this panel reports each scan's response up via onChanged and // keeps only its own UI bits locally (no polling). Turn-based: one scan costs a turn, each // body once. interface LocalSystemPanelProps { system: SystemState | null onChanged: (next: SystemState) => void } export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) { const [lastOutcome, setLastOutcome] = useState(null) const [error, setError] = useState(null) const [exploringId, setExploringId] = useState(null) // #44 — the armed planet (the last-clicked unexplored row); the builder above the list picks // the team and its DISPATCH performs the scan. const [armedPlanetId, setArmedPlanetId] = useState(null) const [crew, setCrew] = useState(null) // JC-14 — the roster + queue the builder mirrors: fetched on mount and after every // successful dispatch (the scan response updates the system state, not the roster). // Keep-old-on-failure, the house .catch(() => {}) pattern. const fetchCrew = useCallback(() => { fetch('/api/crew') .then(async (res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return (await res.json()) as CrewState }) .then((c) => setCrew(c)) .catch(() => {}) }, []) useEffect(() => { fetchCrew() }, [fetchCrew]) const explore = useCallback( async (planetId: number, leadCrewId: number, helperCrewIds: number[]) => { setExploringId(planetId) setArmedPlanetId(null) try { const res = await fetch('/api/explore', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ planetId, leadCrewId, helperCrewIds }), }) const body = (await res.json()) as ExploreResult & { error?: string } if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) onChanged(body) // fresh state straight from the response — parent mirrors it to CARGO HOLD setLastOutcome(body.outcome) fetchCrew() // JC-14 — the dispatch committed a team; refresh the roster + queue for the builder } catch (err: unknown) { setError(err instanceof Error ? err.message : String(err)) } finally { setExploringId(null) } }, [onChanged, fetchCrew] ) if (!system) { return (
SCANNING SYSTEM…
) } const badgeFor = (explored: boolean, hazard: boolean) => explored ? { label: 'SCANNED', variant: 'ACTIVE' as const } : hazard ? { label: 'HAZARD', variant: 'WARNING' as const } : { label: 'UNEXPLORED', variant: 'SCANNING' as const } return (
{/* #44 — the armed planet's crew picker (JC-13: mounted only while a planet is armed); its DISPATCH performs the scan with the picked team. */} {armedPlanetId !== null && crew !== null && ( void explore(armedPlanetId, leadCrewId, helperCrewIds)} /> )}
EXPLORABLE WORLDS // {system.systemName.toUpperCase()} — TURN {system.turn}
{system.planets.map((row, i) => { const depth = row.parent !== null ? 1 : 0 const badge = badgeFor(row.explored, row.hazard) return (
{row.name}
{depth ? `MOON OF ${system.planets.find((p) => p.id === row.parent)?.name ?? '?'} // ${row.type}` : row.type}
{badge.label} {!row.explored && ( )}
) })}
{lastOutcome !== null && (
SCAN RESULT //{' '} {lastOutcome.length > 0 ? lastOutcome.map((i) => `${i.qty}× ${i.name.toUpperCase()}`).join(', ') : 'NOTHING FOUND'}
)} {error && (
SCAN FAILED // {error.toUpperCase()}
)}
) }