diff --git a/components/mission-control.tsx b/components/mission-control.tsx index f67cb28..d3e67ef 100644 --- a/components/mission-control.tsx +++ b/components/mission-control.tsx @@ -13,12 +13,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Terminal, type TerminalLine } from '@/components/ui/terminal' import { CrewPanel } from '@/components/crew-panel' import { LocalSystemPanel } from '@/components/local-system' +import { ShipSystemsPanel } from '@/components/ship-systems' import { WorldMapPanel } from '@/components/world-map' +import { CREW_CAPACITY } from '@/lib/game/ship-costs' +import type { CrewDto } from '@/lib/game/save' +import type { ShipState } from '@/lib/game/ship' // 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, LOCAL SYSTEM and CREW tabs render live data (/api/world, /api/system, /api/crew); -// the other tabs keep placeholder content until their own issues land. Single player, no auth, by design. +// WORLD MAP, LOCAL SYSTEM, CREW and SHIP SYSTEMS tabs render live data (/api/world, /api/system, +// /api/crew, /api/ship); 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( @@ -38,22 +43,6 @@ function now(): string { // ── Placeholder content (no game logic — real values land with the turn engine) ── -const shipSystems: SystemEntry[] = [ - { name: 'REACTOR', status: 'ACTIVE', detail: '94.2%' }, - { name: 'LIFE SUPPORT', status: 'ACTIVE' }, - { name: 'NAV COMPUTER', status: 'SCANNING' }, - { name: 'WEAPONS', status: 'OFFLINE' }, - { name: 'COMMS', status: 'ACTIVE' }, -] - -const shipSystemCards = [ - { name: 'REACTOR', level: 'LV 2', status: 'ACTIVE' as const, actions: ['EXPAND', 'UPGRADE'] }, - { name: 'LIFE SUPPORT', level: 'LV 1', status: 'ACTIVE' as const, actions: ['EXPAND', 'UPGRADE'] }, - { name: 'NAV COMPUTER', level: 'LV 3', status: 'SCANNING' as const, actions: ['EXPAND', 'UPGRADE'] }, - { name: 'WEAPONS', level: 'OFFLINE', status: 'OFFLINE' as const, actions: ['BUILD NEW'] }, - { name: 'COMMS ARRAY', level: 'LV 1', status: 'ACTIVE' as const, actions: ['EXPAND', 'UPGRADE'] }, -] - const skillCards = [ { name: 'ADVANCED FUSION CORE', unlocks: 'REACTOR LV 4 + OVERCLOCK', state: 'AVAILABLE' as const }, { name: 'HULL PLATING SCHEMATICS', unlocks: 'BUILD ARMOR BAY MODULES', state: 'AVAILABLE' as const }, @@ -89,6 +78,36 @@ const logLines: TerminalLine[] = [ export function MissionControl() { const narrow = useNarrow() const [tick, setTick] = useState(0) + // Ship state is owned here (not in the tab panel) so the left column mirrors ship actions + // immediately; the SHIP SYSTEMS tab applies each action's response via setShip. Crew count + // feeds the roster bar — refetched on page load, like the other panels. + const [ship, setShip] = useState(null) + const [crewCount, setCrewCount] = useState(null) + + useEffect(() => { + let cancelled = false + fetch('/api/ship') + .then(async (res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as ShipState + }) + .then((s) => { + if (!cancelled) setShip(s) + }) + .catch(() => {}) // left column + tab keep their loading state on failure + fetch('/api/crew') + .then(async (res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as CrewDto[] + }) + .then((c) => { + if (!cancelled) setCrewCount(c.length) + }) + .catch(() => {}) + return () => { + cancelled = true + } + }, []) useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 1000) @@ -257,11 +276,25 @@ export function MissionControl() { SHIP SYSTEMS
- 4 ONLINE + s.built) ? 'ACTIVE' : 'OFFLINE'}> + {ship ? `${ship.systems.filter((s) => s.built).length} ONLINE` : '—'} +
- + {ship ? ( + ({ + name: s.type, + status: s.built ? ('ACTIVE' as const) : ('OFFLINE' as const), + detail: s.built ? `LV ${s.level} · CAP ${s.capacity}` : undefined, + }))} + /> + ) : ( +
+ LOADING… +
+ )} @@ -269,11 +302,17 @@ export function MissionControl() { VESSEL METRICS -
- - - -
+ {ship ? ( + // Bars driven by real values: hull/reactor from system levels (level × 20, capped), + // crew roster vs the display cap. +
+ s.type === 'HULL')?.level ?? 0) * 20)} /> + s.type === 'REACTOR')?.level ?? 0) * 20)} /> + +
+ ) : ( + LOADING… + )}
@@ -330,40 +369,9 @@ export function MissionControl() { - {/* SHIP SYSTEMS TAB */} + {/* SHIP SYSTEMS TAB — live data from /api/ship (state owned by MissionControl) */} -
- {shipSystemCards.map((system) => ( - - -
- {system.name} - - {system.level} - -
-
- -

- MODULE BAY // PLACEHOLDER READOUT -

-
- - {system.actions.map((action) => ( - - ))} - -
- ))} -
+
{/* SKILL TREE TAB */} diff --git a/components/ship-systems.tsx b/components/ship-systems.tsx new file mode 100644 index 0000000..37177bc --- /dev/null +++ b/components/ship-systems.tsx @@ -0,0 +1,122 @@ +'use client' + +import { 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 { BUILD_PART_COST, MAX_CAPACITY, MAX_LEVEL, expandCost, totalQty, upgradeCost } from '@/lib/game/ship-costs' +import type { ShipState, ShipSystemDto } from '@/lib/game/ship' + +// 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). Buttons are disabled when unaffordable or capped, using the same +// cost constants the API enforces (lib/game/ship-costs.ts). + +interface ShipSystemsPanelProps { + ship: ShipState | null + narrow?: boolean + onChanged: (next: ShipState) => void +} + +export function ShipSystemsPanel({ ship, narrow, onChanged }: ShipSystemsPanelProps) { + const [error, setError] = useState(null) + const [busy, setBusy] = useState(false) // one action at a time — turn-based game + + 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) + 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 + } catch (err: unknown) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusy(false) + } + } + + return ( +
+ {error && ( +
+ {error.toUpperCase()} +
+ )} +
+ {ship.systems.map((system) => ( + + ))} +
+
+ ) +} + +function SystemCard({ + system, + resources, + parts, + busy, + onAct, +}: { + system: ShipSystemDto + resources: number + parts: number + busy: boolean + onAct: (url: string, body: unknown) => void +}) { + const upgradeDisabled = busy || !system.built || system.level >= MAX_LEVEL || resources < upgradeCost(system.level) + const expandDisabled = busy || !system.built || system.capacity >= MAX_CAPACITY || resources < expandCost(system.capacity) + const buildDisabled = busy || system.built || 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 ? ( + <> + + + + ) : ( + + )} + +
+ ) +} diff --git a/lib/game/ship-costs.ts b/lib/game/ship-costs.ts index cdb213b..5067772 100644 --- a/lib/game/ship-costs.ts +++ b/lib/game/ship-costs.ts @@ -23,6 +23,9 @@ export const BASE_CAPACITY = 2 export const MAX_LEVEL = 5 // UPGRADE cap export const MAX_CAPACITY = 10 // EXPAND cap +// Display-only cap for the VESSEL METRICS crew roster bar (no gameplay effect yet). +export const CREW_CAPACITY = 6 + // Cost model — all costs are in inventory items, consumed from the oldest cargo row of the // required kind first (see spendItems in lib/game/ship.ts): // UPGRADE {systemId} — `level` units of any resource (scales with current level)