139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { NodeGraph } from '@/components/ui/node-graph'
|
|
import type { WorldState } from '@/lib/game/save'
|
|
|
|
// WORLD MAP tab — the first live game panel: renders real world state from /api/world and
|
|
// offers a jump button per system adjacent to the current one (NodeGraph nodes are not
|
|
// clickable upstream, so the affordance lives in this list). Turn-based: fetch on mount,
|
|
// refetch via the jump response — no polling.
|
|
|
|
interface WorldMapPanelProps {
|
|
height?: number
|
|
}
|
|
|
|
export function WorldMapPanel({ height = 380 }: WorldMapPanelProps) {
|
|
const [world, setWorld] = useState<WorldState | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [jumpingId, setJumpingId] = useState<number | null>(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
fetch('/api/world')
|
|
.then(async (res) => {
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
return (await res.json()) as WorldState
|
|
})
|
|
.then((state) => {
|
|
if (!cancelled) setWorld(state)
|
|
})
|
|
.catch((err: unknown) => {
|
|
if (!cancelled) setError(err instanceof Error ? err.message : String(err))
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
const jump = useCallback(async (systemId: number) => {
|
|
setJumpingId(systemId)
|
|
try {
|
|
const res = await fetch('/api/jump', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ systemId }),
|
|
})
|
|
const body = (await res.json()) as WorldState & { error?: string }
|
|
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`)
|
|
setWorld(body) // fresh state straight from the response — no refetch needed
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : String(err))
|
|
} finally {
|
|
setJumpingId(null)
|
|
}
|
|
}, [])
|
|
|
|
if (error && !world) {
|
|
return (
|
|
<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' }}>
|
|
CHART OFFLINE // {error.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!world) {
|
|
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' }}>SCANNING CHART…</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const current = world.systems.find((s) => s.current)!
|
|
const reachable = world.edges
|
|
.filter(([a, b]) => a === current.id || b === current.id)
|
|
.map(([a, b]) => (a === current.id ? b : a))
|
|
.map((id) => world.systems.find((s) => s.id === id)!)
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
<NodeGraph
|
|
nodes={world.systems.map((s) => ({
|
|
id: String(s.id),
|
|
label: s.name,
|
|
x: s.x,
|
|
y: s.y,
|
|
status: s.current ? 'ACTIVE' : s.discovered ? 'NEUTRAL' : 'OFFLINE',
|
|
sublabel: s.current ? 'CURRENT' : s.discovered ? 'DISCOVERED' : 'UNKNOWN',
|
|
}))}
|
|
edges={world.edges.map(([a, b]) => ({ from: String(a), to: String(b) }))}
|
|
variant="ACTIVE"
|
|
title={`GALACTIC CHART // JUMP ROUTES — TURN ${world.turn}`}
|
|
height={height}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
|
|
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', overflow: 'hidden' }}>
|
|
<div
|
|
style={{
|
|
padding: '0.4rem 0.75rem',
|
|
borderBottom: '1px solid var(--border)',
|
|
background: 'var(--surface-raised)',
|
|
fontSize: '0.6rem',
|
|
color: 'var(--text-muted)',
|
|
letterSpacing: '0.12em',
|
|
textTransform: 'uppercase',
|
|
}}
|
|
>
|
|
JUMP ROUTES // FROM {current.name.toUpperCase()}
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', padding: '0.6rem 0.75rem' }}>
|
|
{reachable.map((s) => (
|
|
<Button
|
|
key={s.id}
|
|
variant="OUTLINE"
|
|
size="SM"
|
|
disabled={jumpingId !== null}
|
|
onClick={() => void jump(s.id)}
|
|
>
|
|
{jumpingId === s.id ? 'JUMPING…' : `${s.name.toUpperCase()} // JUMP`}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.5rem 0.75rem' }}>
|
|
<span style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>
|
|
JUMP FAILED // {error.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|