// hud.js : maj des elements html du hud (hp, cd, vagues, upgrades, end game) import { UPGRADE_CATALOG, ABILITY_COOLDOWNS, DISPLACEMENT_COOLDOWNS, SCORE_PER_KILL, SCORE_PER_SOUL, SCORE_VICTORY, SCORE_PER_WAVE } from './constants.js'; import { showEndGameForm } from './leaderboard.js'; import { send } from './network.js'; import { setInputBlocked } from './input.js'; const BUFF_LABELS = { invulnerable: 'Invulnerable', casting: 'Incantation', flying: 'Vol', intangible: 'Intangible', combat_buff: 'Buff combat', damage_mult: 'Degats x3', }; const WAVE_STATES = { combat: 'Combat', preparation: 'Preparation', boss: 'Boss', victory: 'Victoire', }; export function updatePlayerHud(local) { document.getElementById('hud-class-name').textContent = (local.class ?? '').toUpperCase(); document.getElementById('hud-souls').textContent = 'Ames : ' + (local.souls ?? 0); const hpPct = local.max_hp > 0 ? Math.max(0, local.hp / local.max_hp * 100) : 0; document.getElementById('hud-hp-fill').style.width = hpPct + '%'; document.getElementById('hud-hp-text').textContent = local.hp + ' / ' + local.max_hp; _updateSkillCooldowns(local); _updateBuffTags(local); } function _updateSkillCooldowns(local) { const cds = local.cooldowns ?? {}; const abMaxes = ABILITY_COOLDOWNS[local.class] ?? [8, 12]; [['hud-s1', 0], ['hud-s2', 1]].forEach(([id, i]) => { const el = document.getElementById(id); const cd = cds['ability_' + (i + 1)] ?? 0; const maxCd = abMaxes[i]; const fill = cd <= 0 ? 100 : (1 - cd / maxCd) * 100; el.querySelector('.cd-fill').style.height = fill + '%'; el.classList.toggle('ready', cd <= 0); el.classList.remove('locked'); }); const elE = document.getElementById('hud-se'); const cdE = cds['displacement'] ?? 0; const maxCdE = DISPLACEMENT_COOLDOWNS[local.class] ?? 6; const fillE = cdE <= 0 ? 100 : (1 - cdE / maxCdE) * 100; elE.querySelector('.cd-fill').style.height = fillE + '%'; elE.classList.toggle('ready', cdE <= 0); elE.classList.remove('locked', 'divine-ready'); } function _updateBuffTags(local) { const buffsEl = document.getElementById('hud-buffs'); buffsEl.innerHTML = ''; for (const b of (local.buffs ?? [])) { const label = BUFF_LABELS[b.type]; if (!label) continue; const tag = document.createElement('span'); tag.className = 'buff-tag ' + b.type; tag.textContent = label; buffsEl.appendChild(tag); } } export function updateSoulgateBar(sg) { if (!sg) return; const pct = sg.max_hp > 0 ? Math.max(0, sg.hp / sg.max_hp * 100) : 0; document.getElementById('sg-fill').style.width = pct + '%'; document.getElementById('sg-hp-text').textContent = sg.hp + ' / ' + sg.max_hp; } export function updateWaveInfo(wave) { if (!wave) return; document.getElementById('wave-label').textContent = 'Vague ' + wave.number + ' / 3'; document.getElementById('wave-state-label').textContent = WAVE_STATES[wave.state] ?? wave.state; document.getElementById('wave-enemies').textContent = 'Ennemis : ' + (wave.enemies_remaining ?? 0); } export function updateBossBar(wave) { const isBoss = wave.state === 'boss' && wave.boss_max_hp > 0; document.getElementById('boss-bar').classList.toggle('hidden', !isBoss); if (isBoss) { document.getElementById('boss-bar-name').textContent = wave.boss_name.toUpperCase(); const pct = Math.max(0, wave.boss_hp / wave.boss_max_hp * 100); document.getElementById('boss-bar-fill').style.width = pct + '%'; document.getElementById('boss-bar-hp').textContent = wave.boss_hp + ' / ' + wave.boss_max_hp; } } export function updateUpgradePanel(wave, local) { const inPrep = wave.state === 'preparation'; document.getElementById('upgrade-panel').classList.toggle('hidden', !inPrep); // pendant la prep on bloque les inputs pour focus sur le menu setInputBlocked(inPrep || gameOver); if (inPrep) { document.getElementById('prep-timer').textContent = Math.ceil(wave.prep_timer); const souls = local?.souls ?? 0; document.getElementById('soul-count').textContent = souls; _updateUpgradeList(souls, local?.upgrades ?? {}); } } let _upgradeListBuilt = false; let _upgradeClickBound = false; function _updateUpgradeList(souls, upgrades) { // on construit la liste 1 seule fois et on met juste a jour les stacks/disabled apres // sinon on detruit les boutons pendant qu'on clique dessus const list = document.getElementById('upgrade-list'); if (!_upgradeListBuilt) { list.innerHTML = ''; for (const [id, spec] of Object.entries(UPGRADE_CATALOG)) { const item = document.createElement('div'); item.className = 'upgrade-item'; item.dataset.id = id; item.innerHTML = `
${spec.name}
${spec.desc}
`; list.appendChild(item); } _upgradeListBuilt = true; } if (!_upgradeClickBound) { // Délégation : 1 seul listener sur la liste, ne meurt jamais avec les boutons list.addEventListener('click', (e) => { const btn = e.target.closest('.upgrade-buy'); if (!btn || btn.disabled) return; send('player_upgrade', { upgrade_id: btn.dataset.id }); }); _upgradeClickBound = true; } for (const [id, spec] of Object.entries(UPGRADE_CATALOG)) { const item = list.querySelector(`.upgrade-item[data-id="${id}"]`); if (!item) continue; const stacks = upgrades[id] ?? 0; const maxed = stacks >= spec.max; item.querySelector('.upgrade-stacks').textContent = '\u25cf'.repeat(stacks) + '\u25cb'.repeat(spec.max - stacks); const btn = item.querySelector('.upgrade-buy'); btn.disabled = maxed || souls < spec.cost; btn.textContent = maxed ? 'MAX' : spec.cost + ' ames'; } } export function resetUpgradeList() { _upgradeListBuilt = false; _upgradeClickBound = false; } let gameOver = false; export function resetGameOver() { gameOver = false; } export function checkGameEnd(msg, gameStartTime, isHost = false, lobbyCode = '', submitterId = '') { if (gameOver) return; const victory = msg.wave?.state === 'victory'; const defeat = msg.soulgate?.hp === 0; if (!victory && !defeat) return; gameOver = true; setInputBlocked(true); const timeSecs = gameStartTime ? Math.floor((Date.now() - gameStartTime) / 1000) : 0; const wavesCompleted = msg.wave?.number ?? 1; const players = msg.players ?? []; const totalKills = players.reduce((s, p) => s + (p.enemies_killed ?? 0), 0); const totalSouls = players.reduce((s, p) => s + (p.souls ?? 0), 0); const score = totalKills * SCORE_PER_KILL + totalSouls * SCORE_PER_SOUL + (victory ? SCORE_VICTORY : 0) + wavesCompleted * SCORE_PER_WAVE; showEndGameForm({ victory, score, timeSecs, wavesCompleted, isHost, lobbyCode, submitterId, players: players.map(p => ({ username: p.username, class: p.class, souls: p.souls ?? 0, enemies_killed: p.enemies_killed ?? 0, })), }); }