diff --git a/components/local-system.tsx b/components/local-system.tsx new file mode 100644 index 0000000..d1c413a --- /dev/null +++ b/components/local-system.tsx @@ -0,0 +1,187 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import type { 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. Fetches /api/system on mount; explore responses are +// applied directly (no polling). Turn-based: one scan costs a turn, each body once. + +export function LocalSystemPanel() { + const [state, setState] = useState(null) + const [lastOutcome, setLastOutcome] = useState(null) + const [error, setError] = useState(null) + const [exploringId, setExploringId] = useState(null) + + useEffect(() => { + let cancelled = false + fetch('/api/system') + .then(async (res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as SystemState + }) + .then((s) => { + if (!cancelled) setState(s) + }) + .catch((err: unknown) => { + if (!cancelled) setError(err instanceof Error ? err.message : String(err)) + }) + return () => { + cancelled = true + } + }, []) + + const explore = useCallback(async (planetId: number) => { + setExploringId(planetId) + try { + const res = await fetch('/api/explore', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ planetId }), + }) + const body = (await res.json()) as ExploreResult & { error?: string } + if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) + setState(body) // fresh state straight from the response — no refetch needed + setLastOutcome(body.outcome) + } catch (err: unknown) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setExploringId(null) + } + }, []) + + if (error && !state) { + return ( +
+ + SYSTEM SCAN OFFLINE // {error.toUpperCase()} + +
+ ) + } + + if (!state) { + 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 ( +
+
+
+ EXPLORABLE WORLDS // {state.systemName.toUpperCase()} — TURN {state.turn} +
+ {state.planets.map((row, i) => { + const depth = row.parent !== null ? 1 : 0 + const badge = badgeFor(row.explored, row.hazard) + return ( +
+
+
+ {row.name} +
+
+ {depth ? `MOON OF ${state.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'} + +
+ )} + +
+
+ CARGO HOLD // INVENTORY +
+ {state.inventory.length === 0 ? ( +
+ EMPTY — EXPLORE WORLDS TO FIND THINGS +
+ ) : ( + state.inventory.map((item) => ( +
+ + {item.name} // {item.kind} + + ×{item.qty} +
+ )) + )} +
+ + {error && ( +
+ SCAN FAILED // {error.toUpperCase()} +
+ )} +
+ ) +} diff --git a/components/mission-control.tsx b/components/mission-control.tsx index 605cc5a..55d8f34 100644 --- a/components/mission-control.tsx +++ b/components/mission-control.tsx @@ -11,12 +11,13 @@ import { StatCard } from '@/components/ui/stat-card' import { StatusGrid, type SystemEntry } from '@/components/ui/status-grid' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Terminal, type TerminalLine } from '@/components/ui/terminal' +import { LocalSystemPanel } from '@/components/local-system' import { WorldMapPanel } from '@/components/world-map' // Interface base: layout + visual treatment rebuilt from the scificn-ui Mission Control // showcase (src/app/_pages/showcase/mission-control-content.tsx in baxy5/scificn-ui). The -// WORLD MAP tab renders live data from /api/world; the other tabs keep placeholder content -// until their own issues land. Single player, no auth, by design. +// WORLD MAP and LOCAL SYSTEM tabs render live data (/api/world, /api/system); the other +// tabs keep placeholder content until their own issues land. Single player, no auth, by design. function useNarrow(threshold = 900): boolean { const [narrow, setNarrow] = useState( @@ -44,20 +45,6 @@ const shipSystems: SystemEntry[] = [ { name: 'COMMS', status: 'ACTIVE' }, ] -// LOCAL SYSTEM — explorable worlds of the current system (regenerated per save after each jump, later). -const explorablePlanets = [ - { - name: 'AURELLE', type: 'TERRESTRIAL // OCEAN CONTINENTS', state: 'UNEXPLORED' as const, - moons: [{ name: 'PALEMOON', type: 'ICE MOON // FROST PLAINS', state: 'UNEXPLORED' as const }], - }, - { - name: 'BRIMSTONE', type: 'GAS GIANT // CLOUD DECKS', state: 'SCANNED' as const, - moons: [{ name: 'ASHFALL', type: 'VOLCANIC MOON // SULFUR VEINS', state: 'UNEXPLORED' as const }], - }, - { name: 'CINDERROCK', type: 'ROCKY // IRON RICH', state: 'UNEXPLORED' as const, moons: [] }, - { name: 'DRIFTBAND', type: 'ASTEROID FIELD // HAZARD', state: 'HAZARD' as const, moons: [] }, -] - const crew = [ { name: 'CMDR. E. VANCE', role: 'COMMANDER', good: 'STEADY UNDER PRESSURE', bad: 'RIGID — SLOW TO ADAPT' }, { name: 'LT. M. OKONKWO', role: 'PILOT', good: 'RECKLESS COURAGE', bad: 'SHORT FUSE' }, @@ -339,87 +326,9 @@ export function MissionControl() { - {/* LOCAL SYSTEM TAB */} + {/* LOCAL SYSTEM TAB — live data from /api/system */} -
-
- EXPLORABLE WORLDS // FIND THINGS HERE (PLACEHOLDER) -
- {explorablePlanets.flatMap((planet) => [ - { name: planet.name, type: planet.type, state: planet.state, depth: 0 }, - ...planet.moons.map((moon) => ({ - name: moon.name, - type: moon.type, - state: moon.state, - depth: 1, - })), - ]).map((row, i, rows) => ( -
-
-
- {row.name} -
-
- {row.type} -
-
- - {row.state} - -
- ))} -
+
{/* CREW TAB */}