Files
Space-Game/js/demos/combat.js
2026-05-25 13:00:20 -04:00

1045 lines
50 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;
/* ═══════════════════════════════════════════════════════════════════
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 (
<div style={{
position: 'relative',
width: '100%',
height: '100%',
minHeight: '620px',
overflow: 'hidden',
background: '#040810',
fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, monospace",
fontSize: '11px',
color: '#d4dce8',
cursor: 'crosshair',
}}>
{/* ── 3D VIEWPORT ── */}
<div ref={containerRef} style={{ position: 'absolute', inset: 0, zIndex: 0 }} />
{/* ═══ HUD OVERLAY LAYER ═══ */}
<div style={{
position: 'absolute', inset: 0, zIndex: 1,
display: 'grid',
gridTemplateColumns: '200px 1fr 220px',
gridTemplateRows: '38px 1fr auto',
gridTemplateAreas: `"top top top" "left center right" "bottom bottom bottom"`,
pointerEvents: 'none',
}}>
{/* ── TOP BAR — System info strip ── */}
<div style={{
gridArea: 'top',
display: 'flex', alignItems: 'center',
padding: '0 14px', gap: '14px',
background: 'linear-gradient(180deg, rgba(4,8,16,0.92) 0%, rgba(4,8,16,0.5) 80%, transparent 100%)',
pointerEvents: 'auto',
}}>
<button onClick={() => window.location.hash = 'overview'} style={{
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '4px',
color: '#94a3b8',
fontSize: '9px',
padding: '3px 8px',
cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: '4px',
fontFamily: 'inherit',
letterSpacing: '0.04em',
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => { e.target.style.background = 'rgba(255,255,255,0.12)'; e.target.style.color = '#e2e8f0'; }}
onMouseLeave={e => { e.target.style.background = 'rgba(255,255,255,0.06)'; e.target.style.color = '#94a3b8'; }}
>
<span style={{ fontSize: '11px' }}></span> DOCS
</button>
<div style={{ width: 1, height: 16, background: 'rgba(28,42,63,0.6)' }} />
<span style={{ fontSize: '10px', fontWeight: 600, color: '#f0a030', letterSpacing: '0.04em' }}>JITA</span>
<span style={{ fontSize: '9px', padding: '1px 6px', borderRadius: '999px', fontWeight: 600, background: 'rgba(34,197,94,0.1)', color: '#22c55e', border: '1px solid rgba(34,197,94,0.25)' }}>1.0 SEC</span>
<div style={{ width: 1, height: 16, background: 'rgba(28,42,63,0.6)' }} />
<span style={{ fontSize: '12px', fontWeight: 600, color: '#f1f5f9', letterSpacing: '-0.01em', fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" }}>
{player.name}
</span>
<span style={{ fontSize: '9px', color: '#5a6b82' }}>{player.class}</span>
<div style={{ width: 1, height: 16, background: 'rgba(28,42,63,0.6)' }} />
<span style={{ fontSize: '9px', color: '#5a6b82' }}>SPD</span>
<span style={{ fontSize: '10px', color: '#94a3b8', fontVariantNumeric: 'tabular-nums' }}>{player.speed.toFixed(0)} m/s</span>
<div style={{ flex: 1 }} />
{overloaded && <span style={{ color: '#f0a030', fontWeight: 700, fontSize: '10px', animation: 'pulse 0.8s infinite' }}> OVERLOAD</span>}
{target && enemy.locked && (
<span style={{ fontSize: '10px', fontWeight: 600, color: '#ef4444', display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#ef4444', boxShadow: '0 0 6px #ef4444' }} />
TARGET LOCKED
</span>
)}
<div style={{ width: 1, height: 16, background: 'rgba(28,42,63,0.6)' }} />
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: 4, height: 4, borderRadius: '50%', background: '#22c55e', boxShadow: '0 0 4px #22c55e' }} />
<span style={{ fontSize: '9px', color: '#22c55e' }}>ONLINE</span>
</span>
</div>
{/* ── LEFT — Ship Systems ── */}
<div style={{
gridArea: 'left',
display: 'flex', flexDirection: 'column', gap: '5px',
padding: '5px',
pointerEvents: 'auto',
}}>
{/* Ship status */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Ship Systems
</div>
<div style={{ padding: '7px 9px' }}>
{[
{ 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 => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '5px', marginBottom: '5px' }}>
<span style={{ fontSize: '8px', color: bar.color, width: 22, textTransform: 'uppercase', letterSpacing: '0.06em', flexShrink: 0 }}>{bar.label}</span>
<div style={{ flex: 1, height: 4, background: 'rgba(255,255,255,0.04)', borderRadius: '999px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: bar.grad, borderRadius: '999px', transition: 'width 0.4s cubic-bezier(0.23,1,0.32,1)' }} />
</div>
<span style={{ fontSize: '9px', color: bar.color, fontVariantNumeric: 'tabular-nums', width: 28, textAlign: 'right', flexShrink: 0 }}>
{bar.label === 'NRG' ? Math.round(bar.value) : bar.value.toFixed(0)}
</span>
</div>
))}
</div>
</div>
{/* Reactor Power */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
flex: 1,
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Reactor
<span style={{ marginLeft: 'auto', fontSize: '7px', color: totalPower < MAX_POWER ? '#22c55e' : '#94a3b8' }}>{totalPower}/{MAX_POWER}</span>
</div>
<div style={{ padding: '5px 9px' }}>
{[
{ 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 => (
<div key={sys.key} style={{ display: 'flex', alignItems: 'center', gap: '3px', marginBottom: '3px' }}>
<span style={{ fontSize: '7px', color: sys.color, width: 18, textTransform: 'uppercase', letterSpacing: '0.08em', flexShrink: 0, textAlign: 'center' }}>{sys.label}</span>
<button onClick={() => adjustPower(sys.key, -1)} style={{
width: 13, height: 13, borderRadius: '2px', border: '1px solid rgba(28,42,63,0.7)',
background: 'rgba(22,32,50,0.8)', color: '#94a3b8', fontSize: '9px',
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: 1,
flexShrink: 0, padding: 0,
}}></button>
<div style={{ display: 'flex', gap: 1, flex: 1 }}>
{Array.from({ length: MAX_POWER }, (_, i) => (
<div key={i} style={{ flex: 1, height: 6, borderRadius: '1px', background: i < power[sys.key] ? sys.color : 'rgba(255,255,255,0.04)', transition: 'background 0.15s' }} />
))}
</div>
<button onClick={() => adjustPower(sys.key, 1)} style={{
width: 13, height: 13, borderRadius: '2px', border: '1px solid rgba(28,42,63,0.7)',
background: 'rgba(22,32,50,0.8)', color: '#94a3b8', fontSize: '9px',
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', lineHeight: 1,
flexShrink: 0, padding: 0,
}}>+</button>
</div>
))}
<div style={{ fontSize: '7px', color: '#5a6b82', marginTop: '3px', lineHeight: 1.5 }}>
<span style={{ color: '#ef4444' }}>WPN</span> ×{weaponMult.toFixed(1)} dmg
{' · '}
<span style={{ color: '#22d3ee' }}>SHD</span> {shieldRegen.toFixed(0)}/s
{' · '}
<span style={{ color: '#22c55e' }}>ENG</span> {dodgeChance}% dodge
{' · '}
<span style={{ color: '#a78bfa' }}>AUX</span> {energyRegen.toFixed(0)} NRG/s
</div>
</div>
</div>
{/* Buffs */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Status
</div>
<div style={{ padding: '4px 7px', display: 'flex', gap: '3px', flexWrap: 'wrap' }}>
{playerBuffs.map(b => (
<span key={b.id} style={{
display: 'inline-flex', alignItems: 'center', gap: '2px',
padding: '1px 5px', borderRadius: '2px', fontSize: '8px',
background: `${b.color}12`, border: `1px solid ${b.color}30`, color: b.color,
}}>
{b.icon} {b.name}{b.duration > 0 ? ` ${b.duration}s` : ''}
</span>
))}
{enemyBuffs.map(b => (
<span key={b.id} style={{
display: 'inline-flex', alignItems: 'center', gap: '2px',
padding: '1px 5px', borderRadius: '2px', fontSize: '8px',
background: `${b.color}12`, border: `1px solid ${b.color}30`, color: b.color,
}}>
{b.icon} {b.name} {b.duration > 0 ? `${b.duration}s` : ''}
</span>
))}
</div>
</div>
</div>
{/* ── CENTER — Crosshair + Lock ── */}
<div style={{
gridArea: 'center',
pointerEvents: 'none',
display: 'flex', alignItems: 'center', justifyContent: 'center',
position: 'relative',
}}>
{/* Crosshair */}
<div style={{ width: 44, height: 44, position: 'relative', opacity: 0.3 }}>
<div style={{ position: 'absolute', width: 1, height: 14, left: '50%', top: 0, transform: 'translateX(-50%)', background: '#94a3b8' }} />
<div style={{ position: 'absolute', width: 1, height: 14, left: '50%', bottom: 0, transform: 'translateX(-50%)', background: '#94a3b8' }} />
<div style={{ position: 'absolute', height: 1, width: 14, top: '50%', left: 0, transform: 'translateY(-50%)', background: '#94a3b8' }} />
<div style={{ position: 'absolute', height: 1, width: 14, top: '50%', right: 0, transform: 'translateY(-50%)', background: '#94a3b8' }} />
<div style={{
width: 30, height: 30, border: '1px solid rgba(212,220,232,0.2)', borderRadius: '50%',
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
}} />
</div>
{/* Lock-in-progress ring */}
{target && !enemy.locked && (
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', textAlign: 'center' }}>
<div style={{ width: 100, height: 100, border: '2px solid rgba(240,160,48,0.25)', borderRadius: '50%', margin: '0 auto 6px', position: 'relative' }}>
<svg width="100" height="100" style={{ position: 'absolute', top: -2, left: -2, transform: 'rotate(-90deg)' }}>
<circle cx="50" cy="50" r="48" fill="none" stroke="#f0a030" strokeWidth="2"
strokeDasharray={`${(lockPct / 100) * 301} 301`}
style={{ transition: 'stroke-dasharray 0.1s' }} />
</svg>
</div>
<div style={{ fontSize: '9px', color: '#f0a030', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
LOCKING {lockPct.toFixed(0)}%
</div>
</div>
)}
{/* Engage prompt */}
{!target && (
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', textAlign: 'center', pointerEvents: 'auto' }}>
<p style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '10px' }}>No hostiles detected</p>
<button onClick={lockTarget} style={{
padding: '7px 20px', borderRadius: '4px',
border: '1.5px solid rgba(240,160,48,0.25)',
background: 'rgba(240,160,48,0.06)', color: '#f0a030',
fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, monospace",
fontSize: '10px', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.1em',
cursor: 'pointer', transition: 'all 0.15s',
}}>
ENGAGE TARGET [SPACE]
</button>
</div>
)}
</div>
{/* ── RIGHT — Target Intel ── */}
<div style={{
gridArea: 'right',
display: 'flex', flexDirection: 'column', gap: '5px',
padding: '5px',
pointerEvents: 'auto',
}}>
{/* Target panel */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#ef4444' }} />
<span style={{ color: target ? '#f1f5f9' : '#5a6b82' }}>{target || 'NO TARGET'}</span>
{target && <span style={{ marginLeft: 'auto', fontSize: '7px', color: enemy.locked ? '#22c55e' : '#f0a030' }}>{enemy.locked ? 'LOCKED' : 'LOCKING'}</span>}
</div>
<div style={{ padding: '7px 9px' }}>
{target && (
<>
{[
{ label: 'SH', value: enemy.shields, color: '#22d3ee' },
{ label: 'AR', value: enemy.armor, color: '#f0a030' },
{ label: 'HU', value: enemy.hull, color: '#22c55e' },
].map(bar => (
<div key={bar.label} style={{ display: 'flex', alignItems: 'center', gap: '5px', marginBottom: '4px' }}>
<span style={{ fontSize: '8px', color: bar.color, width: 14, textTransform: 'uppercase', letterSpacing: '0.06em', flexShrink: 0 }}>{bar.label}</span>
<div style={{ flex: 1, height: 4, background: 'rgba(255,255,255,0.04)', borderRadius: '999px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${bar.value}%`, background: `linear-gradient(90deg, ${bar.color}88, ${bar.color})`, borderRadius: '999px', transition: 'width 0.3s' }} />
</div>
<span style={{ fontSize: '9px', color: bar.color, fontVariantNumeric: 'tabular-nums', width: 26, textAlign: 'right', flexShrink: 0 }}>{bar.value.toFixed(0)}</span>
</div>
))}
</>
)}
</div>
</div>
{/* Subsystem targeting */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
flex: 1,
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Subsystem
</div>
<div style={{ padding: '5px 7px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px' }}>
{[
{ 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 => (
<button key={sys.key} onClick={() => { setSubsystem(sys.key); addLog(`Retargeting: ${sys.key.toUpperCase()}`, '#a78bfa'); }} style={{
padding: '4px 5px', borderRadius: '3px',
border: `1px solid ${subsystem === sys.key ? sys.color : 'rgba(28,42,63,0.6)'}`,
background: subsystem === sys.key ? `${sys.color}10` : 'rgba(15,22,35,0.7)',
cursor: 'pointer', textAlign: 'left', transition: 'all 0.12s',
}}>
<span style={{ fontSize: '7px', color: sys.color, textTransform: 'uppercase', letterSpacing: '0.06em', display: 'block', marginBottom: '2px' }}>
{sys.label} {subsystem === sys.key ? '◄' : ''}
</span>
<div style={{ height: 2, background: 'rgba(255,255,255,0.04)', borderRadius: '999px', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${sys.hp}%`, background: sys.color, borderRadius: '999px', transition: 'width 0.3s' }} />
</div>
<span style={{ fontSize: '7px', color: '#94a3b8', display: 'block', marginTop: '1px' }}>{sys.hp.toFixed(0)}%</span>
</button>
))}
</div>
</div>
</div>
{/* Power coupling readout */}
<div style={{
background: 'rgba(12,18,30,0.85)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
overflow: 'hidden',
}}>
<div style={{
padding: '5px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Power Readout
</div>
<div style={{ padding: '5px 9px', fontSize: '8px', color: '#94a3b8', lineHeight: 1.6 }}>
<div><span style={{ color: '#ef4444' }}>WPN {power.weapons}</span> → ×{weaponMult.toFixed(1)} dmg, {Math.max(1, Math.floor(power.weapons / 2))} rounds</div>
<div><span style={{ color: '#22d3ee' }}>SHD {power.shields}</span> {shieldRegen.toFixed(0)}/s, {(shieldAbsorb * 100).toFixed(0)}% abs</div>
<div><span style={{ color: '#22c55e' }}>ENG {power.engines}</span> {dodgeChance}% dodge, ×{speedMult.toFixed(1)}</div>
<div><span style={{ color: '#a78bfa' }}>AUX {power.aux}</span> {energyRegen.toFixed(0)} NRG/s, {cdReduction}% CD</div>
</div>
</div>
</div>
{/* ── BOTTOM — Module Bar + Combat Log ── */}
<div style={{
gridArea: 'bottom',
display: 'flex', gap: '5px',
padding: '0 5px 5px',
pointerEvents: 'auto',
}}>
{/* Module bar */}
<div style={{
background: 'rgba(12,18,30,0.88)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
flex: 2,
overflow: 'hidden',
}}>
<div style={{
padding: '3px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Modules
<span style={{ marginLeft: 'auto', fontSize: '7px', color: '#5a6b82' }}>16 or click</span>
</div>
<div style={{ display: 'flex', gap: '3px', alignItems: 'flex-end', padding: '6px 8px' }}>
{/* Reactor gauge */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '42px', flexShrink: 0, marginRight: '2px' }}>
<span style={{ fontSize: '7px', color: '#a78bfa', marginBottom: '2px' }}>REACTOR</span>
<div style={{ width: '100%', height: '36px', background: 'rgba(255,255,255,0.04)', borderRadius: '3px', overflow: 'hidden', position: 'relative' }}>
<div style={{
position: 'absolute', bottom: 0, width: '100%', height: `${player.energy}%`,
background: player.energy > 25 ? 'linear-gradient(0deg, #4f46e5, #8b5cf6)' : 'linear-gradient(0deg, #dc2626, #ef4444)',
transition: 'height 0.15s',
}} />
<span style={{
position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '10px', fontWeight: 700, color: '#f1f5f9',
}}>{Math.round(player.energy)}</span>
</div>
</div>
<div style={{ width: 1, height: 36, background: 'rgba(28,42,63,0.6)', flexShrink: 0 }} />
{/* Module buttons */}
{modules.map(ab => {
const onCd = ab.cd > 0;
const noNRG = player.energy < ab.cost;
const canCast = target && enemy.locked && !onCd && !noNRG;
return (
<button key={ab.id} onClick={() => castModule(ab.id)} title={`${ab.name}${ab.desc} (${ab.cost} NRG)`} style={{
width: '52px', height: '42px', borderRadius: '4px',
border: `1.5px solid ${canCast ? 'rgba(240,160,48,0.25)' : 'rgba(28,42,63,0.6)'}`,
background: canCast ? 'rgba(240,160,48,0.04)' : 'rgba(15,22,35,0.7)',
cursor: canCast ? 'pointer' : 'default',
position: 'relative', overflow: 'hidden',
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
gap: '1px', transition: 'all 0.1s', padding: 0,
}}>
{onCd && <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: `${(ab.cd / (ab.maxCd * (1 - power.aux * 0.06))) * 100}%`, background: 'rgba(0,0,0,0.55)', transition: 'height 0.1s' }} />}
{noNRG && !onCd && <div style={{ position: 'absolute', inset: 0, background: 'rgba(80,15,15,0.25)' }} />}
<span style={{ fontSize: '12px', position: 'relative', zIndex: 1, opacity: canCast ? 1 : 0.4 }}>{ab.icon}</span>
<span style={{ fontSize: '8px', color: canCast ? '#f0a030' : '#5a6b82', position: 'relative', zIndex: 1 }}>
{onCd ? ab.cd.toFixed(1) : ab.key}
</span>
<span style={{ position: 'absolute', bottom: '1px', right: '2px', fontSize: '7px', color: noNRG && !onCd ? '#ef4444' : '#5a6b82', zIndex: 1 }}>{ab.cost}</span>
</button>
);
})}
</div>
</div>
{/* Combat log */}
<div style={{
background: 'rgba(12,18,30,0.88)',
border: '1px solid rgba(28,42,63,0.6)',
borderRadius: '6px',
backdropFilter: 'blur(8px)',
flex: 1,
overflow: 'hidden',
display: 'flex', flexDirection: 'column',
}}>
<div style={{
padding: '3px 9px',
borderBottom: '1px solid rgba(28,42,63,0.6)',
fontSize: '8px', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#5a6b82',
display: 'flex', alignItems: 'center', gap: '5px',
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: '#f0a030' }} />
Log
</div>
<div ref={logScrollRef} style={{
flex: 1, overflowY: 'auto', maxHeight: '80px', padding: '3px 7px',
fontSize: '9px',
}}>
{combatLog.length === 0 && <div style={{ color: '#5a6b82', padding: '2px 0' }}>SPACE to engage, then 16 for modules.</div>}
{combatLog.map((entry, i) => (
<div key={i} style={{ display: 'flex', gap: '5px', marginBottom: '1px', lineHeight: 1.3 }}>
<span style={{ color: '#5a6b82', flexShrink: 0, fontSize: '8px' }}>{entry.time}</span>
<span style={{ color: entry.color, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{entry.msg}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
window.GDD.CombatDemo = CombatDemo;