Files
Space-Game/js/demos/movement.js
2026-05-25 13:00:20 -04:00

851 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div style={{ position: 'relative', width: '100%', height: '100vh', background: '#040810', overflow: 'hidden', cursor: 'crosshair' }}>
{/* 3D Viewport — visual only, no click interaction */}
<div ref={containerRef} style={{ position: 'absolute', inset: 0, zIndex: 0 }} />
{/* HUD Overlay */}
<div style={{ position: 'absolute', inset: 0, zIndex: 1, display: 'flex', flexDirection: 'column', pointerEvents: 'none' }}>
{/* ===== TOP BAR ===== */}
<div style={{
height: '42px', display: 'flex', alignItems: 'center', padding: '0 16px', gap: '16px',
background: 'linear-gradient(180deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%)',
fontFamily: 'var(--font-mono)', fontSize: '12px', pointerEvents: 'auto', flexShrink: 0,
}}>
<button onClick={() => window.GDD.router.navigate('overview')} style={{ display: 'flex', alignItems: 'center', gap: '6px', background: 'rgba(255,255,255,0.06)', border: '1px solid var(--border)', borderRadius: '6px', padding: '3px 10px', color: 'var(--fg-dim)', fontSize: '11px', fontFamily: 'var(--font-mono)', cursor: 'pointer', textTransform: 'uppercase', letterSpacing: '0.06em', transition: 'background 0.15s, color 0.15s' }} onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.12)'; e.currentTarget.style.color = 'var(--fg-bright)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; e.currentTarget.style.color = 'var(--fg-dim)'; }}> Docs</button>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ fontFamily: 'var(--font-display)', fontSize: '15px', fontWeight: 600, color: 'var(--fg-bright)', letterSpacing: '-0.01em' }}>Sol</span>
<span style={{ fontSize: '11px', padding: '1px 8px', borderRadius: 'var(--radius-pill)', fontWeight: 600, background: 'var(--green-bg)', color: 'var(--green)', border: '1px solid rgba(34,197,94,0.3)' }}>1.0 HIGH SEC</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Ship</span>
<span style={{ color: 'var(--fg-dim)' }}>{shipStatus?.name || 'USS Enterprise'}</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>SPD</span>
<span style={{ color: moving ? 'var(--cyan)' : 'var(--fg-dim)', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{shipSpeed.toFixed(0)}<span style={{ fontSize: '9px', color: 'var(--muted)', marginLeft: '2px' }}>m/s</span></span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>HDG</span>
<span style={{ color: 'var(--fg-bright)', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{shipHeading.toFixed(0)}°</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>POS</span>
<span style={{ color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>{formatCoord(shipPos.x)}, {formatCoord(shipPos.y)}</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>125,740</span>
<div style={{ flex: 1 }} />
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 6px var(--green)' }} />
<span style={{ color: 'var(--green)', fontSize: '10px' }}>CONNECTED</span>
</span>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>{serverTime}</span>
{moving && currentTarget && (
<>
<div style={{ width: 1, height: 20, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontSize: '11px', fontWeight: 600 }}> EN ROUTE {currentTarget.label}</span>
</>
)}
</div>
{/* ===== MIDDLE ===== */}
<div style={{ flex: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
{/* ===== LEFT PANEL — Ship Status + Speed + Waypoints ===== */}
<div style={{ width: '220px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0, overflowY: 'auto' }}>
{/* Ship Health */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Ship Status
</div>
<div style={{ padding: '10px 12px' }}>
<div style={{ marginBottom: '10px' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '2px' }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: '14px', fontWeight: 600, color: 'var(--fg-bright)' }}>{shipStatus?.name || 'USS Enterprise'}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{shipStatus?.class?.split(' ')[0] || 'Venture'}</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{[
{ 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 => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em', color: bar.color, width: '52px', flexShrink: 0 }}>{bar.label}</span>
<div style={{ flex: 1, height: '6px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: 'var(--radius-pill)', transition: 'width 0.6s cubic-bezier(0.23,1,0.32,1)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right', flexShrink: 0 }}>{bar.value}%</span>
</div>
))}
</div>
</div>
</div>
{/* Propulsion */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--cyan)' }} />Propulsion
</div>
<div style={{ padding: '10px 12px' }}>
<div style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: '22px', fontWeight: 700, color: moving ? 'var(--fg-bright)' : 'var(--muted)', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>
{moving ? shipSpeed.toFixed(0) : '—'}
<span style={{ fontSize: '10px', fontWeight: 400, color: 'var(--muted)', letterSpacing: '0.06em', marginLeft: '4px' }}>m/s</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', justifyContent: 'center', marginTop: '6px' }}>
<button style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '14px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)' }}></button>
<div style={{ flex: 1, height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${moving ? Math.min(shipSpeed / 250 * 100, 100) : 0}%`, background: 'var(--cyan)', borderRadius: 'var(--radius-pill)', transition: 'width 0.4s cubic-bezier(0.23,1,0.32,1)' }} />
</div>
<button style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '14px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)' }}>+</button>
</div>
<div style={{ textAlign: 'center', padding: '6px', marginTop: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.1em', borderRadius: '6px', color: moving ? 'var(--cyan)' : 'var(--muted)', background: moving ? 'var(--cyan-bg)' : 'transparent' }}>
{moving ? '● SUBLIGHT ACTIVE' : '○ IDLE'}
</div>
</div>
</div>
{/* Waypoints */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1, minHeight: '120px', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Waypoints
{waypoints.length > 0 && (
<span style={{ marginLeft: 'auto', fontSize: '9px', color: 'var(--muted)', cursor: 'pointer' }} onClick={clearWaypoints}>CLEAR</span>
)}
</div>
<div style={{ padding: '8px 12px', flex: 1, overflowY: 'auto' }}>
{waypoints.length === 0 ? (
<div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--muted)' }}>
<div style={{ fontSize: '16px', marginBottom: '6px', opacity: 0.5 }}></div>
<div style={{ fontSize: '11px' }}>Select a target from Overview</div>
<div style={{ fontSize: '10px', marginTop: '4px', color: 'var(--muted)' }}>or use action buttons to navigate</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{waypoints.map((wp, idx) => {
const dist = Math.sqrt((wp.x - shipPos.x) ** 2 + (wp.y - shipPos.y) ** 2);
return (
<div key={wp.id} style={{
display: 'flex', alignItems: 'center', gap: '8px',
padding: '6px 8px',
background: idx === 0 ? 'var(--accent-bg)' : 'var(--surface-raised)',
border: `1px solid ${idx === 0 ? 'var(--accent-border)' : 'var(--border)'}`,
borderRadius: '6px', fontSize: '11px',
}}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: idx === 0 ? 'var(--accent)' : 'var(--cyan)', minWidth: '18px' }}>{idx + 1}.</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: idx === 0 ? 'var(--fg-bright)' : 'var(--fg-dim)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{wp.label}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: 'var(--muted)' }}>{dist.toFixed(0)} km</div>
</div>
<span style={{ color: 'var(--muted)', cursor: 'pointer', fontSize: '12px', padding: '2px', lineHeight: 1 }} onClick={(e) => { e.stopPropagation(); removeWaypoint(wp.id); }}>×</span>
</div>
);
})}
{moving && moveProgress > 0 && (
<div style={{ marginTop: '4px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3px' }}>
<span style={{ fontSize: '9px', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>TRIP</span>
<span style={{ fontSize: '9px', color: 'var(--accent)', fontFamily: 'var(--font-mono)' }}>{(moveProgress * 100).toFixed(0)}%</span>
</div>
<div style={{ height: '3px', background: 'var(--surface-raised)', borderRadius: '2px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${moveProgress * 100}%`, background: 'var(--accent)', borderRadius: '2px', transition: 'width 0.1s linear' }} />
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* CENTER — crosshair + nav status */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', position: 'relative' }}>
{/* Subtle grid lines */}
<div style={{ position: 'absolute', top: '42px', bottom: 0, left: '50%', width: '1px', background: 'linear-gradient(180deg, rgba(34,211,238,0.06) 0%, transparent 40%, transparent 60%, rgba(34,211,238,0.06) 100%)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', left: 0, right: 0, top: '50%', height: '1px', background: 'linear-gradient(90deg, rgba(34,211,238,0.06) 0%, transparent 30%, transparent 70%, rgba(34,211,238,0.06) 100%)', pointerEvents: 'none' }} />
{/* Crosshair */}
<div style={{ width: '60px', height: '60px', position: 'relative', opacity: 0.35 }}>
<div style={{ width: '1px', height: '20px', background: 'var(--fg-dim)', position: 'absolute', left: '50%', top: 0, transform: 'translateX(-50%)' }} />
<div style={{ width: '1px', height: '20px', background: 'var(--fg-dim)', position: 'absolute', left: '50%', bottom: 0, transform: 'translateX(-50%)' }} />
<div style={{ height: '1px', width: '20px', background: 'var(--fg-dim)', position: 'absolute', top: '50%', left: 0, transform: 'translateY(-50%)' }} />
<div style={{ height: '1px', width: '20px', background: 'var(--fg-dim)', position: 'absolute', top: '50%', right: 0, transform: 'translateY(-50%)' }} />
<div style={{ width: '40px', height: '40px', border: '1px solid rgba(212,220,232,0.25)', borderRadius: '50%', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
</div>
{/* Navigation status toast */}
{moving && currentTarget && (
<div style={{
position: 'absolute', top: '16px', left: '50%', transform: 'translateX(-50%)',
padding: '6px 18px', borderRadius: 'var(--radius-pill)',
background: 'rgba(240,160,48,0.12)', border: '1px solid rgba(240,160,48,0.25)',
fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--accent)',
letterSpacing: '0.03em', pointerEvents: 'none',
}}>
EN ROUTE {currentTarget.label}
</div>
)}
{!moving && waypoints.length === 0 && (
<div style={{
position: 'absolute', top: '16px', left: '50%', transform: 'translateX(-50%)',
padding: '6px 18px', borderRadius: 'var(--radius-pill)',
background: 'rgba(15,22,35,0.7)', border: '1px solid var(--border)',
fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--muted)',
letterSpacing: '0.03em', pointerEvents: 'none', backdropFilter: 'blur(6px)',
}}>
IDLE Select a target from Overview
</div>
)}
{/* Grid toggle */}
<div style={{ position: 'absolute', bottom: '160px', left: '50%', transform: 'translateX(-50%)', pointerEvents: 'auto' }}>
<button
style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', padding: '4px 14px', background: showGrid ? 'rgba(34,211,238,0.12)' : 'rgba(15,22,35,0.8)', border: `1px solid ${showGrid ? 'rgba(34,211,238,0.25)' : 'var(--border)'}`, borderRadius: 'var(--radius-pill)', color: showGrid ? 'var(--cyan)' : 'var(--muted)', cursor: 'pointer', backdropFilter: 'blur(6px)', transition: 'all 0.15s' }}
onClick={() => setShowGrid(!showGrid)}
>
{showGrid ? 'Grid On' : 'Grid Off'}
</button>
</div>
</div>
{/* ===== RIGHT PANEL — Overview ===== */}
<div style={{ width: '280px', display: 'flex', flexDirection: 'column', gap: '6px', padding: '8px', pointerEvents: 'auto', flexShrink: 0 }}>
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--cyan)' }} />Overview
<span style={{ marginLeft: 'auto', color: 'var(--fg-dim)', fontSize: '10px' }}>{sortedEntities.length} entities</span>
</div>
<div style={{ flex: 1, overflowY: 'auto', maxHeight: '340px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}></th>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'left', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}>Name</th>
<th style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)', textAlign: 'right', padding: '4px 8px', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }}>Dist</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={ent.id}
style={{ cursor: 'pointer' }}
onClick={() => 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'}
>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '10px', textAlign: 'center', color: col, width: '16px' }}>{ico}</td>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '11px', color: selectedEntity?.id === ent.id ? 'var(--fg-bright)' : 'var(--fg)', background: selectedEntity?.id === ent.id ? 'var(--accent-bg)' : 'transparent' }}>{ent.name}</td>
<td style={{ padding: '5px 8px', borderBottom: '1px solid rgba(28,42,63,0.4)', fontFamily: 'var(--font-mono)', fontSize: '11px', color: col, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{dist.toFixed(0)} km</td>
<td style={{ padding: '5px 6px', borderBottom: '1px solid rgba(28,42,63,0.4)', width: '24px', textAlign: 'center' }}>
<button
title="Navigate to target"
onClick={(e) => { e.stopPropagation(); navigateToEntity(ent); }}
style={{
background: 'none', border: '1px solid var(--accent-border)', borderRadius: '3px',
color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '11px',
cursor: 'pointer', padding: '1px 4px', lineHeight: 1,
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--accent-bg)'; e.currentTarget.style.color = 'var(--fg-bright)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'none'; e.currentTarget.style.color = 'var(--accent)'; }}
></button>
</td>
<td style={{ padding: '5px 6px', borderBottom: '1px solid rgba(28,42,63,0.4)', width: '24px', textAlign: 'center' }}>
<button
title="Add as waypoint"
onClick={(e) => { e.stopPropagation(); addWaypointEntity(ent); }}
style={{
background: 'none', border: '1px solid var(--border)', borderRadius: '3px',
color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '11px',
cursor: 'pointer', padding: '1px 4px', lineHeight: 1,
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.background = 'var(--surface-raised)'; e.currentTarget.style.color = 'var(--fg-dim)'; }}
onMouseLeave={e => { e.currentTarget.style.background = 'none'; e.currentTarget.style.color = 'var(--muted)'; }}
>+</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Selected Entity Detail */}
{selectedEntity && (() => {
const col = entityColor(selectedEntity.type);
const dist = Math.sqrt((selectedEntity.x - shipPos.x) ** 2 + (selectedEntity.y - shipPos.y) ** 2);
return (
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden' }}>
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: col }} />
<span style={{ color: col }}>{selectedEntity.name}</span>
</div>
<div style={{ padding: '8px 12px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', marginBottom: '8px' }}>
{selectedEntity.type.toUpperCase()} · {dist.toFixed(0)} km
</div>
<div style={{ display: 'flex', gap: '4px' }}>
{selectedEntity.type === 'asteroid' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--accent-border)', background: 'var(--accent-bg)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(240,160,48,0.25)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--accent-bg)'}>Approach</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Mine</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'hostile' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid rgba(239,68,68,0.3)', background: 'var(--red-bg)', color: 'var(--red)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(239,68,68,0.18)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--red-bg)'}>Approach</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid rgba(239,68,68,0.3)', background: 'var(--red-bg)', color: 'var(--red)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(239,68,68,0.18)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--red-bg)'}>Orbit 20km</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'station' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--accent-border)', background: 'var(--accent-bg)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'rgba(240,160,48,0.25)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--accent-bg)'}>Dock</button>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Approach</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
{selectedEntity.type === 'player' && (
<>
<button onClick={() => navigateToEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--fg-dim)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>Approach</button>
<button onClick={() => addWaypointEntity(selectedEntity)} style={{ padding: '3px 10px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--muted)', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer', transition: 'background 0.15s' }} onMouseEnter={e => e.currentTarget.style.background = 'var(--surface-raised)'} onMouseLeave={e => e.currentTarget.style.background = 'var(--surface)'}>+ Waypoint</button>
</>
)}
</div>
</div>
</div>
);
})()}
</div>
</div>
{/* ===== BOTTOM BAR ===== */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '6px', padding: '0 8px 8px', pointerEvents: 'auto', flexShrink: 0, background: 'linear-gradient(0deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.6) 80%, transparent 100%)' }}>
{/* Module Rack */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 1 }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Modules
<span style={{ marginLeft: 'auto', fontSize: '9px', color: 'var(--fg-dim)' }}>{activeModuleCount} active</span>
</div>
<div style={{ padding: '6px 12px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{['high', 'med', 'low'].map(slotType => (
<div key={slotType} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: slotType === 'high' ? 'var(--red)' : slotType === 'med' ? 'var(--cyan)' : 'var(--green)', width: '36px', flexShrink: 0 }}>
{slotType === 'high' ? 'HIGH' : slotType === 'med' ? 'MED' : 'LOW'}
</span>
{modules[slotType].map((mod, i) => (
mod.id ? (
<div key={mod.id}
onClick={() => 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',
}}
>
<span style={{ fontSize: '12px', lineHeight: 1 }}>{mod.icon}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: mod.active ? 'var(--accent)' : 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.04em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '44px' }}>{mod.name}</span>
{mod.active && <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '2px', background: 'var(--accent)', animation: 'module-cycle 3s linear infinite' }} />}
</div>
) : (
<div key={`empty-${i}`} style={{ width: '48px', height: '40px', borderRadius: '6px', border: '1px dashed var(--border)', cursor: 'default', opacity: 0.4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--muted)', fontSize: '10px' }}></span>
</div>
)
))}
</div>
))}
</div>
</div>
{/* Cargo */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', width: '200px', flexShrink: 0 }}>
<div style={{ padding: '6px 12px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '8px', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '4px', height: '4px', borderRadius: '50%', background: 'var(--accent)' }} />Cargo Hold
</div>
<div style={{ padding: '8px 12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--muted)' }}>12,400 / 25,000 </span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}>50%</span>
</div>
<div style={{ height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden', marginBottom: '6px' }}>
<div style={{ height: '100%', width: '50%', background: 'var(--accent)', borderRadius: 'var(--radius-pill)' }} />
</div>
{[
{ name: 'Veldspar', qty: '8,500' },
{ name: 'Scordite', qty: '2,300' },
{ name: 'Kernite', qty: '400' },
{ name: 'Pyroxeres', qty: '1,200' },
].map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', marginBottom: '2px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{item.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--accent)', fontVariantNumeric: 'tabular-nums' }}>×{item.qty}</span>
</div>
))}
</div>
</div>
{/* Chat */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', width: '300px', flexShrink: 0, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)' }}>
{['local', 'corp', 'trade'].map(tab => (
<button key={tab} onClick={() => setChatTab(tab)}
style={{
flex: 1, padding: '6px 12px',
fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em',
color: chatTab === tab ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer',
background: 'none', borderTop: 'none', borderLeft: 'none', borderRight: 'none',
borderBottom: chatTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
transition: 'all 0.15s',
}}
>{tab}</button>
))}
</div>
<div style={{ height: '80px', overflowY: 'auto', padding: '6px 10px', display: 'flex', flexDirection: 'column', gap: '3px' }}>
{chatMessages.slice(-6).map((msg, i) => (
<div key={i} style={{ fontSize: '11px', lineHeight: 1.4 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--cyan)', marginRight: '6px' }}>{msg.sender}</span>
<span style={{ color: 'var(--fg-dim)' }}>{msg.body}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: 'var(--muted)', marginLeft: '6px' }}>{msg.time}</span>
</div>
))}
</div>
<div style={{ display: 'flex', borderTop: '1px solid var(--border)' }}>
<input
value={chatInput}
onChange={(e) => 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' }}
/>
<button onClick={handleChatSend}
style={{ padding: '6px 12px', background: 'var(--accent-bg)', border: 'none', borderLeft: '1px solid var(--border)', color: 'var(--accent)', fontFamily: 'var(--font-mono)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer' }}
>Send</button>
</div>
</div>
</div>
</div>
{/* Module cycle animation keyframes */}
<style>{`
@keyframes module-cycle {
0% { transform: scaleX(0); transform-origin: left; }
100% { transform: scaleX(1); transform-origin: left; }
}
`}</style>
</div>
);
}
window.GDD.ShipMovementDemo = ShipMovementDemo;