Files
SpaceWanderer/components/ship-systems.tsx

123 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}