feat(ui): team builder — lead + helpers with live duration preview, wired into the action panels (#44)
This commit is contained in:
+47
-11
@@ -1,16 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useState } from 'react'
|
||||
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 { 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. 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.
|
||||
// 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
|
||||
@@ -21,27 +24,50 @@ export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) {
|
||||
const [lastOutcome, setLastOutcome] = useState<FoundItem[] | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [exploringId, setExploringId] = useState<number | null>(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<number | null>(null)
|
||||
const [crew, setCrew] = useState<CrewState | null>(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) => {
|
||||
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 }),
|
||||
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]
|
||||
[onChanged, fetchCrew]
|
||||
)
|
||||
|
||||
if (!system) {
|
||||
@@ -57,6 +83,16 @@ export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{/* #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 && (
|
||||
<TeamBuilder
|
||||
type="EXPLORE"
|
||||
roster={crew.crew}
|
||||
tasks={crew.tasks}
|
||||
onQueue={(leadCrewId, helperCrewIds) => void explore(armedPlanetId, leadCrewId, helperCrewIds)}
|
||||
/>
|
||||
)}
|
||||
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
@@ -108,9 +144,9 @@ export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) {
|
||||
variant="OUTLINE"
|
||||
size="SM"
|
||||
disabled={exploringId !== null}
|
||||
onClick={() => void explore(row.id)}
|
||||
onClick={() => setArmedPlanetId(armedPlanetId === row.id ? null : row.id)} // #44 — arms/disarms; the builder dispatches
|
||||
>
|
||||
{exploringId === row.id ? 'SCANNING…' : `${row.name.toUpperCase()} // EXPLORE`}
|
||||
{exploringId === row.id ? 'SCANNING…' : `${row.name.toUpperCase()} // ${armedPlanetId === row.id ? 'PICK TEAM' : 'EXPLORE'}`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user