Initial commit

This commit is contained in:
2026-05-25 13:00:20 -04:00
commit e14e43da42
49 changed files with 26892 additions and 0 deletions

850
js/demos/movement.js Normal file
View File

@@ -0,0 +1,850 @@
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;