ui: real SHIP SYSTEMS tab (upgrade/expand/build buttons) + left column mirrors live ship state
This commit is contained in:
@@ -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<string | null>(null)
|
||||
const [busy, setBusy] = useState(false) // one action at a time — turn-based game
|
||||
|
||||
if (!ship) {
|
||||
return (
|
||||
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.75rem' }}>
|
||||
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>LOADING SHIP SYSTEMS…</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{error && (
|
||||
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.75rem' }}>
|
||||
<span style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>{error.toUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: narrow ? '1fr' : 'repeat(2, 1fr)', gap: '0.75rem' }}>
|
||||
{ship.systems.map((system) => (
|
||||
<SystemCard key={system.id} system={system} resources={resources} parts={parts} busy={busy} onAct={act} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CardTitle>{system.type}</CardTitle>
|
||||
<Badge variant={system.built ? 'ACTIVE' : 'OFFLINE'} className="ml-auto">
|
||||
{system.built ? `LV ${system.level}` : 'OFFLINE'}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', letterSpacing: '0.08em' }}>
|
||||
{system.built ? `LEVEL ${system.level} · CAPACITY ${system.capacity}` : 'NOT INSTALLED'}
|
||||
</p>
|
||||
<p style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
|
||||
{system.built
|
||||
? `UPGRADE ${upgradeCost(system.level)}× RES · EXPAND ${expandCost(system.capacity)}× RES`
|
||||
: `BUILD ${BUILD_PART_COST}× PARTS`}
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
{system.built ? (
|
||||
<>
|
||||
<Button variant="OUTLINE" size="SM" disabled={upgradeDisabled} onClick={() => onAct('/api/ship/upgrade', { systemId: system.id })}>
|
||||
UPGRADE
|
||||
</Button>
|
||||
<Button variant="OUTLINE" size="SM" disabled={expandDisabled} onClick={() => onAct('/api/ship/expand', { systemId: system.id })}>
|
||||
EXPAND
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="OUTLINE" size="SM" disabled={buildDisabled} onClick={() => onAct('/api/ship/build', { type: system.type })}>
|
||||
BUILD NEW
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user