Codex CLI
feat: update infinite ammo color to a brighter indigo for better visibility
578d40a
import { G } from './globals.js';
import { CFG } from './config.js';
// Cache HUD element refs and last values to minimize DOM churn
const HUD = {
waveEl: /** @type {HTMLElement|null} */(document.getElementById('wave')),
scoreEl: /** @type {HTMLElement|null} */(document.getElementById('score')),
enemiesEl: /** @type {HTMLElement|null} */(document.getElementById('enemies')),
ammoEl: /** @type {HTMLElement|null} */(document.getElementById('ammo')),
grenadesEl: /** @type {HTMLElement|null} */(document.getElementById('grenades')),
healthText: /** @type {HTMLElement|null} */(document.getElementById('health-text')),
healthFill: /** @type {HTMLElement|null} */(document.getElementById('health-fill')),
powerupsEl: /** @type {HTMLElement|null} */(document.getElementById('powerup-chips')),
ch: {
root: /** @type {HTMLElement|null} */(document.getElementById('crosshair')),
left: /** @type {HTMLElement|null} */(document.getElementById('ch-left')),
right: /** @type {HTMLElement|null} */(document.getElementById('ch-right')),
top: /** @type {HTMLElement|null} */(document.getElementById('ch-top')),
bottom: /** @type {HTMLElement|null} */(document.getElementById('ch-bottom')),
hitA1: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-a1')),
hitA2: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-a2')),
hitB1: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-b1')),
hitB2: /** @type {HTMLElement|null} */(document.getElementById('ch-hit-b2')),
lastGap: -1,
lastLen: -1,
hitLen: -1,
lastHitOpacity: -1,
hitGap: -1
},
last: {
hp: -1,
hpPct: -1,
wave: -1,
score: -1,
enemies: -1,
ammo: '',
grenades: -1,
powerupsKey: ''
}
};
export function updateHUD() {
const hp = Math.ceil(G.player.health);
const hpPct = Math.max(0, Math.min(1, G.player.health / CFG.player.health));
if (hp !== HUD.last.hp) {
HUD.last.hp = hp;
if (HUD.healthText) HUD.healthText.textContent = String(hp);
}
if (Math.abs(hpPct - HUD.last.hpPct) > 0.005) {
HUD.last.hpPct = hpPct;
if (HUD.healthFill) HUD.healthFill.style.width = ((hpPct * 100) | 0) + '%';
}
if (G.waves.current !== HUD.last.wave) {
HUD.last.wave = G.waves.current;
if (HUD.waveEl) HUD.waveEl.textContent = String(G.waves.current);
}
if (G.player.score !== HUD.last.score) {
HUD.last.score = G.player.score;
if (HUD.scoreEl) HUD.scoreEl.textContent = String(G.player.score);
}
if (G.waves.aliveCount !== HUD.last.enemies) {
HUD.last.enemies = G.waves.aliveCount;
if (HUD.enemiesEl) HUD.enemiesEl.textContent = String(G.waves.aliveCount);
}
let ammoText;
if (G.weapon.infiniteAmmoTimer > 0) {
ammoText = '∞/∞';
} else {
const reserveText = G.weapon.reserve === Infinity ? '∞' : String(G.weapon.reserve);
ammoText = `${G.weapon.ammo}/${reserveText}`;
}
if (ammoText !== HUD.last.ammo) {
HUD.last.ammo = ammoText;
if (HUD.ammoEl) HUD.ammoEl.textContent = ammoText;
}
if (G.grenadeCount !== HUD.last.grenades) {
HUD.last.grenades = G.grenadeCount;
if (HUD.grenadesEl) HUD.grenadesEl.textContent = String(G.grenadeCount);
}
// Powerup chips next to health
const active = [];
if (G.weapon.rofBuffTimer > 0) active.push({ id: 'accelerator', name: 'ACCELERATE', color: 0xffd84d, time: G.weapon.rofBuffTimer });
if (G.weapon.infiniteAmmoTimer > 0) active.push({ id: 'infinite', name: 'INFINITE AMMO', color: 0x6366f1, time: G.weapon.infiniteAmmoTimer });
const key = active.map(a => a.id).join(',');
if (key !== HUD.last.powerupsKey) {
HUD.last.powerupsKey = key;
const el = HUD.powerupsEl;
if (el) {
// Clear and rebuild chips
el.innerHTML = '';
for (const p of active) {
const chip = document.createElement('div');
chip.className = 'pu-chip';
chip.dataset.id = p.id;
const r = (p.color >> 16) & 255;
const g = (p.color >> 8) & 255;
const b = p.color & 255;
chip.style.borderColor = `rgba(${r},${g},${b},0.75)`;
chip.style.boxShadow = `0 0 10px rgba(${r},${g},${b},0.35)`;
chip.style.backgroundColor = `rgba(${r},${g},${b},0.12)`;
const fill = document.createElement('div');
fill.className = 'pu-fill';
fill.style.backgroundColor = `rgba(${r},${g},${b},0.35)`;
fill.style.width = '100%';
const text = document.createElement('span');
text.className = 'pu-text';
text.textContent = p.name;
chip.appendChild(fill);
chip.appendChild(text);
el.appendChild(chip);
}
}
}
// Update blink state even if set didn't change
const el = HUD.powerupsEl;
if (el) {
for (const p of active) {
const chip = el.querySelector(`.pu-chip[data-id="${p.id}"]`);
if (chip) {
const shouldBlink = p.time != null && p.time <= 3;
if (shouldBlink) chip.classList.add('blink'); else chip.classList.remove('blink');
// Update progress fill width if total known
const total = (p.id === 'accelerator') ? (G.weapon.rofBuffTotal || 0) : (p.id === 'infinite' ? (G.weapon.infiniteAmmoTotal || 0) : 0);
const fill = chip.querySelector('.pu-fill');
if (fill && total > 0 && p.time != null) {
const t = Math.max(0, Math.min(1, p.time / total));
fill.style.width = (t * 100).toFixed(1) + '%';
}
}
}
// Also remove blink from any chips not active
const nodes = el.querySelectorAll('.pu-chip');
nodes.forEach(node => {
const id = node.dataset.id;
if (!active.find(a => a.id === id)) node.classList.remove('blink');
});
}
}
export function showWaveBanner(text) {
const banner = document.getElementById('wave-banner');
if (!banner) return;
banner.textContent = text;
banner.classList.add('show');
setTimeout(() => banner.classList.remove('show'), 2000);
}
export function showOverlay(type) {
const overlay = document.getElementById('overlay');
const content = document.getElementById('overlay-content');
if (!overlay || !content) return;
overlay.classList.remove('hidden');
if (type === 'paused') {
content.innerHTML = `
<h1>Paused</h1>
<p>Click to Resume</p>
`;
} else if (type === 'gameover') {
content.innerHTML = `
<h1>You Died</h1>
<p>Score: ${G.player.score}</p>
<p>Wave: ${G.waves.current}</p>
<p>Click to Restart</p>
`;
}
}
// Simple red damage overlay that fades over time
export function updateDamageEffect(delta) {
const el = document.getElementById('damage-overlay');
if (!el) return;
// Fade
G.damageFlash = Math.max(0, G.damageFlash - CFG.hud.damageFadeSpeed * delta);
const opacity = Math.min(CFG.hud.damageMaxOpacity, G.damageFlash * CFG.hud.damageMaxOpacity);
el.style.opacity = String(opacity);
}
// Green heal overlay that fades over time
export function updateHealEffect(delta) {
const el = document.getElementById('heal-overlay');
if (!el) return;
G.healFlash = Math.max(0, G.healFlash - CFG.hud.healFadeSpeed * delta);
const opacity = Math.min(CFG.hud.healMaxOpacity, G.healFlash * CFG.hud.healMaxOpacity);
el.style.opacity = String(opacity);
}
// Crosshair widening based on current spread
export function updateCrosshair(delta) {
const { ch } = HUD;
if (!ch.root || !ch.left || !ch.right || !ch.top || !ch.bottom) return;
// Convert NDC spread to pixel gap (approximate using viewport width)
// When crouched, the effective minimum spread is reduced
const baseMin = (CFG.gun.spreadMin || CFG.gun.bloom || 0);
const crouchMin = G.input.crouch ? baseMin * (CFG.gun.spreadCrouchMult || 1) : baseMin;
const ndc = Math.max(G.weapon.spread, crouchMin);
const baseGap = 6; // px baseline gap
const gapPx = baseGap + ndc * 0.5 * window.innerWidth; // half-width maps NDC to px
const armLen = 10 + Math.min(20, ndc * window.innerWidth * 0.4); // grow a bit with spread
if (Math.abs(gapPx - ch.lastGap) < 0.5 && Math.abs(armLen - ch.lastLen) < 0.5) return;
ch.lastGap = gapPx;
ch.lastLen = armLen;
// Horizontal arms
ch.left.style.width = armLen + 'px';
ch.left.style.left = -(gapPx + armLen) + 'px';
ch.left.style.top = '-1px';
ch.right.style.width = armLen + 'px';
ch.right.style.left = gapPx + 'px';
ch.right.style.top = '-1px';
// Vertical arms
ch.top.style.height = armLen + 'px';
ch.top.style.top = -(gapPx + armLen) + 'px';
ch.top.style.left = '-1px';
ch.bottom.style.height = armLen + 'px';
ch.bottom.style.top = gapPx + 'px';
ch.bottom.style.left = '-1px';
}
// Small white X that flashes briefly when hitting an enemy
export function updateHitMarker(delta) {
const { ch } = HUD;
if (!ch.root || !ch.hitA1 || !ch.hitA2 || !ch.hitB1 || !ch.hitB2) return;
// Fade out hit flash
G.hitFlash = Math.max(0, G.hitFlash - (CFG.hud.hitFadeSpeed || 12) * delta);
const maxOp = (CFG.hud.hitMaxOpacity != null ? CFG.hud.hitMaxOpacity : 0.3);
const op = Math.max(0, Math.min(maxOp, G.hitFlash));
// Only touch DOM when something changed
if (Math.abs(op - ch.lastHitOpacity) > 0.01) {
ch.lastHitOpacity = op;
ch.hitA1.style.opacity = String(op);
ch.hitA2.style.opacity = String(op);
ch.hitB1.style.opacity = String(op);
ch.hitB2.style.opacity = String(op);
}
// Size and placement: four segments with a center gap
const L = (CFG.hud.hitSize || 16) | 0; // length of each segment in px
const extra = (CFG.hud.hitGapExtra || 6) | 0;
const baseGap = ch.lastGap >= 0 ? ch.lastGap : 10; // from crosshair
const gap = baseGap + extra; // ensure larger than crosshair gap
if (L !== ch.hitLen || Math.abs(gap - ch.hitGap) > 0.5) {
ch.hitLen = L;
ch.hitGap = gap;
// Distance from center to each segment center along the diagonal
const d = gap + L * 0.5;
const s = Math.SQRT1_2; // 1 / sqrt(2)
const dx = d * s;
const dy = d * s;
const leftBase = (v) => (v - L * 0.5) + 'px';
const topBase = (v) => (v - 1) + 'px'; // thickness ~2px
// A diagonal (+45deg): NE and SW
ch.hitA1.style.width = L + 'px';
ch.hitA1.style.left = leftBase(dx);
ch.hitA1.style.top = topBase(dy);
ch.hitA1.style.transform = 'rotate(45deg)';
ch.hitA2.style.width = L + 'px';
ch.hitA2.style.left = leftBase(-dx);
ch.hitA2.style.top = topBase(-dy);
ch.hitA2.style.transform = 'rotate(45deg)';
// B diagonal (-45deg): NW and SE
ch.hitB1.style.width = L + 'px';
ch.hitB1.style.left = leftBase(-dx);
ch.hitB1.style.top = topBase(dy);
ch.hitB1.style.transform = 'rotate(-45deg)';
ch.hitB2.style.width = L + 'px';
ch.hitB2.style.left = leftBase(dx);
ch.hitB2.style.top = topBase(-dy);
ch.hitB2.style.transform = 'rotate(-45deg)';
}
}