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 (
| Name | Dist | |||
|---|---|---|---|---|
| {ico} | {ent.name} | {dist.toFixed(0)} km |