window.GDD = window.GDD || {}; const { useState, useEffect, useRef, useCallback } = React; const TH = window.GDD.THREE; function ShipMovementDemo() { const containerRef = useRef(null); const sceneRef = useRef(null); const animIdRef = useRef(null); const moveRef = useRef(null); const waypointsRef = useRef([]); const targetRef = useRef(null); const shipGroupRef = useRef(null); const waypointMarkersRef = useRef([]); const entityMeshesRef = useRef([]); const trailRef = useRef(null); const gridRef = useRef(null); const trailPositionsRef = useRef([]); const frameCountRef = useRef(0); const clockRef = useRef(null); // HUD state const [shipSpeed, setShipSpeed] = useState(0); const [shipHeading, setShipHeading] = useState(0); const [shipPos, setShipPos] = useState({ x: 400, y: 300 }); const [moving, setMoving] = useState(false); const [currentTarget, setCurrentTarget] = useState(null); const [waypoints, setWaypoints] = useState([]); const [entities, setEntities] = useState([]); const [selectedEntity, setSelectedEntity] = useState(null); const [shipStatus, setShipStatus] = useState(null); const [serverTime, setServerTime] = useState('14:34:07'); const [showGrid, setShowGrid] = useState(true); const [moveProgress, setMoveProgress] = useState(0); // Chat state const [chatTab, setChatTab] = useState('local'); const [chatInput, setChatInput] = useState(''); const [chatMessages, setChatMessages] = useState([ { sender: 'CMDR Picard', body: 'Heading to Jita with a cargo of Kernite.', time: '14:22' }, { sender: 'CMDR Worf', body: 'Pirates spotted near U-IRTYR gate.', time: '14:25' }, { sender: 'CMDR Data', body: 'Scordite prices up 12% in Amarr.', time: '14:28' }, { sender: 'CMDR Troi', body: 'Mining fleet forming in Sol.', time: '14:31' }, ]); // Modules state const [modules, setModules] = useState({ high: [ { id: 'laser1', name: 'Mine Laser', icon: '⛏', active: false }, { id: 'turret1', name: '150mm Rail', icon: '◆', active: true }, { id: null }, ], med: [ { id: 'shield1', name: 'Shield Bst', icon: '◎', active: false }, { id: 'warp1', name: 'Afterburn', icon: '»', active: true }, { id: 'scram1', name: 'Scrambler', icon: '↯', active: false }, ], low: [ { id: 'armor1', name: 'Armor Plt', icon: '▭', active: false }, { id: 'magstab1', name: 'Mag Field', icon: '⚡', active: false }, ], }); // Load data useEffect(() => { window.GDD.api.getNearbyEntities().then(e => setEntities(e)); window.GDD.api.getShipStatus().then(s => setShipStatus(s)); }, []); // Server time tick useEffect(() => { const interval = setInterval(() => { setServerTime(prev => { const parts = prev.split(':'); let sec = parseInt(parts[2]) + 1; let min = parseInt(parts[1]); let hr = parseInt(parts[0]); if (sec >= 60) { sec = 0; min++; } if (min >= 60) { min = 0; hr++; } if (hr >= 24) hr = 0; return `${String(hr).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; }); }, 1000); return () => clearInterval(interval); }, []); // Build 3D scene useEffect(() => { const container = containerRef.current; if (!container) return; const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x040810, 0.0005); const camera = new THREE.PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 5000); camera.position.set(0, 40, 50); camera.lookAt(0, 0, 0); const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); TH.handleResize(renderer, camera, container); const stars = TH.createStarField(4000, 3000); scene.add(stars); TH.addNebula(scene, 0x22d3ee, [-200, 100, -500], 500); TH.addNebula(scene, 0xf0a030, [300, -50, -400], 400); TH.setupSpaceLighting(scene); const grid = new THREE.GridHelper(600, 30, 0x0d1520, 0x0d1520); grid.material.transparent = true; grid.material.opacity = 0.2; scene.add(grid); gridRef.current = grid; // Ship const shipGroup = new THREE.Group(); const shipMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.8); shipMesh.rotation.y = -Math.PI / 2; shipGroup.add(shipMesh); const engineGlow = TH.createEngineGlow(0x22d3ee, 3, 20); engineGlow.position.set(0, 0, -8); shipGroup.add(engineGlow); const trail = TH.createEngineTrail(0xf0a030, 50); shipGroup.add(trail); trailRef.current = trail; const label = TH.createLabel('USS ENTERPRISE', '#22d3ee', 18); label.position.y = 5; shipGroup.add(label); scene.add(shipGroup); shipGroupRef.current = shipGroup; sceneRef.current = { scene, camera, renderer, stars, shipGroup, engineGlow }; const clock = new THREE.Clock(); clockRef.current = clock; const animate = () => { animIdRef.current = requestAnimationFrame(animate); frameCountRef.current++; const t = clock.getElapsedTime(); const move = moveRef.current; if (move) { const dx = move.tx - move.sx; const dy = move.ty - move.sy; const totalDist = Math.sqrt(dx * dx + dy * dy); const speed = Math.max(0.008, 0.04 / (1 + totalDist / 500)); move.progress += speed; let nx, ny, angle; if (move.progress >= 1) { nx = move.tx; ny = move.ty; angle = Math.atan2(dy, dx); moveRef.current = null; if (waypointsRef.current.length > 0) { waypointsRef.current.shift(); setWaypoints([...waypointsRef.current]); if (waypointsRef.current.length > 0) { const next = waypointsRef.current[0]; targetRef.current = next; setCurrentTarget(next); moveRef.current = { sx: nx, sy: ny, tx: next.x, ty: next.y, progress: 0 }; } else { targetRef.current = null; setCurrentTarget(null); setMoving(false); } } else { setMoving(false); } } else { const ease = move.progress < 0.5 ? 2 * move.progress * move.progress : -1 + (4 - 2 * move.progress) * move.progress; nx = move.sx + dx * ease; ny = move.sy + dy * ease; angle = Math.atan2(dy, dx); trailPositionsRef.current.push({ x: nx * 0.1, y: 0, z: ny * 0.1, age: 0 }); if (trailPositionsRef.current.length > 50) trailPositionsRef.current.shift(); } shipGroup.position.x = nx * 0.1; shipGroup.position.z = ny * 0.1; shipGroup.position.y = 0; shipGroup.rotation.y = -angle + Math.PI / 2; engineGlow.intensity = 4; if (trailPositionsRef.current.length > 0) { const posAttr = trail.geometry.attributes.position; trailPositionsRef.current.forEach((p, i) => { p.age++; if (i < posAttr.count) { posAttr.setXYZ(i, p.x - shipGroup.position.x, p.y, p.z - shipGroup.position.z); } }); posAttr.needsUpdate = true; } if (frameCountRef.current % 6 === 0) { setShipSpeed(totalDist * speed * 60); setShipHeading(((angle * 180 / Math.PI + 360) % 360)); setShipPos({ x: nx, y: ny }); setMoveProgress(move.progress); } } else { engineGlow.intensity = 1; if (trailPositionsRef.current.length > 0) { trailPositionsRef.current = []; const posAttr = trail.geometry.attributes.position; for (let i = 0; i < posAttr.count; i++) posAttr.setXYZ(i, 0, -100, 0); posAttr.needsUpdate = true; } shipGroup.position.y = Math.sin(t * 1.5) * 0.3; } TH.followTarget(camera, shipGroup.position, { x: 0, y: 40, z: 50 }, 0.04); stars.position.x = shipGroup.position.x * -0.02; stars.position.z = shipGroup.position.z * -0.02; 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); }; }, []); // Update entities in 3D useEffect(() => { if (!sceneRef.current) return; const { scene } = sceneRef.current; entityMeshesRef.current.forEach(m => scene.remove(m)); entityMeshesRef.current = []; entities.forEach(ent => { const x = ent.x * 0.1; const z = ent.y * 0.1; let mesh; if (ent.type === 'asteroid') { mesh = TH.createAsteroid(1.5, 0x3d2a5c); } else if (ent.type === 'station') { mesh = TH.createStation(2, 0x22d3ee); } else if (ent.type === 'hostile') { mesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.6); mesh.rotation.y = -Math.PI / 2; } else { mesh = TH.createShipMesh(0x1a3a2a, 0x22c55e, 0.5); mesh.rotation.y = -Math.PI / 2; } const group = new THREE.Group(); group.add(mesh); const label = TH.createLabel(ent.name, ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : '#22c55e', 14); label.position.y = 4; group.add(label); group.position.set(x, 0, z); scene.add(group); entityMeshesRef.current.push(group); }); }, [entities]); // Update waypoints in 3D useEffect(() => { if (!sceneRef.current) return; const { scene } = sceneRef.current; waypointMarkersRef.current.forEach(m => scene.remove(m)); waypointMarkersRef.current = []; waypoints.forEach((wp, idx) => { const x = wp.x * 0.1; const z = wp.y * 0.1; const isFirst = idx === 0; const geo = new THREE.OctahedronGeometry(1.2, 0); const mat = new THREE.MeshBasicMaterial({ color: isFirst ? 0xf0a030 : 0x22d3ee, transparent: true, opacity: 0.6, wireframe: true, }); const marker = new THREE.Mesh(geo, mat); marker.position.set(x, 2, z); const group = new THREE.Group(); group.add(marker); const lbl = TH.createLabel(wp.label, isFirst ? '#f0a030' : '#22d3ee', 14); lbl.position.y = 5; group.add(lbl); scene.add(group); waypointMarkersRef.current.push(group); }); }, [waypoints]); // Grid toggle useEffect(() => { if (gridRef.current) gridRef.current.visible = showGrid; }, [showGrid]); // Navigate to entity — used by overview rows, action buttons, and waypoint panel const navigateToEntity = useCallback((ent) => { const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type }; waypointsRef.current = [wp]; setWaypoints([wp]); targetRef.current = wp; setCurrentTarget(wp); const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 }; setMoving(true); setSelectedEntity(ent); }, []); // Add entity as next waypoint (queued, doesn't interrupt current move) const addWaypointEntity = useCallback((ent) => { const wp = { id: Date.now(), x: ent.x, y: ent.y, label: ent.name, type: ent.type }; waypointsRef.current = [...waypointsRef.current, wp]; setWaypoints([...waypointsRef.current]); if (!moveRef.current && waypointsRef.current.length === 1) { targetRef.current = wp; setCurrentTarget(wp); const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: wp.x, ty: wp.y, progress: 0 }; setMoving(true); } }, []); const clearWaypoints = useCallback(() => { waypointsRef.current = []; targetRef.current = null; moveRef.current = null; setWaypoints([]); setCurrentTarget(null); setMoving(false); }, []); const removeWaypoint = useCallback((id) => { waypointsRef.current = waypointsRef.current.filter(w => w.id !== id); setWaypoints([...waypointsRef.current]); if (targetRef.current?.id === id) { moveRef.current = null; const next = waypointsRef.current[0] || null; targetRef.current = next; setCurrentTarget(next); if (next) { const shipPos2d = { x: shipGroupRef.current.position.x * 10, y: shipGroupRef.current.position.z * 10 }; moveRef.current = { sx: shipPos2d.x, sy: shipPos2d.y, tx: next.x, ty: next.y, progress: 0 }; } else setMoving(false); } }, []); const toggleModule = useCallback((slotType, index) => { setModules(prev => { const row = [...prev[slotType]]; row[index] = { ...row[index], active: !row[index].active }; return { ...prev, [slotType]: row }; }); }, []); const handleChatSend = useCallback(() => { if (!chatInput.trim()) return; setChatMessages(prev => [...prev, { sender: 'You', body: chatInput, time: serverTime.slice(0, 5) }]); setChatInput(''); }, [chatInput, serverTime]); const formatCoord = (v) => v.toFixed(0); const entityColor = (type) => { switch (type) { case 'hostile': return 'var(--red)'; case 'asteroid': return 'var(--purple)'; case 'station': return 'var(--cyan)'; case 'player': return 'var(--green)'; default: return 'var(--muted)'; } }; const entityIcon = (type) => { switch (type) { case 'hostile': return '✸'; case 'asteroid': return '◉'; case 'station': return '⬡'; case 'player': return '◈'; default: return '○'; } }; const sortedEntities = [...entities].sort((a, b) => (a.distance || 0) - (b.distance || 0)); const activeModuleCount = Object.values(modules).flat().filter(m => m && m.active).length; return (
{/* 3D Viewport — visual only, no click interaction */}
{/* HUD Overlay */}
{/* ===== TOP BAR ===== */}
Sol 1.0 HIGH SEC
Ship {shipStatus?.name || 'USS Enterprise'}
SPD {shipSpeed.toFixed(0)}m/s
HDG {shipHeading.toFixed(0)}°
POS {formatCoord(shipPos.x)}, {formatCoord(shipPos.y)}
₢125,740
CONNECTED
{serverTime} {moving && currentTarget && ( <>
● EN ROUTE → {currentTarget.label} )}
{/* ===== MIDDLE ===== */}
{/* ===== LEFT PANEL — Ship Status + Speed + Waypoints ===== */}
{/* Ship Health */}
Ship Status
{shipStatus?.name || 'USS Enterprise'} {shipStatus?.class?.split(' ')[0] || 'Venture'}
{[ { label: 'SHIELD', value: shipStatus?.shields ?? 100, color: '#22d3ee', cls: 'shield' }, { label: 'ARMOR', value: shipStatus?.armor ?? 100, color: '#f0a030', cls: 'armor' }, { label: 'HULL', value: shipStatus?.hull ?? 100, color: '#22c55e', cls: 'hull' }, { label: 'CAP', value: shipStatus?.capacitor ?? 85, color: '#a78bfa', cls: 'cap' }, ].map(bar => (
{bar.label}
{bar.value}%
))}
{/* Propulsion */}
Propulsion
{moving ? shipSpeed.toFixed(0) : '—'} m/s
{moving ? '● SUBLIGHT ACTIVE' : '○ IDLE'}
{/* Waypoints */}
Waypoints {waypoints.length > 0 && ( CLEAR )}
{waypoints.length === 0 ? (
Select a target from Overview
or use action buttons to navigate
) : (
{waypoints.map((wp, idx) => { const dist = Math.sqrt((wp.x - shipPos.x) ** 2 + (wp.y - shipPos.y) ** 2); return (
{idx + 1}.
{wp.label}
{dist.toFixed(0)} km
{ e.stopPropagation(); removeWaypoint(wp.id); }}>×
); })} {moving && moveProgress > 0 && (
TRIP {(moveProgress * 100).toFixed(0)}%
)}
)}
{/* CENTER — crosshair + nav status */}
{/* Subtle grid lines */}
{/* Crosshair */}
{/* Navigation status toast */} {moving && currentTarget && (
● EN ROUTE → {currentTarget.label}
)} {!moving && waypoints.length === 0 && (
○ IDLE — Select a target from Overview
)} {/* Grid toggle */}
{/* ===== RIGHT PANEL — Overview ===== */}
Overview {sortedEntities.length} entities
{sortedEntities.map(ent => { const col = entityColor(ent.type); const ico = entityIcon(ent.type); const dist = Math.sqrt((ent.x - shipPos.x) ** 2 + (ent.y - shipPos.y) ** 2); return ( setSelectedEntity(ent)} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={(e) => e.currentTarget.style.background = selectedEntity?.id === ent.id ? 'var(--accent-bg)' : 'transparent'} > ); })}
Name Dist
{ico} {ent.name} {dist.toFixed(0)} km
{/* Selected Entity Detail */} {selectedEntity && (() => { const col = entityColor(selectedEntity.type); const dist = Math.sqrt((selectedEntity.x - shipPos.x) ** 2 + (selectedEntity.y - shipPos.y) ** 2); return (
{selectedEntity.name}
{selectedEntity.type.toUpperCase()} · {dist.toFixed(0)} km
{selectedEntity.type === 'asteroid' && ( <> )} {selectedEntity.type === 'hostile' && ( <> )} {selectedEntity.type === 'station' && ( <> )} {selectedEntity.type === 'player' && ( <> )}
); })()}
{/* ===== BOTTOM BAR ===== */}
{/* Module Rack */}
Modules {activeModuleCount} active
{['high', 'med', 'low'].map(slotType => (
{slotType === 'high' ? 'HIGH' : slotType === 'med' ? 'MED' : 'LOW'} {modules[slotType].map((mod, i) => ( mod.id ? (
toggleModule(slotType, i)} title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`} style={{ width: '48px', height: '40px', borderRadius: '6px', border: `1px solid ${mod.active ? 'var(--accent-border)' : 'var(--border)'}`, background: mod.active ? 'var(--accent-bg)' : 'var(--surface)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.15s ease', position: 'relative', overflow: 'hidden', gap: '2px', }} > {mod.icon} {mod.name} {mod.active &&
}
) : (
) ))}
))}
{/* Cargo */}
Cargo Hold
12,400 / 25,000 m³ 50%
{[ { name: 'Veldspar', qty: '8,500' }, { name: 'Scordite', qty: '2,300' }, { name: 'Kernite', qty: '400' }, { name: 'Pyroxeres', qty: '1,200' }, ].map((item, i) => (
{item.name} ×{item.qty}
))}
{/* Chat */}
{['local', 'corp', 'trade'].map(tab => ( ))}
{chatMessages.slice(-6).map((msg, i) => (
{msg.sender} {msg.body} {msg.time}
))}
setChatInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleChatSend()} placeholder="Send message..." style={{ flex: 1, background: 'var(--bg-subtle)', border: 'none', padding: '6px 10px', fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg)', outline: 'none' }} />
{/* Module cycle animation keyframes */}
); } window.GDD.ShipMovementDemo = ShipMovementDemo;