Files
SpaceWanderer/components/skill-tree.tsx
T

111 lines
4.2 KiB
TypeScript
Raw 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 { totalQty } from '@/lib/game/ship-costs'
import type { ShipState } from '@/lib/game/ship'
import type { TechDto } from '@/lib/game/tech'
// SKILL TREE tab — live data. The parent (MissionControl) owns the tech state + ship state; this panel
// applies each research response directly via onResearched (no polling). RESEARCH is enabled only when
// the tech is AVAILABLE and its cost is payable from the cargo hold, mirroring what POST /api/research enforces.
interface SkillTreePanelProps {
techs: TechDto[] | null
ship: ShipState | null
narrow?: boolean
onResearched: (next: TechDto[]) => void
}
export function SkillTreePanel({ techs, ship, narrow, onResearched }: SkillTreePanelProps) {
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false) // one action at a time — turn-based game
if (!techs || !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 SKILL TREE…</span>
</div>
)
}
const resources = totalQty(ship.inventory, 'resource')
const parts = totalQty(ship.inventory, 'part')
async function research(tech: TechDto) {
setBusy(true)
setError(null)
try {
const res = await fetch('/api/research', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ techId: tech.id }) })
const data = (await res.json()) as TechDto[] & { error?: string }
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`)
onResearched(data) // response is the fresh tech tree 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' }}>
{techs.map((tech) => (
<TechCard key={tech.slug} tech={tech} resources={resources} parts={parts} busy={busy} onResearch={research} />
))}
</div>
</div>
)
}
function TechCard({
tech,
resources,
parts,
busy,
onResearch,
}: {
tech: TechDto
resources: number
parts: number
busy: boolean
onResearch: (tech: TechDto) => void
}) {
const affordable = tech.costKind === 'resource' ? resources >= tech.costQty : parts >= tech.costQty
const researchDisabled = busy || tech.state !== 'AVAILABLE' || !affordable
return (
<Card>
<CardHeader>
<div style={{ display: 'flex', alignItems: 'center' }}>
<CardTitle>{tech.name}</CardTitle>
<Badge variant={tech.state === 'RESEARCHED' ? 'ACTIVE' : tech.state === 'AVAILABLE' ? 'SCANNING' : 'OFFLINE'} className="ml-auto">
{tech.state}
</Badge>
</div>
</CardHeader>
<CardContent>
<p style={{ fontSize: '0.65rem', color: 'var(--text-secondary)', letterSpacing: '0.08em' }}>
COST {tech.costQty}× {tech.costKind === 'resource' ? 'RESOURCES' : 'PARTS'}
</p>
<p style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>UNLOCKS // {tech.effect}</p>
{tech.state === 'LOCKED' && (
<p style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>REQUIRES // {tech.missingPrereqs.join(' + ')}</p>
)}
</CardContent>
<CardFooter>
<Button variant="OUTLINE" size="SM" disabled={researchDisabled} onClick={() => onResearch(tech)}>
RESEARCH
</Button>
</CardFooter>
</Card>
)
}