2026-05-04 03:42:47 +02:00

219 lines
7.6 KiB
JavaScript

// 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 = `
<div class="upgrade-info">
<div class="upgrade-name">${spec.name}</div>
<div class="upgrade-desc">${spec.desc}</div>
<div class="upgrade-stacks"></div>
</div>
<button class="upgrade-buy" data-id="${id}"></button>`;
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,
})),
});
}