'use client' import { useCallback, useEffect, useState } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { TeamBuilder } from '@/components/team-builder' import { BUILD_PART_COST, expandCost, totalQty, upgradeCost } from '@/lib/game/ship-costs' import type { ShipState, ShipSystemDto } from '@/lib/game/ship' import type { CrewState } from '@/lib/game/save' import { buildGate, effectiveCapacityCap, effectiveLevelCap } from '@/lib/game/tech' import type { TaskType } from '@/lib/game/tasks' // SHIP SYSTEMS tab — live data. The parent (MissionControl) owns the ship state so the left // column mirrors actions immediately; this panel applies each action's response directly via // onChanged (no polling). A card's UPGRADE / EXPAND / BUILD button ARMS the action (#44): the // crew team builder mounts at the top of the column and its DISPATCH POSTs the ship route with // the team. Buttons are disabled when unaffordable, capped or gated by an // unresearched tech, using the same pure helpers the API enforces (lib/game/ship-costs.ts + // lib/game/tech.ts). interface ShipSystemsPanelProps { ship: ShipState | null narrow?: boolean onChanged: (next: ShipState) => void researchedSlugs: Set // slugs of researched techs — raises caps, lifts build gates } export function ShipSystemsPanel({ ship, narrow, onChanged, researchedSlugs }: ShipSystemsPanelProps) { const [error, setError] = useState(null) const [busy, setBusy] = useState(false) // one action at a time — turn-based game // #44 — the armed ship action (which card + which of UPGRADE/EXPAND/BUILD); the builder above // the grid picks the team and its DISPATCH performs it. const [armed, setArmed] = useState<{ type: TaskType; url: string; body: Record; systemId: number } | null>(null) const [crew, setCrew] = useState(null) // JC-14 — the roster + queue the builder mirrors: fetched on mount and after every // successful dispatch (the action response updates the ship 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]) // #44 — arm an action (toggle: clicking the armed action's button again disarms it). const arm = useCallback((type: TaskType, url: string, body: Record, systemId: number) => { setArmed((a) => (a && a.systemId === systemId && a.type === type ? null : { type, url, body, systemId })) }, []) if (!ship) { return (
LOADING SHIP SYSTEMS…
) } const resources = totalQty(ship.inventory, 'resource') const parts = totalQty(ship.inventory, 'part') async function act(url: string, body: unknown) { setBusy(true) setError(null) setArmed(null) try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const data = (await res.json()) as ShipState & { error?: string } if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`) onChanged(data) // response is the fresh ship state — apply directly 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 { setBusy(false) } } return (
{/* #44 — the armed ship action's crew picker (JC-13: mounted only while an action is armed); its DISPATCH performs it with the picked team. */} {armed !== null && crew !== null && ( void act(armed.url, { ...armed.body, leadCrewId, helperCrewIds })} /> )} {error && (
{error.toUpperCase()}
)}
{ship.systems.map((system) => ( ))}
) } function SystemCard({ system, resources, parts, busy, researchedSlugs, armedType, onArm, }: { system: ShipSystemDto resources: number parts: number busy: boolean researchedSlugs: Set armedType: TaskType | null // #44 — which of this card's actions is the armed target onArm: (type: TaskType, url: string, body: Record, systemId: number) => void }) { const maxLevel = effectiveLevelCap(system.type, researchedSlugs) const maxCapacity = effectiveCapacityCap(system.type, researchedSlugs) const gate = buildGate(system.type, researchedSlugs) // name of the required tech, or null when ungated/researched const upgradeDisabled = busy || !system.built || system.level >= maxLevel || resources < upgradeCost(system.level) const expandDisabled = busy || !system.built || system.capacity >= maxCapacity || resources < expandCost(system.capacity) const buildDisabled = busy || system.built || gate !== null || parts < BUILD_PART_COST return (
{system.type} {system.built ? `LV ${system.level}` : 'OFFLINE'}

{system.built ? `LEVEL ${system.level} · CAPACITY ${system.capacity}` : 'NOT INSTALLED'}

{system.built ? `UPGRADE ${upgradeCost(system.level)}× RES · EXPAND ${expandCost(system.capacity)}× RES` : `BUILD ${BUILD_PART_COST}× PARTS`}

{!system.built && gate !== null && (

REQUIRES // {gate}

)}
{system.built ? ( <> ) : ( )}
) }