window.GDD = window.GDD || {}; const { useState, useEffect, useRef, useCallback } = React; const TH = window.GDD.THREE; /* ═══════════════════════════════════════════════════════════════════ Combat System — FTL-style Reactor Power Management Immersive Game HUD — full 3D viewport with diegetic overlays ═══════════════════════════════════════════════════════════════════ */ const MAX_POWER = 8; const TICK_MS = 50; // ── 3D Projectile Pool ── function createProjectilePool3D(scene) { const pool = []; return { spawn(x, y, z, tx, ty, tz, color, dmg, type, subsystem) { let mesh; if (type === 'beam') { const dx = tx - x, dy = ty - y, dz = tz - z; const len = Math.sqrt(dx*dx + dy*dy + dz*dz); const geo = new THREE.CylinderGeometry(0.08, 0.08, len, 4); geo.rotateX(Math.PI / 2); const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }); mesh = new THREE.Mesh(geo, mat); mesh.position.set((x + tx) / 2, (y + ty) / 2, (z + tz) / 2); mesh.lookAt(tx, ty, tz); mesh.userData = { life: 8, maxLife: 8, type, dmg, subsystem, tx, ty, tz, color }; } else if (type === 'pulse') { const geo = new THREE.RingGeometry(1, 2, 24); const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); mesh = new THREE.Mesh(geo, mat); mesh.position.set(x, y, z); mesh.userData = { life: 20, maxLife: 20, type, dmg: 0, subsystem: 'none', scale: 1 }; } else { mesh = new THREE.Mesh( new THREE.SphereGeometry(type === 'missile' ? 0.2 : 0.12, 4, 4), new THREE.MeshBasicMaterial({ color }) ); const dx = tx - x, dy = ty - y, dz = tz - z; const dist = Math.sqrt(dx*dx + dy*dy + dz*dz); const speed = type === 'missile' ? 1.5 : 2.5; mesh.position.set(x, y, z); mesh.userData = { vx: (dx / dist) * speed, vy: (dy / dist) * speed, vz: (dz / dist) * speed, life: Math.ceil(dist / speed), maxLife: Math.ceil(dist / speed), type, dmg, subsystem, tx, ty, tz, color }; } scene.add(mesh); pool.push(mesh); }, tick() { for (let i = pool.length - 1; i >= 0; i--) { const m = pool[i]; const ud = m.userData; ud.life--; if (ud.type === 'beam' || ud.type === 'pulse') { m.material.opacity = Math.max(0, ud.life / ud.maxLife); if (ud.type === 'pulse') { ud.scale += 0.15; m.scale.setScalar(ud.scale); } } else { m.position.x += ud.vx; m.position.y += ud.vy; m.position.z += ud.vz; } if (ud.life <= 0) { scene.remove(m); m.geometry.dispose(); m.material.dispose(); pool.splice(i, 1); } } }, getArrived() { return pool.filter(m => m.userData.life <= 1); }, clear() { pool.forEach(m => { scene.remove(m); m.geometry.dispose(); m.material.dispose(); }); pool.length = 0; }, get all() { return pool; }, }; } // ── Impact flash pool ── function createImpactPool3D(scene) { const impacts = []; return { spawn(x, y, z, color, size) { const glow = TH.createGlowSprite(color, size || 3); glow.position.set(x, y, z); scene.add(glow); impacts.push({ mesh: glow, life: 10, maxLife: 10 }); }, tick() { for (let i = impacts.length - 1; i >= 0; i--) { impacts[i].life--; const progress = 1 - impacts[i].life / impacts[i].maxLife; impacts[i].mesh.material.opacity = (1 - progress) * 0.8; impacts[i].mesh.scale.setScalar((impacts[i].mesh.scale.x || 3) * (1 + progress * 0.5)); if (impacts[i].life <= 0) { scene.remove(impacts[i].mesh); impacts[i].mesh.material.map?.dispose(); impacts[i].mesh.material.dispose(); impacts.splice(i, 1); } } }, }; } function CombatDemo() { const containerRef = useRef(null); const sceneRef = useRef(null); const animIdRef = useRef(null); const tickRef = useRef(0); const projectilePoolRef = useRef(null); const impactPoolRef = useRef(null); // Ship 3D refs const playerShipRef = useRef(null); const enemyShipRef = useRef(null); const playerShieldRef = useRef(null); const enemyLockRef = useRef(null); const targetLineRef = useRef(null); const playerPosRef = useRef({ orbitAngle: 0, strafeTimer: 0, speed: 0 }); const enemyPosRef = useRef({ orbitAngle: Math.PI }); // ── Player State ── const [player, setPlayer] = useState({ shields: 100, armor: 100, hull: 100, energy: 100, maxEnergy: 100, speed: 0, maxSpeed: 420, name: 'USS ENTERPRISE', class: 'VENTURE-CLASS', }); const playerRef = useRef(player); useEffect(() => { playerRef.current = player; }, [player]); // ── Power Allocation ── const [power, setPower] = useState({ weapons: 3, shields: 2, engines: 2, aux: 1 }); const powerRef = useRef(power); useEffect(() => { powerRef.current = power; }, [power]); // ── Ship Modules ── const [modules, setModules] = useState([ { id: 'q', key: '1', name: 'Railgun', icon: '⊕', type: 'weapon', cd: 0, maxCd: 3, cost: 12, damage: 18, desc: 'Kinetic turret', color: '#ef4444' }, { id: 'w', key: '2', name: 'Shield Bst', icon: '◎', type: 'shield', cd: 0, maxCd: 8, cost: 20, damage: 0, desc: 'Burst recharge', color: '#22d3ee' }, { id: 'e', key: '3', name: 'EM Pulse', icon: '⟐', type: 'ewar', cd: 0, maxCd: 12, cost: 30, damage: 0, desc: 'Disrupt systems', color: '#a78bfa' }, { id: 'r', key: '4', name: 'Overload', icon: '⚡', type: 'reactor', cd: 0, maxCd: 30, cost: 45, damage: 0, desc: 'Push reactor', color: '#f0a030' }, { id: 'd', key: '5', name: 'Afterburn', icon: '»', type: 'engine', cd: 0, maxCd: 6, cost: 10, damage: 0, desc: 'Emergency thrust', color: '#22c55e' }, { id: 'f', key: '6', name: 'Hull Patch', icon: '✚', type: 'repair', cd: 0, maxCd: 15, cost: 25, damage: 0, desc: 'Nanite repair', color: '#fb923c' }, ]); const modulesRef = useRef(modules); useEffect(() => { modulesRef.current = modules; }, [modules]); const [playerBuffs, setPlayerBuffs] = useState([{ id: 'b1', name: 'Dmg Ctrl', icon: '↯', duration: -1, color: '#22c55e' }]); const [enemyBuffs, setEnemyBuffs] = useState([]); const [target, setTarget] = useState(null); const [subsystem, setSubsystem] = useState('hull'); const [enemy, setEnemy] = useState({ name: 'Guristas Pirata', class: 'Frigate', shields: 100, armor: 100, hull: 100, weapons: 100, engines: 100, locked: false, lockTimer: 0, lockTime: 3, }); const enemyRef = useRef(enemy); useEffect(() => { enemyRef.current = enemy; }, [enemy]); const targetRef = useRef(target); useEffect(() => { targetRef.current = target; }, [target]); const subsystemRef = useRef(subsystem); useEffect(() => { subsystemRef.current = subsystem; }, [subsystem]); const [overloaded, setOverloaded] = useState(false); const overloadRef = useRef(false); const [combatLog, setCombatLog] = useState([]); const logRef = useRef([]); const logScrollRef = useRef(null); const addLog = useCallback((msg, color) => { const time = new Date().toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); logRef.current = [...logRef.current.slice(-50), { time, msg, color }]; setCombatLog(logRef.current); }, []); const autoFireTimerRef = useRef(0); const enemyFireTimerRef = useRef(0); // ── Derived stats ── const totalPower = power.weapons + power.shields + power.engines + power.aux; const weaponMult = 1 + power.weapons * 0.20; const shieldRegen = power.shields * 1.2; const shieldAbsorb = 0.4 + power.shields * 0.08; const dodgeChance = power.engines * 4; const speedMult = 1 + power.engines * 0.25; const cdReduction = power.aux * 6; const energyRegen = 2 + power.aux * 2; // ── Build 3D scene ── useEffect(() => { const container = containerRef.current; if (!container) return; const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x040810, 0.0008); const camera = new THREE.PerspectiveCamera(55, container.clientWidth / container.clientHeight, 0.1, 3000); camera.position.set(0, 35, 60); camera.lookAt(0, 0, 0); const renderer = TH.createRenderer(container, { clearColor: 0x040810 }); TH.handleResize(renderer, camera, container); const stars = TH.createStarField(3000, 2000); scene.add(stars); TH.addNebula(scene, 0x22d3ee, [-100, 40, -200], 200); TH.addNebula(scene, 0xa78bfa, [80, -30, -150], 150); const grid = new THREE.GridHelper(300, 15, 0x0d1520, 0x0d1520); grid.material.transparent = true; grid.material.opacity = 0.12; scene.add(grid); TH.setupSpaceLighting(scene); // Player ship const playerGroup = new THREE.Group(); const pMesh = TH.createShipMesh(0xc8d6e5, 0xf0a030, 0.6); pMesh.rotation.y = Math.PI / 2; playerGroup.add(pMesh); const pEngine = TH.createEngineGlow(0x22d3ee, 3, 15); pEngine.position.set(0, 0, 6); playerGroup.add(pEngine); const pShield = TH.createShield(3, 0x22d3ee, 0.06); playerGroup.add(pShield); playerShieldRef.current = pShield; playerGroup.position.set(-15, 0, 0); scene.add(playerGroup); playerShipRef.current = playerGroup; // Enemy ship const enemyGroup = new THREE.Group(); const eMesh = TH.createShipMesh(0x7f1d1d, 0xef4444, 0.5); eMesh.rotation.y = Math.PI / 2; enemyGroup.add(eMesh); const eEngine = TH.createEngineGlow(0xef4444, 2, 10); eEngine.position.set(0, 0, 5); enemyGroup.add(eEngine); enemyGroup.position.set(15, 0, 0); scene.add(enemyGroup); enemyShipRef.current = enemyGroup; // Targeting line const lineGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 0, 0)]); const lineMat = new THREE.LineDashedMaterial({ color: 0xf0a030, dashSize: 1, gapSize: 0.5, transparent: true, opacity: 0.2 }); const targetLine = new THREE.Line(lineGeo, lineMat); targetLine.computeLineDistances(); targetLine.visible = false; scene.add(targetLine); targetLineRef.current = targetLine; // Lock brackets const lockBrackets = TH.createLockBrackets(3, 0xf0a030); lockBrackets.visible = false; scene.add(lockBrackets); enemyLockRef.current = lockBrackets; const projPool = createProjectilePool3D(scene); projectilePoolRef.current = projPool; const impPool = createImpactPool3D(scene); impactPoolRef.current = impPool; sceneRef.current = { scene, camera, renderer, stars, playerGroup, enemyGroup, pEngine, eEngine, lockBrackets }; const clock = new THREE.Clock(); const animate = () => { animIdRef.current = requestAnimationFrame(animate); const t = clock.getElapsedTime(); const pwr = powerRef.current; const eng = enemyRef.current; const tgt = targetRef.current; // Player orbit const pp = playerPosRef.current; pp.orbitAngle += 0.008 * (1 + pwr.engines * 0.25); const pr = 12 + pwr.engines * 1.5; playerGroup.position.x = -pr * Math.cos(pp.orbitAngle); playerGroup.position.z = pr * Math.sin(pp.orbitAngle) * 0.6; playerGroup.position.y = Math.sin(pp.orbitAngle * 1.3) * 2; if (tgt && eng.locked) playerGroup.lookAt(enemyGroup.position); pEngine.intensity = 2 + pwr.engines * 0.5 + (overloadRef.current ? 3 : 0); if (overloadRef.current) { pMesh.material.emissive.setHex(0xfbbf24); pMesh.material.emissiveIntensity = 0.3 + Math.sin(t * 8) * 0.15; } else { pMesh.material.emissive.setHex(0xf0a030); pMesh.material.emissiveIntensity = 0.15; } pShield.material.opacity = 0.03 + pwr.shields * 0.015; pShield.scale.setScalar(1 + pwr.shields * 0.05); // Enemy orbit const ep = enemyPosRef.current; const engineScale = eng.engines / 100; ep.orbitAngle += 0.01 * engineScale; const er = 14 * engineScale; enemyGroup.position.x = 15 + er * Math.cos(ep.orbitAngle); enemyGroup.position.z = er * Math.sin(ep.orbitAngle) * 0.5; enemyGroup.position.y = Math.sin(ep.orbitAngle * 1.1) * 1.5 * engineScale; eEngine.intensity = 1.5 * engineScale; if (tgt) enemyGroup.lookAt(playerGroup.position); if (lockBrackets.visible) { lockBrackets.position.copy(enemyGroup.position); lockBrackets.rotation.y = t * 0.3; } if (targetLine.visible && tgt && eng.locked) { const pPos = playerGroup.position; const ePos = enemyGroup.position; const positions = targetLine.geometry.attributes.position; positions.setXYZ(0, pPos.x, pPos.y, pPos.z); positions.setXYZ(1, ePos.x, ePos.y, ePos.z); positions.needsUpdate = true; targetLine.computeLineDistances(); } stars.rotation.y = t * 0.003; projPool.tick(); impPool.tick(); 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); projPool.clear(); }; }, []); // Auto-scroll log useEffect(() => { if (logScrollRef.current) logScrollRef.current.scrollTop = logScrollRef.current.scrollHeight; }, [combatLog]); // ── Lock Target ── const lockTarget = useCallback(() => { addLog('Initiating target lock...', '#f0a030'); setEnemy(prev => ({ ...prev, lockTimer: 0, locked: false })); setTarget('Guristas Pirata'); if (targetLineRef.current) targetLineRef.current.visible = true; }, [addLog]); // ── Adjust Power ── const adjustPower = useCallback((system, delta) => { setPower(prev => { const newVal = prev[system] + delta; if (newVal < 0) return prev; const newTotal = Object.entries(prev).reduce((sum, [k, v]) => sum + (k === system ? newVal : v), 0); if (newTotal > MAX_POWER) return prev; return { ...prev, [system]: newVal }; }); }, []); // ── Cast Ability ── const castModule = useCallback((abilityId) => { const ab = modulesRef.current.find(a => a.id === abilityId); if (!ab) return; if (!targetRef.current) { addLog('No target locked.', '#ef4444'); return; } if (!enemyRef.current.locked) { addLog('Target not locked yet.', '#ef4444'); return; } if (ab.cd > 0) { addLog(`${ab.name} recharging (${ab.cd.toFixed(1)}s).`, '#ef4444'); return; } const pwr = powerRef.current; const pl = playerRef.current; if (pl.energy < ab.cost) { addLog('Insufficient energy.', '#ef4444'); return; } setPlayer(prev => ({ ...prev, energy: prev.energy - ab.cost })); const actualCd = ab.maxCd * (1 - pwr.aux * 0.06); setModules(prev => prev.map(a => a.id === abilityId ? { ...a, cd: Math.max(0.5, actualCd) } : a)); const pPos = playerShipRef.current?.position || { x: -15, y: 0, z: 0 }; const ePos = enemyShipRef.current?.position || { x: 15, y: 0, z: 0 }; const proj = projectilePoolRef.current; const imp = impactPoolRef.current; if (ab.id === 'q') { const dmg = ab.damage * weaponMult; proj.spawn(pPos.x + 3, pPos.y, pPos.z, ePos.x, ePos.y, ePos.z, 0xef4444, dmg, 'beam', subsystemRef.current); addLog(`Railgun → ${subsystemRef.current.toUpperCase()} (×${weaponMult.toFixed(1)})`, '#ef4444'); } if (ab.id === 'w') { const restore = 15 + pwr.shields * 6; setPlayer(prev => ({ ...prev, shields: Math.min(100, prev.shields + restore) })); imp.spawn(pPos.x, pPos.y, pPos.z, 0x22d3ee, 5); addLog(`Shield Boost: +${Math.round(restore)}%`, '#22d3ee'); } if (ab.id === 'e') { const dur = 3 + pwr.aux * 0.5; const mid = { x: (pPos.x + ePos.x) / 2, y: (pPos.y + ePos.y) / 2, z: (pPos.z + ePos.z) / 2 }; proj.spawn(mid.x, mid.y, mid.z, ePos.x, ePos.y, ePos.z, 0xa78bfa, 0, 'pulse', 'none'); setEnemyBuffs([{ id: 'emp', name: 'EM Disrupted', icon: '⟐', duration: Math.round(dur), color: '#a78bfa' }]); setEnemy(prev => ({ ...prev, weapons: Math.max(0, prev.weapons - 15 - pwr.aux * 3), engines: Math.max(0, prev.engines - 10 - pwr.aux * 2) })); addLog(`EM Pulse! Enemy disrupted ${dur.toFixed(1)}s`, '#a78bfa'); setTimeout(() => { setEnemyBuffs([]); addLog('Enemy systems restored.', '#5a6b82'); }, dur * 1000); } if (ab.id === 'r') { overloadRef.current = true; setOverloaded(true); setPlayerBuffs(prev => [...prev, { id: 'overload', name: 'Overload', icon: '⚡', duration: 8, color: '#f0a030' }]); addLog('⚡ OVERLOAD — double fire rate 8s!', '#f0a030'); setTimeout(() => { overloadRef.current = false; setOverloaded(false); setPlayerBuffs(prev => prev.filter(b => b.id !== 'overload')); addLog('Overload ended.', '#5a6b82'); }, 8000); } if (ab.id === 'd') { playerPosRef.current.speed = 300 * speedMult; addLog(`Afterburners! Speed ×${speedMult.toFixed(1)}`, '#22c55e'); setTimeout(() => { playerPosRef.current.speed = 0; }, 2500); } if (ab.id === 'f') { const repair = 12 + pwr.aux * 4; setPlayer(prev => ({ ...prev, hull: Math.min(100, prev.hull + repair) })); imp.spawn(pPos.x, pPos.y, pPos.z, 0xfb923c, 6); addLog(`Hull Patch: +${Math.round(repair)}%`, '#fb923c'); } }, [addLog, weaponMult, speedMult]); // ── Keyboard ── useEffect(() => { const handler = (e) => { const key = e.key.toLowerCase(); if (['1', '2', '3', '4', '5', '6'].includes(key)) { e.preventDefault(); castModule(key); } if (key === ' ' && !targetRef.current) { e.preventDefault(); lockTarget(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [castModule, lockTarget]); // ── Combat Tick ── useEffect(() => { const interval = setInterval(() => { tickRef.current++; const pwr = powerRef.current; const eng = enemyRef.current; const sub = subsystemRef.current; const tgt = targetRef.current; const tps = 1000 / TICK_MS; setModules(prev => prev.map(a => ({ ...a, cd: Math.max(0, a.cd - TICK_MS / 1000) }))); const eRegen = (2 + pwr.aux * 2) / tps; const eDrain = overloadRef.current ? (3 + pwr.weapons * 0.5) / tps : 0; setPlayer(prev => ({ ...prev, energy: Math.min(prev.maxEnergy, Math.max(0, prev.energy + eRegen - eDrain)) })); setPlayer(prev => { if (prev.shields < 100) return { ...prev, shields: Math.min(100, prev.shields + (pwr.shields * 1.2) / tps) }; return prev; }); const proj = projectilePoolRef.current; const imp = impactPoolRef.current; const pPos = playerShipRef.current?.position; const ePos = enemyShipRef.current?.position; if (!pPos || !ePos) return; if (tgt && eng.locked && eng.hull > 0) { const baseInterval = overloadRef.current ? 15 : 40; autoFireTimerRef.current++; if (autoFireTimerRef.current >= baseInterval) { autoFireTimerRef.current = 0; const bullets = Math.max(1, Math.floor(pwr.weapons / 2)); const dmgPerBullet = (3 + pwr.weapons * 1.5) / bullets; for (let b = 0; b < bullets; b++) { const jx = ePos.x + (Math.random() - 0.5) * 2; const jy = ePos.y + (Math.random() - 0.5) * 1; const jz = ePos.z + (Math.random() - 0.5) * 2; proj.spawn(pPos.x + 2, pPos.y, pPos.z, jx, jy, jz, overloadRef.current ? 0xfbbf24 : 0xf0a030, dmgPerBullet, 'bullet', sub); } } enemyFireTimerRef.current++; if (enemyFireTimerRef.current >= 30 && eng.weapons > 10) { enemyFireTimerRef.current = 0; const enemyDmg = 4 * (eng.weapons / 100); if (Math.random() < pwr.engines * 0.04) { proj.spawn(ePos.x - 1, ePos.y, ePos.z, pPos.x + (Math.random() - 0.5) * 4, pPos.y + (Math.random() - 0.5) * 2, pPos.z + (Math.random() - 0.5) * 4, 0xef4444, 0, 'bullet', 'none'); } else { proj.spawn(ePos.x - 1, ePos.y, ePos.z, pPos.x, pPos.y, pPos.z, 0xef4444, enemyDmg, 'bullet', 'hull'); } } } if (tgt && !eng.locked && eng.lockTimer < eng.lockTime) { setEnemy(prev => { const newTimer = prev.lockTimer + TICK_MS / 1000; if (newTimer >= prev.lockTime) { addLog('★ TARGET LOCKED', '#22c55e'); if (enemyLockRef.current) enemyLockRef.current.visible = true; return { ...prev, locked: true, lockTimer: newTimer }; } return { ...prev, lockTimer: newTimer }; }); } const arrived = proj.getArrived(); for (const p of arrived) { if (p.userData.dmg <= 0) continue; const isPlayer = p.userData.color === 0xf0a030 || p.userData.color === 0xfbbf24; if (isPlayer) { imp.spawn(p.userData.tx, p.userData.ty, p.userData.tz, 0xef4444, 2); setEnemy(prev => { let ne = { ...prev }; const d = p.userData.dmg; if (p.userData.subsystem === 'shields') { ne.shields = Math.max(0, ne.shields - d); } else if (p.userData.subsystem === 'hull') { if (ne.shields > 0) ne.shields = Math.max(0, ne.shields - d * 0.4); else { ne.armor = Math.max(0, ne.armor - d * 0.5); ne.hull = Math.max(0, ne.hull - d * 0.5); } } else if (p.userData.subsystem === 'weapons') { ne.weapons = Math.max(0, ne.weapons - d); } else if (p.userData.subsystem === 'engines') { ne.engines = Math.max(0, ne.engines - d); } return ne; }); } else { imp.spawn(p.userData.tx, p.userData.ty, p.userData.tz, 0xef4444, 2); setPlayer(prev => { let np = { ...prev }; const d = p.userData.dmg; const absorb = 0.4 + pwr.shields * 0.08; if (np.shields > 0) { const ab = Math.min(d * absorb, np.shields); np.shields = Math.max(0, np.shields - ab); const bl = d - ab; if (bl > 0) np.armor = Math.max(0, np.armor - bl * 0.5); } else if (np.armor > 0) { np.armor = Math.max(0, np.armor - d * 0.6); np.hull = Math.max(0, np.hull - d * 0.4); } else { np.hull = Math.max(0, np.hull - d); } return np; }); } } if (tickRef.current % Math.round(tps) === 0) { setPlayerBuffs(prev => prev.map(b => b.duration > 0 ? { ...b, duration: b.duration - 1 } : b).filter(b => b.duration !== 0)); } }, TICK_MS); return () => clearInterval(interval); }, [addLog]); /* ═══ RENDER — Immersive Game HUD ═══ */ const lockPct = target && !enemy.locked ? Math.min(100, (enemy.lockTimer / enemy.lockTime) * 100) : 0; return (
{/* ── 3D VIEWPORT ── */}
{/* ═══ HUD OVERLAY LAYER ═══ */}
{/* ── TOP BAR — System info strip ── */}
JITA 1.0 SEC
{player.name} {player.class}
SPD {player.speed.toFixed(0)} m/s
{overloaded && ⚡ OVERLOAD} {target && enemy.locked && ( TARGET LOCKED )}
ONLINE
{/* ── LEFT — Ship Systems ── */}
{/* Ship status */}
Ship Systems
{[ { label: 'SH', value: player.shields, color: '#22d3ee', grad: 'linear-gradient(90deg, #0891b2, #22d3ee)' }, { label: 'AR', value: player.armor, color: '#f0a030', grad: 'linear-gradient(90deg, #b47818, #f0a030)' }, { label: 'HU', value: player.hull, color: '#22c55e', grad: 'linear-gradient(90deg, #16a34a, #22c55e)' }, { label: 'NRG', value: player.energy, color: player.energy > 25 ? '#a78bfa' : '#ef4444', grad: player.energy > 25 ? 'linear-gradient(90deg, #6366f1, #a78bfa)' : 'linear-gradient(90deg, #dc2626, #ef4444)' }, ].map(bar => (
{bar.label}
{bar.label === 'NRG' ? Math.round(bar.value) : bar.value.toFixed(0)}
))}
{/* Reactor Power */}
Reactor {totalPower}/{MAX_POWER}
{[ { key: 'weapons', label: 'WPN', color: '#ef4444' }, { key: 'shields', label: 'SHD', color: '#22d3ee' }, { key: 'engines', label: 'ENG', color: '#22c55e' }, { key: 'aux', label: 'AUX', color: '#a78bfa' }, ].map(sys => (
{sys.label}
{Array.from({ length: MAX_POWER }, (_, i) => (
))}
))}
WPN ×{weaponMult.toFixed(1)} dmg {' · '} SHD {shieldRegen.toFixed(0)}/s {' · '} ENG {dodgeChance}% dodge {' · '} AUX {energyRegen.toFixed(0)} NRG/s
{/* Buffs */}
Status
{playerBuffs.map(b => ( {b.icon} {b.name}{b.duration > 0 ? ` ${b.duration}s` : ''} ))} {enemyBuffs.map(b => ( {b.icon} {b.name} {b.duration > 0 ? `${b.duration}s` : ''} ))}
{/* ── CENTER — Crosshair + Lock ── */}
{/* Crosshair */}
{/* Lock-in-progress ring */} {target && !enemy.locked && (
LOCKING {lockPct.toFixed(0)}%
)} {/* Engage prompt */} {!target && (

No hostiles detected

)}
{/* ── RIGHT — Target Intel ── */}
{/* Target panel */}
{target || 'NO TARGET'} {target && {enemy.locked ? 'LOCKED' : 'LOCKING'}}
{target && ( <> {[ { label: 'SH', value: enemy.shields, color: '#22d3ee' }, { label: 'AR', value: enemy.armor, color: '#f0a030' }, { label: 'HU', value: enemy.hull, color: '#22c55e' }, ].map(bar => (
{bar.label}
{bar.value.toFixed(0)}
))} )}
{/* Subsystem targeting */}
Subsystem
{[ { key: 'hull', label: 'Hull', color: '#f0a030', hp: enemy.hull }, { key: 'shields', label: 'Shields', color: '#22d3ee', hp: enemy.shields }, { key: 'weapons', label: 'Weapons', color: '#ef4444', hp: enemy.weapons }, { key: 'engines', label: 'Engines', color: '#22c55e', hp: enemy.engines }, ].map(sys => ( ))}
{/* Power coupling readout */}
Power Readout
WPN {power.weapons} → ×{weaponMult.toFixed(1)} dmg, {Math.max(1, Math.floor(power.weapons / 2))} rounds
SHD {power.shields} → {shieldRegen.toFixed(0)}/s, {(shieldAbsorb * 100).toFixed(0)}% abs
ENG {power.engines} → {dodgeChance}% dodge, ×{speedMult.toFixed(1)}
AUX {power.aux} → {energyRegen.toFixed(0)} NRG/s, −{cdReduction}% CD
{/* ── BOTTOM — Module Bar + Combat Log ── */}
{/* Module bar */}
Modules 1–6 or click
{/* Reactor gauge */}
REACTOR
25 ? 'linear-gradient(0deg, #4f46e5, #8b5cf6)' : 'linear-gradient(0deg, #dc2626, #ef4444)', transition: 'height 0.15s', }} /> {Math.round(player.energy)}
{/* Module buttons */} {modules.map(ab => { const onCd = ab.cd > 0; const noNRG = player.energy < ab.cost; const canCast = target && enemy.locked && !onCd && !noNRG; return ( ); })}
{/* Combat log */}
Log
{combatLog.length === 0 &&
SPACE to engage, then 1–6 for modules.
} {combatLog.map((entry, i) => (
{entry.time} {entry.msg}
))}
); } window.GDD.CombatDemo = CombatDemo;