Files
Space-Game/archive/legacy-static/js/demos/gamehud.js
francy51 316a44661b Restructure into pnpm monorepo with game shell, docs, and SpacetimeDB backend
- Restructure flat static prototype into pnpm workspace monorepo
- apps/game: playable shell with R3F 3D scene, HUD, SpacetimeDB connection
- apps/docs: design docs and prototypes
- apps/site: landing page
- packages/ui: shared Button and Panel primitives
- services/spacetimedb: backend module (9 tables, 11 reducers)
- Archive legacy static files to archive/legacy-static/
- Game loop: connect, undock, target, approach, dock, mine, sell
- Add pnpm-workspace.yaml, tsconfig.base.json, spacetime.json
2026-05-31 17:56:56 -04:00

498 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.GDD = window.GDD || {};
const { useState, useEffect, useRef, useCallback } = React;
const TH = window.GDD.THREE;
/* ===== Game HUD Demo — 3D space viewport inside HUD overlay ===== */
function GameHudDemo() {
const containerRef = useRef(null);
const sceneRef = useRef(null);
const animIdRef = useRef(null);
const [modules, setModules] = useState([
{ id: 'h1', name: '150mm Railgun', icon: '⊕', active: false, type: 'weapon', slot: 'high' },
{ id: 'h2', name: 'Missile Launcher', icon: '⊕', active: false, type: 'weapon', slot: 'high' },
{ id: 'h3', name: 'Mining Laser', icon: '⛏', active: false, type: 'mining', slot: 'high' },
{ id: 'm1', name: 'Shield Booster', icon: '◎', active: false, type: 'shield', slot: 'med' },
{ id: 'm2', name: 'Afterburner', icon: '»', active: false, type: 'propulsion', slot: 'med' },
{ id: 'm3', name: 'Warp Scram', icon: '◎', active: false, type: 'ewar', slot: 'med' },
{ id: 'l1', name: 'Armor Plate', icon: '◼', active: true, type: 'armor', slot: 'low' },
{ id: 'l2', name: 'Damage Control', icon: '↯', active: true, type: 'damage_mod', slot: 'low' },
{ id: 'l3', name: 'Cargo Expander', icon: '□', active: false, type: 'cargo', slot: 'low' },
]);
const [target] = useState({ name: 'Guristas Pirate', type: 'Frigate', locked: true, shields: 62, armor: 85, hull: 100, distance: 12400, bounty: 85000 });
const [cargo] = useState({ used: 340, total: 600, items: [{ name: 'Veldspar', qty: 120 }, { name: 'Scordite', qty: 80 }, { name: 'Pyroxeres', qty: 45 }, { name: 'Tritanium', qty: 200 }] });
const [chatState, setChatState] = useState({
activeTab: 'local',
messages: [
{ sender: 'CMDR Picard', body: ' Pirates in belt 3, be careful ', time: '14:23' },
{ sender: 'MinerBob', body: ' Anyone selling compressed ore? ', time: '14:21' },
{ sender: 'CMDR Worf', body: ' Target locked, engaging hostiles ', time: '14:19' },
{ sender: '[SYSTEM]', body: ' Guristas fleet detected in nearby system ', time: '14:15' },
{ sender: 'TraderAlice', body: ' Best buy orders at Jita IV — check market ', time: '14:12' },
],
});
const [ship, setShip] = useState({ shields: 100, armor: 92, hull: 100, capacitor: 78, speed: 0, maxSpeed: 420, name: 'USS ENTERPRISE', class: 'VENTURE-CLASS' });
const [entities] = useState([
{ id: 'e1', name: 'Asteroid Belt', type: 'asteroid', dist: '12 km' },
{ id: 'e2', name: 'Guristas Pirate', type: 'hostile', dist: '24 km' },
{ id: 'e3', name: 'CMDR Riker', type: 'friendly', dist: '38 km' },
{ id: 'e4', name: 'Jita IV Station', type: 'station', dist: '45 km' },
{ id: 'e5', name: 'Veldspar Rock', type: 'asteroid', dist: '8 km' },
{ id: 'e6', name: 'MinerBob', type: 'friendly', dist: '52 km' },
{ id: 'e7', name: 'Jump Gate', type: 'gate', dist: '120 km' },
{ id: 'e8', name: 'Scordite Deposit', type: 'asteroid', dist: '15 km' },
]);
const [overviewFilter, setOverviewFilter] = useState('all');
const [system] = useState({ name: 'Jita', security: 0.9 });
// Build 3D scene
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const scene = new THREE.Scene();
const w = container.clientWidth;
const h = container.clientHeight;
const camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 5000);
camera.position.set(0, 8, 25);
camera.lookAt(0, 0, -20);
const renderer = TH.createRenderer(container, { clearColor: 0x040810 });
renderer.setSize(w, h);
// Stars
const stars = TH.createStarField(4000, 3000);
scene.add(stars);
// Nebulae
TH.addNebula(scene, 0x22d3ee, [-30, 20, -100], 80);
TH.addNebula(scene, 0xa78bfa, [50, -10, -80], 60);
TH.addNebula(scene, 0xf0a030, [-60, 15, -120], 50);
// Lighting
TH.setupSpaceLighting(scene);
// Player ship (small, at bottom center of view)
const playerGroup = new THREE.Group();
const pMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.5);
pMesh.rotation.y = Math.PI / 2;
playerGroup.add(pMesh);
const pEngine = TH.createEngineGlow(0x22d3ee, 2, 8);
pEngine.position.set(0, 0, 5);
playerGroup.add(pEngine);
const pShield = TH.createShield(2.5, 0x22d3ee, 0.05);
playerGroup.add(pShield);
playerGroup.position.set(0, -1, 15);
scene.add(playerGroup);
// Enemy ship (in the distance)
const enemyGroup = new THREE.Group();
const eMesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.4);
eMesh.rotation.y = -Math.PI / 2;
enemyGroup.add(eMesh);
const eEngine = TH.createEngineGlow(0xef4444, 1.5, 6);
eEngine.position.set(0, 0, -4);
enemyGroup.add(eEngine);
const lockBrackets = TH.createLockBrackets(2, 0xf0a030);
enemyGroup.add(lockBrackets);
const eLabel = TH.createLabel('GURISTAS PIRATE', '#ef4444', 12);
eLabel.position.y = 4;
enemyGroup.add(eLabel);
enemyGroup.position.set(5, 1, -20);
scene.add(enemyGroup);
// Asteroids
const asteroidPositions = [
{ x: -25, y: -3, z: -15, size: 3 },
{ x: -20, y: -2, z: -10, size: 2 },
{ x: -30, y: -4, z: -20, size: 2.5 },
{ x: 30, y: -3, z: -18, size: 2 },
{ x: 35, y: -2, z: -12, size: 1.5 },
{ x: -15, y: -5, z: -30, size: 3.5 },
{ x: 20, y: -4, z: -35, size: 2 },
];
asteroidPositions.forEach(ap => {
const ast = TH.createAsteroid(ap.size);
ast.position.set(ap.x, ap.y, ap.z);
ast.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
scene.add(ast);
});
// Station (far right)
const station = TH.createStation(3, 0x22d3ee);
station.position.set(40, 2, -40);
scene.add(station);
const stnLabel = TH.createLabel('JITA IV STN', '#22d3ee', 12);
stnLabel.position.set(40, 8, -40);
scene.add(stnLabel);
// Targeting line (dashed)
const linePoints = [playerGroup.position, enemyGroup.position];
const lineGeo = new THREE.BufferGeometry().setFromPoints(linePoints);
const lineMat = new THREE.LineDashedMaterial({ color: 0xf0a030, dashSize: 1, gapSize: 0.8, transparent: true, opacity: 0.2 });
const targetLine = new THREE.Line(lineGeo, lineMat);
targetLine.computeLineDistances();
scene.add(targetLine);
sceneRef.current = { scene, camera, renderer, stars, playerGroup, enemyGroup, pEngine, eEngine, lockBrackets, targetLine };
// Animation
const clock = new THREE.Clock();
const animate = () => {
animIdRef.current = requestAnimationFrame(animate);
const t = clock.getElapsedTime();
// Star drift
stars.rotation.y = t * 0.002;
stars.rotation.x = t * 0.001;
// Player ship idle bob
playerGroup.position.y = -1 + Math.sin(t * 1.5) * 0.2;
playerGroup.rotation.z = Math.sin(t * 0.3) * 0.02;
// Enemy ship slight movement
enemyGroup.position.x = 5 + Math.sin(t * 1.2) * 1;
enemyGroup.position.y = 1 + Math.cos(t * 0.8) * 0.5;
enemyGroup.rotation.z = Math.sin(t * 0.4) * 0.03;
// Lock brackets rotate
lockBrackets.rotation.y = t * 0.3;
// Update target line
const positions = targetLine.geometry.attributes.position;
positions.setXYZ(0, playerGroup.position.x, playerGroup.position.y, playerGroup.position.z);
positions.setXYZ(1, enemyGroup.position.x, enemyGroup.position.y, enemyGroup.position.z);
positions.needsUpdate = true;
targetLine.computeLineDistances();
// Engine glow pulse
pEngine.intensity = 2 + Math.sin(t * 3) * 0.5;
eEngine.intensity = 1 + Math.sin(t * 2.5) * 0.3;
// Shield shimmer
pShield.material.opacity = 0.04 + Math.sin(t * 2) * 0.02;
renderer.render(scene, camera);
};
animate();
const onResize = () => {
const w2 = container.clientWidth;
const h2 = container.clientHeight;
renderer.setSize(w2, h2);
camera.aspect = w2 / h2;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', onResize);
return () => {
if (animIdRef.current) cancelAnimationFrame(animIdRef.current);
window.removeEventListener('resize', onResize);
};
}, []);
const toggleModule = useCallback((modId) => {
setModules(prev => prev.map(m => m.id === modId ? { ...m, active: !m.active } : m));
}, []);
// Simulate capacitor tick
useEffect(() => {
const interval = setInterval(() => {
setShip(prev => {
const activeCount = modules.filter(m => m.active).length;
return { ...prev, capacitor: Math.max(0, Math.min(100, prev.capacitor - activeCount * 0.3 + 0.8)), speed: modules.find(m => m.id === 'm2' && m.active) ? 280 : 0 };
});
}, 1000);
return () => clearInterval(interval);
}, [modules]);
const filteredEntities = overviewFilter === 'all' ? entities : entities.filter(e => e.type === overviewFilter);
const activeModules = modules.filter(m => m.active).length;
return (
<div className="content-inner">
<h1 style={{ marginBottom: '8px' }}>Game HUD Live Concept (3D)</h1>
<p style={{ color: 'var(--fg-dim)', fontSize: '0.9rem' }}>
The in-game HUD with a 3D WebGL space viewport. All panels overlay the Three.js scene ship health, modules, overview, target info, cargo, and chat.
</p>
{/* Full HUD mockup */}
<div style={{
position: 'relative', width: '100%', height: '680px',
background: '#040810', borderRadius: 'var(--radius-lg)',
border: '1px solid var(--border)', overflow: 'hidden', marginTop: 'var(--sp-5)',
}}>
{/* 3D Canvas */}
<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: '38px', display: 'flex', alignItems: 'center', padding: '0 14px', gap: '16px',
background: 'linear-gradient(180deg, rgba(8,12,20,0.92) 0%, rgba(8,12,20,0.5) 80%, transparent 100%)',
fontFamily: 'var(--font-mono)', fontSize: '11px', pointerEvents: 'auto', flexShrink: 0,
}}>
<a
href="#overview"
style={{
display: 'flex', alignItems: 'center', gap: '5px',
padding: '2px 10px 2px 6px', borderRadius: 'var(--radius-pill)',
background: 'rgba(15,22,35,0.9)', border: '1px solid var(--border)',
color: 'var(--fg-dim)', fontSize: '10px', fontFamily: 'var(--font-mono)',
textDecoration: 'none', cursor: 'pointer',
transition: 'color 0.15s, border-color 0.15s',
}}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--fg-bright)'; e.currentTarget.style.borderColor = 'var(--accent)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--fg-dim)'; e.currentTarget.style.borderColor = 'var(--border)'; }}
title="Back to Game Docs"
>
<span style={{ fontSize: '12px' }}></span>
<span>DOCS</span>
</a>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ fontSize: '13px', fontWeight: 600, color: 'var(--fg-bright)', fontFamily: 'var(--font-display)' }}>{system.name}</span>
<span style={{ fontSize: '10px', padding: '1px 7px', borderRadius: 'var(--radius-pill)', fontWeight: 600, background: 'var(--green-bg)', color: 'var(--green)', border: '1px solid rgba(34,197,94,0.3)' }}>
{system.security.toFixed(1)}
</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--muted)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.06em' }}>Ship</span>
<span style={{ color: 'var(--fg-dim)' }}>{ship.name}</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>125,000</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 5px var(--green)' }} />
<span style={{ color: 'var(--fg-dim)' }}>TQ Online</span>
</span>
<span style={{ marginLeft: 'auto', color: 'var(--muted)', fontSize: '10px' }}>14:23 UTC</span>
<div style={{ width: 1, height: 18, background: 'var(--border-light)' }} />
<span style={{ color: 'var(--accent)', fontSize: '9px', fontWeight: 600 }}>3D</span>
</div>
{/* MIDDLE */}
<div style={{ flex: 1, display: 'flex', minHeight: 0, position: 'relative' }}>
{/* LEFT — Ship Panel */}
<div style={{ width: '200px', 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' }}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Ship Status
</div>
<div style={{ padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: '6px' }}>
{[
{ label: 'SH', value: ship.shields, color: '#22d3ee' },
{ label: 'AR', value: ship.armor, color: '#f0a030' },
{ label: 'HU', value: ship.hull, color: '#22c55e' },
{ label: 'CA', value: ship.capacitor, color: '#a78bfa' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '9px', color: bar.color, width: '20px' }}>{bar.label}</span>
<div style={{ flex: 1, height: '5px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.label === 'CA' ? (bar.value > 30 ? 'linear-gradient(90deg, #6366f1, #a78bfa)' : 'linear-gradient(90deg, #dc2626, #ef4444)') : `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '10px', color: 'var(--fg-dim)', width: '28px', textAlign: 'right' }}>{bar.label === 'CA' ? Math.round(bar.value) : bar.value}%</span>
</div>
))}
</div>
</div>
{/* Speed */}
<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 10px', textAlign: 'center' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '20px', fontWeight: 700, color: ship.speed > 0 ? 'var(--fg-bright)' : 'var(--muted)', letterSpacing: '-0.02em' }}>
{ship.speed}<span style={{ fontSize: '9px', fontWeight: 400, color: 'var(--muted)', marginLeft: '3px' }}>m/s</span>
</div>
<div style={{ height: '3px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden', marginTop: '6px' }}>
<div style={{ height: '100%', width: `${(ship.speed / ship.maxSpeed) * 100}%`, background: 'var(--cyan)', borderRadius: 'var(--radius-pill)', transition: 'width 0.4s' }} />
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: '4px', marginTop: '6px' }}>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}></button>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}></button>
<button style={{ width: '24px', height: '24px', borderRadius: '4px', border: '1px solid var(--border)', background: 'var(--surface-raised)', color: 'var(--fg-dim)', fontSize: '12px', cursor: 'pointer' }}>+</button>
</div>
</div>
</div>
</div>
<div style={{ flex: 1, position: 'relative' }} />
{/* RIGHT — Overview */}
<div style={{ width: '210px', 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: '6px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--cyan)' }} />Overview
<span style={{ marginLeft: 'auto', fontSize: '8px' }}>{filteredEntities.length} items</span>
</div>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)', padding: '0 4px' }}>
{['all', 'hostile', 'asteroid', 'friendly'].map(f => (
<button key={f} style={{ flex: 1, padding: '3px 0', fontSize: '8px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', background: 'none', border: 'none', borderBottom: overviewFilter === f ? '1.5px solid var(--accent)' : '1.5px solid transparent', color: overviewFilter === f ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer' }} onClick={() => setOverviewFilter(f)}>
{f === 'all' ? 'All' : f === 'hostile' ? 'Hostile' : f === 'asteroid' ? 'Rock' : 'Ally'}
</button>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '2px' }}>
{filteredEntities.map(ent => {
const col = ent.type === 'hostile' ? '#ef4444' : ent.type === 'asteroid' ? '#a78bfa' : ent.type === 'station' ? '#22d3ee' : ent.type === 'gate' ? '#5a6b82' : '#22c55e';
return (
<div key={ent.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 8px', fontSize: '10px', background: ent.type === 'hostile' ? 'rgba(239,68,68,0.06)' : 'transparent', borderRadius: '4px', cursor: 'pointer', borderLeft: `2px solid ${col}` }}>
<span style={{ flex: 1, color: col, fontSize: '9px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{ent.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--muted)' }}>{ent.dist}</span>
</div>
);
})}
</div>
</div>
</div>
</div>
{/* BOTTOM BAR */}
<div style={{ display: 'flex', gap: '6px', padding: '0 8px 8px', pointerEvents: 'auto', flexShrink: 0 }}>
{/* Modules */}
<div style={{ background: 'rgba(15,22,35,0.88)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', backdropFilter: 'blur(8px)', overflow: 'hidden', flex: 2 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Modules
<span style={{ marginLeft: 'auto', fontSize: '8px', color: 'var(--fg-dim)' }}>{activeModules} active</span>
</div>
<div style={{ padding: '6px 10px', 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: '8px', width: '28px', color: slotType === 'high' ? 'var(--red)' : slotType === 'med' ? 'var(--cyan)' : 'var(--green)' }}>
{slotType.toUpperCase()}
</span>
{modules.filter(m => m.slot === slotType).map(mod => (
<div key={mod.id} style={{ padding: '2px 6px', fontSize: '9px', borderRadius: '4px', background: mod.active ? 'var(--accent-bg)' : 'var(--surface-raised)', border: `1px solid ${mod.active ? 'var(--accent-border)' : 'var(--border)'}`, color: mod.active ? 'var(--fg-bright)' : 'var(--fg-dim)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '3px' }} onClick={() => toggleModule(mod.id)} title={`${mod.name} (${mod.active ? 'Active' : 'Inactive'})`}>
<span style={{ width: '5px', height: '5px', borderRadius: '50%', background: mod.active ? (mod.type === 'weapon' ? '#ef4444' : mod.type === 'shield' ? '#22d3ee' : mod.type === 'mining' ? '#a78bfa' : '#22c55e') : 'var(--border-light)', boxShadow: mod.active ? '0 0 4px currentColor' : 'none' }} />
<span style={{ fontSize: '8px' }}>{mod.name.length > 14 ? mod.name.slice(0, 12) + '…' : mod.name}</span>
</div>
))}
</div>
))}
</div>
</div>
{/* Target */}
<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.2 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--red)' }} />Target
</div>
<div style={{ padding: '6px 10px' }}>
<div style={{ fontSize: '10px', color: 'var(--red)', fontWeight: 600, marginBottom: '2px' }}>{target.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', marginBottom: '6px' }}>{target.type} · LOCKED</div>
{[
{ label: 'SH', value: target.shields, color: '#22d3ee' },
{ label: 'AR', value: target.armor, color: '#f0a030' },
{ label: 'HU', value: target.hull, color: '#22c55e' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: bar.color, width: '16px' }}>{bar.label}</span>
<div style={{ flex: 1, height: '4px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.color, borderRadius: 'var(--radius-pill)' }} />
</div>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: '8px', color: 'var(--fg-dim)', width: '22px', textAlign: 'right' }}>{bar.value}%</span>
</div>
))}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '4px', fontFamily: 'var(--font-mono)', fontSize: '8px' }}>
<span style={{ color: 'var(--cyan)' }}>{target.distance.toLocaleString()} km</span>
<span style={{ color: 'var(--accent)' }}>{target.bounty.toLocaleString()}</span>
</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', flex: 1 }}>
<div style={{ padding: '4px 10px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: '6px', fontFamily: 'var(--font-mono)', fontSize: '9px', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--muted)' }}>
<span style={{ width: '3px', height: '3px', borderRadius: '50%', background: 'var(--accent)' }} />Cargo
</div>
<div style={{ padding: '6px 10px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'var(--font-mono)', fontSize: '9px', marginBottom: '3px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{cargo.used}/{cargo.total} </span>
<span style={{ color: 'var(--accent)' }}>{Math.round(cargo.used / cargo.total * 100)}%</span>
</div>
<div style={{ height: '3px', background: 'rgba(255,255,255,0.04)', borderRadius: 'var(--radius-pill)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${(cargo.used / cargo.total) * 100}%`, background: 'var(--accent)', borderRadius: 'var(--radius-pill)' }} />
</div>
{cargo.items.map((item, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '9px', marginTop: '3px' }}>
<span style={{ color: 'var(--fg-dim)' }}>{item.name}</span>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--muted)' }}>×{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', flex: 1.2, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border)' }}>
{['local', 'corp', 'trade'].map(tab => (
<button key={tab} style={{ flex: 1, padding: '4px 0', fontSize: '8px', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', background: 'none', border: 'none', borderBottom: chatState.activeTab === tab ? '1.5px solid var(--accent)' : '1.5px solid transparent', color: chatState.activeTab === tab ? 'var(--accent)' : 'var(--muted)', cursor: 'pointer' }} onClick={() => setChatState(s => ({ ...s, activeTab: tab }))}>
{tab}
</button>
))}
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 8px' }}>
{chatState.messages.map((msg, i) => (
<div key={i} style={{ fontSize: '9px', marginBottom: '2px', lineHeight: 1.4 }}>
<span style={{ color: msg.sender.startsWith('[SYS') ? 'var(--accent)' : 'var(--cyan)', fontWeight: 600 }}>{msg.sender}</span>
<span style={{ color: 'var(--fg-dim)' }}>{msg.body}</span>
<span style={{ color: 'var(--muted)', fontSize: '7px', marginLeft: '4px' }}>{msg.time}</span>
</div>
))}
</div>
<div style={{ display: 'flex', gap: '3px', padding: '4px', borderTop: '1px solid var(--border)' }}>
<input style={{ flex: 1, padding: '3px 6px', fontSize: '9px', background: 'var(--surface-raised)', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--fg)', fontFamily: 'var(--font-mono)' }} placeholder="Send message..." />
<button style={{ padding: '3px 8px', fontSize: '8px', background: 'var(--accent)', border: 'none', borderRadius: '4px', color: 'var(--bg)', fontWeight: 600, cursor: 'pointer' }}>Send</button>
</div>
</div>
</div>
</div>
</div>
{/* Architecture notes */}
<div style={{ marginTop: 'var(--sp-6)' }}>
<div className="section-header">
<span className="section-num">HUD</span>
<h2 style={{ margin: 0 }}>HUD Panel Architecture 3D</h2>
</div>
<div className="grid-2" style={{ marginTop: 'var(--sp-4)' }}>
<div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
<h4 style={{ color: 'var(--accent)', marginBottom: 'var(--sp-3)' }}>3D Viewport</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg-bright)' }}>Three.js WebGL</strong> replaces 2D Canvas proper 3D ship meshes, asteroids, stations, star fields</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Depth and lighting</strong> ships and asteroids are lit by ambient + directional lights</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Particle star field</strong> 4000 point-based stars with subtle rotation for depth</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Engine glows</strong> point lights on each ship with pulsing intensity</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Lock brackets</strong> 3D wireframe targeting indicator that rotates around the target</li>
</ul>
</div>
<div className="card" style={{ borderLeft: '3px solid var(--cyan)' }}>
<h4 style={{ color: 'var(--cyan)', marginBottom: 'var(--sp-3)' }}>HUD Overlay Pattern</h4>
<ul style={{ color: 'var(--fg-dim)', fontSize: '0.85rem', margin: 0, paddingLeft: 'var(--sp-5)' }}>
<li><strong style={{ color: 'var(--fg-bright)' }}>CSS overlay</strong> HUD panels are positioned absolutely over the 3D canvas</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Glass morphism</strong> backdrop-filter blur + semi-transparent backgrounds</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Pointer events</strong> HUD panels capture clicks, center viewport passes through</li>
<li><strong style={{ color: 'var(--fg-bright)' }}>Performance</strong> Three.js renderer is separate from React DOM updates</li>
</ul>
</div>
</div>
</div>
<div className="callout callout-info" style={{ marginTop: 'var(--sp-6)' }}>
<strong>Rendering upgrade:</strong> The space viewport now uses Three.js with WebGL. Ships are 3D meshes with lighting, asteroids use icosahedron geometry with vertex perturbation, and the star field is a 4000-particle Points system. The HUD overlay panels remain identical React components.
</div>
</div>
);
}
window.GDD.GameHudDemo = GameHudDemo;