ui: real SHIP SYSTEMS tab (upgrade/expand/build buttons) + left column mirrors live ship state
This commit is contained in:
@@ -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<ShipState | null>(null)
|
||||
const [crewCount, setCrewCount] = useState<number | null>(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() {
|
||||
<PanelHeader>
|
||||
<PanelTitle>SHIP SYSTEMS</PanelTitle>
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<Badge variant="ACTIVE">4 ONLINE</Badge>
|
||||
<Badge variant={ship && ship.systems.some((s) => s.built) ? 'ACTIVE' : 'OFFLINE'}>
|
||||
{ship ? `${ship.systems.filter((s) => s.built).length} ONLINE` : '—'}
|
||||
</Badge>
|
||||
</div>
|
||||
</PanelHeader>
|
||||
|
||||
<StatusGrid systems={shipSystems} />
|
||||
{ship ? (
|
||||
<StatusGrid
|
||||
systems={ship.systems.map((s) => ({
|
||||
name: s.type,
|
||||
status: s.built ? ('ACTIVE' as const) : ('OFFLINE' as const),
|
||||
detail: s.built ? `LV ${s.level} · CAP ${s.capacity}` : undefined,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '0.5rem 0.75rem' }}>
|
||||
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>LOADING…</span>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
<Panel notch="sm">
|
||||
@@ -269,11 +302,17 @@ export function MissionControl() {
|
||||
<PanelTitle>VESSEL METRICS</PanelTitle>
|
||||
</PanelHeader>
|
||||
<PanelContent>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
|
||||
<Progress label="HULL INTEGRITY" value={87} />
|
||||
<Progress label="REACTOR OUTPUT" value={94} />
|
||||
<Progress label="CREW ROSTER / 6" value={50} />
|
||||
</div>
|
||||
{ship ? (
|
||||
// Bars driven by real values: hull/reactor from system levels (level × 20, capped),
|
||||
// crew roster vs the display cap.
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
|
||||
<Progress label="HULL INTEGRITY" value={Math.min(100, (ship.systems.find((s) => s.type === 'HULL')?.level ?? 0) * 20)} />
|
||||
<Progress label="REACTOR OUTPUT" value={Math.min(100, (ship.systems.find((s) => s.type === 'REACTOR')?.level ?? 0) * 20)} />
|
||||
<Progress label={`CREW ROSTER / ${CREW_CAPACITY}`} value={Math.round(((crewCount ?? 0) / CREW_CAPACITY) * 100)} />
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>LOADING…</span>
|
||||
)}
|
||||
</PanelContent>
|
||||
</Panel>
|
||||
</div>
|
||||
@@ -330,40 +369,9 @@ export function MissionControl() {
|
||||
<CrewPanel narrow={narrow} />
|
||||
</TabsContent>
|
||||
|
||||
{/* SHIP SYSTEMS TAB */}
|
||||
{/* SHIP SYSTEMS TAB — live data from /api/ship (state owned by MissionControl) */}
|
||||
<TabsContent value="ship-systems" style={{ margin: 0, padding: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: narrow ? '1fr' : 'repeat(2, 1fr)',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
{shipSystemCards.map((system) => (
|
||||
<Card key={system.name}>
|
||||
<CardHeader>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CardTitle>{system.name}</CardTitle>
|
||||
<Badge variant={system.status} className="ml-auto">
|
||||
{system.level}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p style={{ fontSize: '0.65rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
|
||||
MODULE BAY // PLACEHOLDER READOUT
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
{system.actions.map((action) => (
|
||||
<Button key={action} variant="OUTLINE" size="SM">
|
||||
{action}
|
||||
</Button>
|
||||
))}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<ShipSystemsPanel ship={ship} narrow={narrow} onChanged={setShip} />
|
||||
</TabsContent>
|
||||
|
||||
{/* SKILL TREE TAB */}
|
||||
|
||||
Reference in New Issue
Block a user