window.GDD = window.GDD || {}; const { useState, useEffect, useRef, useCallback, useMemo } = React; const TH = window.GDD.THREE; /* ── Scale constants ── */ const SCALE = 0.5; // world position scale const ORBIT_SCALE = 0.22; // orbital radius scale const PLANET_SCALE = 0.7; // planet mesh scale function StarMapDemo() { const containerRef = useRef(null); const sceneRef = useRef(null); const [systems, setSystems] = useState([]); const [connections, setConnections] = useState([]); const [selected, setSelected] = useState(null); const [destination, setDestination] = useState(null); const [hovered, setHovered] = useState(null); const [waypoints, setWaypoints] = useState([]); const [route, setRoute] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [systemFilter, setSystemFilter] = useState('all'); const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, target: null }); const systemMeshesRef = useRef([]); const routeLineRef = useRef(null); const connLinesRef = useRef([]); const animIdRef = useRef(null); const cameraCtrlRef = useRef(null); const minimapCanvasRef = useRef(null); const focusTargetRef = useRef(null); const orbitalBodiesRef = useRef([]); const asteroidBeltsRef = useRef([]); /* ── Celestial data ── */ const celestialData = useMemo(() => { return window.GDD.CONSTANTS.CELESTIAL_BODIES || {}; }, []); const selectedCelestial = useMemo(() => { if (!selected) return null; return celestialData[selected.id] || null; }, [selected, celestialData]); /* ── Load data ── */ useEffect(() => { window.GDD.api.getSystems().then(s => setSystems(s)); window.GDD.api.getConnections().then(c => setConnections(c)); }, []); /* ── Camera fly-to on selection ── */ useEffect(() => { if (!focusTargetRef.current || !cameraCtrlRef.current) return; cameraCtrlRef.current.flyTo(focusTargetRef.current.x, focusTargetRef.current.y, focusTargetRef.current.z || 0); focusTargetRef.current = null; }, [selected]); /* ── Context menu close on click / Escape / scroll ── */ useEffect(() => { const close = () => setContextMenu(prev => ({ ...prev, visible: false })); const onKey = (e) => { if (e.key === 'Escape') close(); }; window.addEventListener('click', close); window.addEventListener('keydown', onKey); return () => { window.removeEventListener('click', close); window.removeEventListener('keydown', onKey); }; }, []); /* ── Build 3D scene ── */ useEffect(() => { if (systems.length === 0) return; const container = containerRef.current; if (!container) return; // Cleanup previous if (sceneRef.current) { sceneRef.current.scene.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (child.material.map) child.material.map.dispose(); child.material.dispose(); } }); sceneRef.current.renderer.dispose(); if (sceneRef.current.renderer.domElement.parentNode) sceneRef.current.renderer.domElement.parentNode.removeChild(sceneRef.current.renderer.domElement); if (animIdRef.current) cancelAnimationFrame(animIdRef.current); if (cameraCtrlRef.current) cameraCtrlRef.current.dispose(); } const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x040810, 0.0003); const w = container.clientWidth; const h = container.clientHeight; const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 5000); camera.position.set(0, 180, 220); camera.lookAt(0, 0, 0); const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); renderer.setSize(w, h); TH.handleResize(renderer, camera, container); // Star field const stars = TH.createStarField(4000, 2500); scene.add(stars); // Nebulae TH.addNebula(scene, 0x22d3ee, [-100, 50, -300], 400); TH.addNebula(scene, 0xa78bfa, [200, -80, -200], 300); TH.addNebula(scene, 0xf0a030, [0, 100, -250], 200); // Grid const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520); grid.material.transparent = true; grid.material.opacity = 0.12; scene.add(grid); TH.setupSpaceLighting(scene); // Build system meshes systemMeshesRef.current = []; orbitalBodiesRef.current = []; asteroidBeltsRef.current = []; systems.forEach(sys => { const group = TH.createStarSystem(sys, SCALE); scene.add(group); systemMeshesRef.current.push(group); // Create celestial bodies for this system const cData = celestialData[sys.id]; if (!cData) return; const sysPos = new THREE.Vector3(sys.x * SCALE, sys.y * SCALE, 0); cData.bodies.forEach(body => { if (body.type === 'belt') { // Asteroid belt const belt = TH.createAsteroidBelt( body.innerOrbit * ORBIT_SCALE, body.outerOrbit * ORBIT_SCALE, body.count, { basePeriod: 20, maxInclination: 0.12 } ); belt.position.copy(sysPos); scene.add(belt); asteroidBeltsRef.current.push({ belt, parentPos: sysPos }); return; } // Planet const orbitR = body.orbit * ORBIT_SCALE; const planetSize = body.size * PLANET_SCALE; const planetMesh = TH.createPlanetMesh(planetSize, body.color, body.atmosphere); scene.add(planetMesh); // Orbit trail const trail = TH.createOrbitTrail(orbitR, body.ecc, body.inc, 0x1a3050, 0.18); trail.position.copy(sysPos); scene.add(trail); // Orbital body const orbBody = new TH.OrbitalBody({ mesh: planetMesh, parentPos: sysPos, orbitRadius: orbitR, eccentricity: body.ecc, inclination: body.inc, period: body.period, }); // Rings if (body.hasRings) { const rings = TH.createRingSystem(planetSize * 1.3, planetSize * 2.1, body.color, 0.25); planetMesh.add(rings); } // Moons if (body.moons) { body.moons.forEach(moon => { const moonSize = moon.size * PLANET_SCALE * 0.8; const moonMesh = TH.createPlanetMesh(moonSize, moon.color); scene.add(moonMesh); const moonOrbitR = moon.orbit * ORBIT_SCALE * 0.5; const moonTrail = TH.createOrbitTrail(moonOrbitR, 0, 0, 0x0d1a2a, 0.08); scene.add(moonTrail); const moonBody = new TH.OrbitalBody({ mesh: moonMesh, orbitRadius: moonOrbitR, period: moon.period, }); orbBody.children.push(moonBody); }); } // Stations in orbit around planets if (sys.stations && body.type === 'terrestrial') { sys.stations.forEach((stn, si) => { const stnMesh = TH.createOrbitalStation(planetSize * 0.25, 0x22d3ee); scene.add(stnMesh); const stnOrbitR = planetSize * 2.5 + si * 1.5; const stnTrail = TH.createOrbitTrail(stnOrbitR, 0, 0, 0x22d3ee, 0.06); scene.add(stnTrail); const stnBody = new TH.OrbitalBody({ mesh: stnMesh, orbitRadius: stnOrbitR, period: 3 + si, phase: si * Math.PI * 0.7, }); orbBody.children.push(stnBody); }); } orbitalBodiesRef.current.push(orbBody); }); }); // Connection lines connLinesRef.current = []; connections.forEach(([a, b]) => { const sa = systems.find(s => s.id === a); const sb = systems.find(s => s.id === b); if (!sa || !sb) return; const line = TH.createConnectionLine( { x: sa.x * SCALE, y: sa.y * SCALE, z: 0 }, { x: sb.x * SCALE, y: sb.y * SCALE, z: 0 }, 0x1c2a3f, 0.4 ); scene.add(line); connLinesRef.current.push({ line, a, b }); }); // Camera controller const ctrl = new TH.OrbitController(camera, renderer.domElement, new THREE.Vector3(0, 0, 0)); ctrl.minDistance = 8; ctrl.maxDistance = 400; ctrl.dampingFactor = 0.08; ctrl.rotateSpeed = 0.4; ctrl.zoomSpeed = 0.8; ctrl.panSpeed = 0.5; ctrl.enablePan = true; ctrl.panButton = 2; cameraCtrlRef.current = ctrl; sceneRef.current = { scene, camera, renderer, stars }; // Animation loop const clock = new THREE.Clock(); const animate = () => { animIdRef.current = requestAnimationFrame(animate); const dt = clock.getDelta(); const t = clock.elapsedTime; ctrl.update(dt); stars.rotation.y = t * 0.001; // Pulse system glows systemMeshesRef.current.forEach(group => { const glow = group.children[1]; if (glow) { const base = 6 * SCALE; glow.scale.setScalar(base + Math.sin(t * 2 + group.position.x) * 0.5); } }); // Update orbital bodies (Keplerian physics) orbitalBodiesRef.current.forEach(ob => ob.update(dt)); // Update asteroid belts asteroidBeltsRef.current.forEach(ab => TH.updateAsteroidBelt(ab.belt, dt, ab.parentPos)); renderer.render(scene, camera); }; animate(); const onResize = () => TH.handleResize(renderer, camera, container); window.addEventListener('resize', onResize); return () => { if (animIdRef.current) cancelAnimationFrame(animIdRef.current); window.removeEventListener('resize', onResize); if (cameraCtrlRef.current) cameraCtrlRef.current.dispose(); }; }, [systems, celestialData]); /* ── Draw minimap ── */ useEffect(() => { const canvas = minimapCanvasRef.current; if (!canvas || systems.length === 0) return; const ctx = canvas.getContext('2d'); const W = canvas.width; const H = canvas.height; const xs = systems.map(s => s.x); const ys = systems.map(s => s.y); const minX = Math.min(...xs) - 40; const maxX = Math.max(...xs) + 40; const minY = Math.min(...ys) - 40; const maxY = Math.max(...ys) + 40; const scaleX = (W - 20) / (maxX - minX); const scaleY = (H - 20) / (maxY - minY); const sc = Math.min(scaleX, scaleY); const mapX = (x) => 10 + (x - minX) * sc; const mapY = (y) => H - 10 - (y - minY) * sc; ctx.clearRect(0, 0, W, H); ctx.fillStyle = 'rgba(8,12,20,0.95)'; ctx.fillRect(0, 0, W, H); ctx.strokeStyle = 'rgba(28,42,63,0.6)'; ctx.lineWidth = 0.5; connections.forEach(([a, b]) => { const sa = systems.find(s => s.id === a); const sb = systems.find(s => s.id === b); if (!sa || !sb) return; ctx.beginPath(); ctx.moveTo(mapX(sa.x), mapY(sa.y)); ctx.lineTo(mapX(sb.x), mapY(sb.y)); ctx.stroke(); }); if (route.length > 1) { ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 2]); ctx.beginPath(); route.forEach((s, i) => { if (i === 0) ctx.moveTo(mapX(s.x), mapY(s.y)); else ctx.lineTo(mapX(s.x), mapY(s.y)); }); ctx.stroke(); ctx.setLineDash([]); } systems.forEach(sys => { const x = mapX(sys.x), y = mapY(sys.y); const isSel = selected && selected.id === sys.id; const isDest = destination === sys.id; const isHov = hovered && hovered.id === sys.id; let col = '#5a6b82'; if (sys.security >= 0.5) col = '#22c55e'; else if (sys.security >= 0.2) col = '#f0a030'; else col = '#ef4444'; if (isSel) { ctx.fillStyle = '#f0a030'; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); } if (isDest) { ctx.strokeStyle = '#22d3ee'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.stroke(); } const r = isHov ? 3 : isSel ? 4 : 2.5; ctx.fillStyle = col; ctx.globalAlpha = isHov || isSel ? 1 : 0.7; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; if (isSel || isHov || isDest) { ctx.font = '8px JetBrains Mono, monospace'; ctx.fillStyle = isSel ? '#f1f5f9' : '#94a3b8'; ctx.textAlign = 'center'; ctx.fillText(sys.name, x, y - 8); } }); }, [systems, connections, selected, hovered, destination, route]); /* ── Update route 3D line ── */ useEffect(() => { if (!sceneRef.current) return; const { scene } = sceneRef.current; if (routeLineRef.current) { scene.remove(routeLineRef.current); if (routeLineRef.current.geometry) routeLineRef.current.geometry.dispose(); if (routeLineRef.current.material) routeLineRef.current.material.dispose(); routeLineRef.current = null; } if (route.length > 1) { const line = TH.createRouteLine(route.map(s => ({ x: s.x * SCALE, y: s.y * SCALE, z: 1 })), 0x22d3ee); scene.add(line); routeLineRef.current = line; } }, [route]); /* ── Update selection visuals ── */ useEffect(() => { systemMeshesRef.current.forEach(group => { const sys = group.userData; const isSel = selected && selected.id === sys.id; const isHov = hovered && hovered.id === sys.id; const isDest = destination === sys.id; const isWP = waypoints.some(w => w.id === sys.id); const core = group.children[0]; if (core) core.scale.setScalar(isSel ? 2.0 : isHov ? 1.5 : isDest ? 1.6 : isWP ? 1.3 : 1); const glow = group.children[1]; if (glow) { let col = new THREE.Color(sys.color); if (isDest) col = new THREE.Color(0x22d3ee); else if (isSel) col = new THREE.Color(0xf0a030); else if (isWP) col = new THREE.Color(0xa78bfa); glow.material.color.copy(col); } const label = group.children[2]; if (label) { let color = '#5a6b82', size = 18; if (isSel) { color = '#f1f5f9'; size = 22; } else if (isHov) { color = '#d4dce8'; size = 20; } else if (isDest) { color = '#22d3ee'; size = 20; } else if (isWP) { color = '#a78bfa'; size = 19; } const prefix = isDest ? '⊕ ' : isWP ? '◇ ' : ''; TH.updateLabelText(label, prefix + sys.name, color, size); } }); }, [selected, hovered, destination, waypoints]); /* ── Highlight connections ── */ useEffect(() => { connLinesRef.current.forEach(({ line, a, b }) => { const isOnRoute = route.some((s, i) => { const next = route[i + 1]; return next && ((a === s.id && b === next.id) || (b === s.id && a === next.id)); }); const isActive = selected && (selected.id === a || selected.id === b); if (isOnRoute) { line.material.color.set(0x22d3ee); line.material.opacity = 0.5; } else if (isActive) { line.material.color.set(0xf0a030); line.material.opacity = 0.5; } else { line.material.color.set(0x1c2a3f); line.material.opacity = 0.25; } }); }, [selected, route]); /* ── Raycast helper ── */ const raycastSystems = useCallback((e) => { if (!sceneRef.current) return null; const { camera } = sceneRef.current; const rect = containerRef.current.getBoundingClientRect(); const mouse = new THREE.Vector2( ((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1 ); const cores = systemMeshesRef.current.map(g => g.children[0]).filter(Boolean); const hits = TH.raycast(mouse, camera, cores); if (hits.length > 0) return hits[0].object.parent; return null; }, []); /* ── Click ── */ const handleClick = useCallback((e) => { const group = raycastSystems(e); if (group) setSelected(group.userData); else setSelected(null); }, [raycastSystems]); /* ── Double-click ── */ const handleDoubleClick = useCallback((e) => { const group = raycastSystems(e); if (group) { const sys = group.userData; setSelected(sys); focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 }; } }, [raycastSystems]); /* ── Hover ── */ const handleMove = useCallback((e) => { const group = raycastSystems(e); if (group) { setHovered(group.userData); containerRef.current.style.cursor = 'pointer'; } else { setHovered(null); containerRef.current.style.cursor = 'grab'; } }, [raycastSystems]); /* ── Right-click context menu ── */ const handleContextMenu = useCallback((e) => { e.preventDefault(); e.stopPropagation(); const group = raycastSystems(e); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: group ? group.userData : null, }); }, [raycastSystems]); /* ── Actions ── */ const setDestFor = useCallback((sys) => { setDestination(sys.id); const sol = systems.find(s => s.id === 'sol'); setRoute(sol && sys.id !== 'sol' ? [sol, sys] : [sys]); }, [systems]); const addWaypointFor = useCallback((sys) => { setWaypoints(prev => { if (prev.some(w => w.id === sys.id)) return prev; const next = [...prev, sys]; const sol = systems.find(s => s.id === 'sol'); setRoute(sol ? [sol, ...next] : next); return next; }); }, [systems]); const handleSetDestination = useCallback(() => { if (selected) setDestFor(selected); }, [selected, setDestFor]); const handleAddWaypoint = useCallback(() => { if (selected) addWaypointFor(selected); }, [selected, addWaypointFor]); const handleClearRoute = useCallback(() => { setDestination(null); setWaypoints([]); setRoute([]); }, []); const handleZoomTo = useCallback((level) => { if (cameraCtrlRef.current) cameraCtrlRef.current.distance = level; }, []); const handleResetView = useCallback(() => { const ctrl = cameraCtrlRef.current; if (!ctrl) return; ctrl.target.set(0, 0, 0); ctrl.distance = 200; ctrl.theta = Math.PI / 4; ctrl.phi = Math.PI / 3; }, []); /* ── Filtered systems ── */ const filteredSystems = useMemo(() => { let list = systems; if (searchQuery) { const q = searchQuery.toLowerCase(); list = list.filter(s => s.name.toLowerCase().includes(q)); } if (systemFilter !== 'all') { if (systemFilter === 'highsec') list = list.filter(s => s.security >= 0.5); else if (systemFilter === 'lowsec') list = list.filter(s => s.security >= 0.2 && s.security < 0.5); else if (systemFilter === 'nullsec') list = list.filter(s => s.security < 0.2); } return list; }, [systems, searchQuery, systemFilter]); const currentSystem = systems.find(s => s.id === 'sol'); const destSystem = destination ? systems.find(s => s.id === destination) : null; const secColor = (sec) => { if (sec >= 0.5) return 'var(--green)'; if (sec >= 0.2) return 'var(--accent)'; return 'var(--red)'; }; const secBg = (sec) => { if (sec >= 0.5) return 'var(--green-bg)'; if (sec >= 0.2) return 'var(--accent-bg)'; return 'var(--red-bg)'; }; const planetIcon = (t) => { if (t === 'gas') return '⛽'; if (t === 'terrestrial') return '🌍'; return '●'; }; /* ══════════════════════════════════════ RENDER ══════════════════════════════════════ */ return (
{/* 3D Canvas */}
{/* ═══ Custom Context Menu ═══ */} {contextMenu.visible && (
e.stopPropagation()} style={{ position: 'fixed', left: contextMenu.x, top: contextMenu.y, zIndex: 9999, minWidth: '200px', background: 'rgba(10,16,28,0.97)', border: '1px solid var(--border-light)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(16px)', boxShadow: '0 8px 40px rgba(0,0,0,0.7), 0 0 1px rgba(34,211,238,0.15)', overflow: 'hidden', fontFamily: 'var(--font-mono)', fontSize: '11px', }}> {contextMenu.target ? ( <> {/* System header */}
{contextMenu.target.name} {contextMenu.target.security.toFixed(1)}
{/* Quick info */}
TYPE {contextMenu.target.type}
PLANETS {contextMenu.target.planets}
{celestialData[contextMenu.target.id] && (
FACTION {celestialData[contextMenu.target.id].faction}
)}
{/* Actions */}
{[ { icon: '✦', label: 'Show Details', action: () => { setSelected(contextMenu.target); focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '⊕', label: 'Set Destination', action: () => { setDestFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); }, highlight: 'var(--cyan)' }, { icon: '◇', label: 'Add Waypoint', action: () => { addWaypointFor(contextMenu.target); setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '◎', label: 'Focus Camera', action: () => { focusTargetRef.current = { x: contextMenu.target.x * SCALE, y: contextMenu.target.y * SCALE, z: 0 }; setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '⚐', label: 'Bookmark', action: () => setContextMenu(prev => ({...prev, visible: false})) }, ].map((item, i) => (
{ e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> {item.icon} {item.label}
))}
) : ( <>
Star Map Controls
{[ { icon: '⟳', label: 'Reset View', action: () => { handleResetView(); setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '⊞', label: 'Zoom — Region', action: () => { handleZoomTo(120); setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '⊡', label: 'Zoom — Show All', action: () => { handleZoomTo(280); setContextMenu(prev => ({...prev, visible: false})); } }, { icon: '◎', label: 'Zoom — System', action: () => { handleZoomTo(30); setContextMenu(prev => ({...prev, visible: false})); } }, ].map((item, i) => (
{ e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> {item.icon} {item.label}
))}
)}
)} {/* ═══ HUD Overlay ═══ */}
{/* ═══ TOP BAR ═══ */}
STAR MAP {currentSystem?.name || 'Sol'} · {currentSystem?.security.toFixed(1) || '1.0'}
Systems {systems.length}
Bridges {connections.length} {destination && ( <>
DEST: {destSystem?.name} · {route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''} )}
Zoom {[{ label: 'SYS', dist: 30 }, { label: 'REG', dist: 120 }, { label: 'ALL', dist: 280 }].map(z => ( ))}
{/* ═══ MIDDLE AREA ═══ */}
{/* ═══ LEFT — System Details Panel ═══ */}
{selected ? ( <> {/* System Header */}
{selected.name}
{selected.security.toFixed(1)}
Star Type {selected.type}
Planets {selected.planets}
Stations {selected.stations?.length || 0}
{selected.stations && selected.stations.length > 0 && (
{selected.stations.map((stn, i) => (
{stn}
))}
)}
{/* ═══ System Details (Celestial Bodies) ═══ */} {selectedCelestial && (
{selectedCelestial.description}
{selectedCelestial.faction} Pop: {selectedCelestial.population}
{/* Resources */} {selectedCelestial.resources.length > 0 && (
Ore: {selectedCelestial.resources.map((r, i) => ( {r} ))}
)} {/* Celestial Bodies List */}
{selectedCelestial.bodies.map((body, i) => (
{/* Icon */} {body.type === 'belt' ? '·' : ''}
{body.name}
{body.type === 'belt' ? `${body.count} objects · ${body.innerOrbit}-${body.outerOrbit} AU` : `${body.type} · orbit ${body.orbit} AU · T ${body.period.toFixed(1)}s` }
{body.ecc > 0.05 && ( e={body.ecc.toFixed(2)} )} {body.moons && body.moons.length > 0 && ( {body.moons.length} moon{body.moons.length > 1 ? 's' : ''} )} {body.hasRings && ( rings )}
))}
)} {/* Connections */}
Connections {connections.filter(([a, b]) => a === selected.id || b === selected.id).length} jumps
{connections.filter(([a, b]) => a === selected.id || b === selected.id).map(([a, b]) => { const neighborId = a === selected.id ? b : a; const neighbor = systems.find(s => s.id === neighborId); if (!neighbor) return null; return (
{ setSelected(neighbor); focusTargetRef.current = { x: neighbor.x * SCALE, y: neighbor.y * SCALE, z: 0 }; }} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: neighbor }); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px', borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s', borderLeft: `2px solid ${secColor(neighbor.security)}`, }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}> {neighbor.name} {neighbor.security.toFixed(1)}
); })}
) : (
Click a system to select
Double-click to focus
Right-click for options
Scroll to zoom
)}
{/* CENTER — hover tooltip */}
{hovered && hovered.id !== selected?.id && (
{hovered.name} SEC {hovered.security.toFixed(1)} {hovered.type} {celestialData[hovered.id] && ( {celestialData[hovered.id].bodies.length} bodies )}
)}
{/* ═══ RIGHT — System List ═══ */}
{/* Search */}
setSearchQuery(e.target.value)} placeholder="Search systems..." style={{ width: '100%', padding: '6px 10px 6px 28px', fontSize: '10px', fontFamily: 'var(--font-mono)', background: 'rgba(15,22,35,0.92)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', color: 'var(--fg)', backdropFilter: 'blur(8px)', outline: 'none', }} />
{/* Filter tabs */}
{[ { key: 'all', label: 'ALL' }, { key: 'highsec', label: 'HI', col: 'var(--green)' }, { key: 'lowsec', label: 'LO', col: 'var(--accent)' }, { key: 'nullsec', label: 'NULL', col: 'var(--red)' }, ].map(f => ( ))}
{/* System list */}
Systems {filteredSystems.length}
{filteredSystems.map(sys => { const isSel = selected?.id === sys.id; const isDest = destination === sys.id; const isWP = waypoints.some(w => w.id === sys.id); return (
setSelected(sys)} onDoubleClick={() => { setSelected(sys); focusTargetRef.current = { x: sys.x * SCALE, y: sys.y * SCALE, z: 0 }; }} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ visible: true, x: Math.min(e.clientX, window.innerWidth - 200), y: Math.min(e.clientY, window.innerHeight - 250), target: sys }); }} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '5px 8px', borderRadius: '4px', cursor: 'pointer', transition: 'background 0.15s', background: isSel ? 'var(--accent-bg)' : isDest ? 'var(--cyan-bg)' : 'transparent', borderLeft: `2px solid ${isDest ? 'var(--cyan)' : isWP ? 'var(--purple)' : secColor(sys.security)}`, }} onMouseEnter={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'var(--surface-raised)'; }} onMouseLeave={(e) => { if (!isSel && !isDest) e.currentTarget.style.background = 'transparent'; }}> {isDest ? '⊕ ' : isWP ? '◇ ' : ''}{sys.name} {sys.security.toFixed(1)}
); })}
{/* ═══ BOTTOM BAR ═══ */}
{/* Route */}
Autopilot Route {route.length > 1 && ( <> {route.length - 1} jump{route.length - 1 !== 1 ? 's' : ''} )} {!route.length && No route set}
{route.length > 0 && (
{route.map((sys, i) => (
{i === 0 ? '⬡' : i === route.length - 1 ? '⊕' : '◇'} {sys.name} {sys.security.toFixed(1)}
{i < route.length - 1 && }
))}
)}
{/* Ship Status */}
Ship Venture
{[{ label: 'SH', value: 100, color: '#22d3ee' }, { label: 'AR', value: 100, color: '#f0a030' }, { label: 'HU', value: 100, color: '#22c55e' }, { label: 'CA', value: 85, color: '#a78bfa' }].map(bar => (
{bar.label} {bar.value}%
))}
{/* Wallet & Time */}
₢125,000
Online 14:23 UTC
{/* Minimap */}
Overview
); } window.GDD.StarMapDemo = StarMapDemo;