851 lines
55 KiB
JavaScript
851 lines
55 KiB
JavaScript
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 m³</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;
|