Remove the bottom JUMP ROUTES button list; systems adjacent to the current one become clickable galactic-chart nodes (same POST /api/jump flow, same state refreshes). Current + unreachable systems are dimmed and not clickable. NodeGraph gains optional onNodeClick + per-node disabled (backward-compatible); two smoke tests updated to click chart fields.
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { NodeGraph } from '@/components/ui/node-graph'
|
|
import type { WorldState } from '@/lib/game/save'
|
|
|
|
// WORLD MAP tab — renders real world state from /api/world. Clicking a system adjacent to the
|
|
// current one on the galactic chart jumps there (same POST /api/jump flow as before); the
|
|
// current and unreachable systems are dimmed and not clickable. 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)!
|
|
// Systems one jump away — the only clickable chart fields (the same set as the old JUMP ROUTES list).
|
|
const reachableIds = new Set(
|
|
world.edges.filter(([a, b]) => a === current.id || b === current.id).map(([a, b]) => (a === current.id ? b : a))
|
|
)
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
<NodeGraph
|
|
nodes={world.systems.map((s) => {
|
|
const jumpable = !s.current && reachableIds.has(s.id)
|
|
return {
|
|
id: String(s.id),
|
|
label: s.name,
|
|
x: s.x,
|
|
y: s.y,
|
|
status: s.current ? 'ACTIVE' : jumpable ? 'NEUTRAL' : 'OFFLINE',
|
|
sublabel: s.current ? 'CURRENT' : jumpable ? (s.discovered ? 'DISCOVERED // JUMP' : 'UNKNOWN // JUMP') : s.discovered ? 'DISCOVERED' : 'UNKNOWN',
|
|
disabled: !jumpable || jumpingId !== null, // dimmed + not clickable; all locked while a jump is in flight
|
|
}
|
|
})}
|
|
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%' }}
|
|
onNodeClick={(id) => void jump(Number(id))}
|
|
/>
|
|
|
|
{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>
|
|
)
|
|
}
|