123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
'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>
|
||
)
|
||
}
|