- 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
498 lines
31 KiB
JavaScript
498 lines
31 KiB
JavaScript
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} m³</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;
|