SOULGATE/client/js/leaderboard.js
2026-05-04 03:42:47 +02:00

248 lines
9.6 KiB
JavaScript

// leaderboard.js — Affichage du classement et soumission d'un résultat
const API = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:8000'
: `http://${window.location.hostname}:8000`;
const CLASS_ICONS = { kael: '⚔️', seris: '🗡️', aldric: '🌿' };
// Leaderboard screen
export async function showLeaderboard() {
document.getElementById('home-screen').classList.add('hidden');
const screen = document.getElementById('leaderboard-screen');
screen.classList.remove('hidden');
await _loadAndRender();
}
export function hideLeaderboard() {
document.getElementById('leaderboard-screen').classList.add('hidden');
document.getElementById('home-screen').classList.remove('hidden');
}
async function _loadAndRender() {
const tbody = document.getElementById('lb-tbody');
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;opacity:.5">Chargement...</td></tr>';
try {
const res = await fetch(`${API}/leaderboard`);
const rows = await res.json();
_renderTable(rows);
} catch {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#f66">Impossible de charger le classement.</td></tr>';
}
}
function _renderTable(rows) {
const tbody = document.getElementById('lb-tbody');
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;opacity:.5">Aucune partie enregistrée.</td></tr>';
return;
}
tbody.innerHTML = rows.map((r, i) => {
const time = _formatTime(r.time_seconds);
const vic = r.victory ? '✓' : '✗';
const discords = [
r.p1_discord ? `@${r.p1_discord}` : (r.p1_username || ''),
r.p2_discord ? `@${r.p2_discord}` : (r.p2_username || ''),
r.p3_discord ? `@${r.p3_discord}` : (r.p3_username || ''),
].filter(Boolean).join(' · ');
return `<tr class="${i === 0 ? 'lb-first' : ''}">
<td class="lb-rank">#${i + 1}</td>
<td class="lb-team">${_esc(r.team_name)}</td>
<td class="lb-score">${r.score.toLocaleString()}</td>
<td>${time}</td>
<td>${vic} ${r.waves_completed}/3</td>
<td class="lb-players">${_esc(discords)}</td>
</tr>`;
}).join('');
}
function _formatTime(s) {
const m = Math.floor(s / 60);
const sec = String(s % 60).padStart(2, '0');
return `${m}m${sec}s`;
}
function _esc(s) {
return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// End-game form
export function showEndGameForm(data) {
// data = { victory, score, timeSecs, wavesCompleted, players: [{username, class, souls, enemies_killed}] }
const overlay = document.getElementById('endgame-overlay');
const title = document.getElementById('endgame-title');
const sub = document.getElementById('endgame-sub');
overlay.classList.remove('hidden');
title.textContent = data.victory ? 'VICTOIRE !' : 'DÉFAITE';
title.className = data.victory ? 'victory' : 'defeat';
sub.textContent = data.victory
? 'Le Soulgate est sauvegardé.'
: 'Le Soulgate a été détruit.';
document.getElementById('endgame-score').textContent = `Score : ${data.score.toLocaleString()} pts`;
document.getElementById('endgame-time').textContent = `Temps : ${_formatTime(data.timeSecs)}`;
document.getElementById('endgame-waves').textContent = `Vagues : ${data.wavesCompleted}/3`;
_buildDiscordForm(data);
}
function _buildDiscordForm(data) {
const form = document.getElementById('endgame-form');
form.innerHTML = '';
// Defaite : pas de soumission au leaderboard, juste rejouer
if (!data.victory) {
const info = document.createElement('div');
info.className = 'lb-host-only';
info.textContent = "Défaite : le score n'est pas enregistré dans le leaderboard.";
form.appendChild(info);
const replayBtn = document.createElement('button');
replayBtn.className = 'mc-btn';
replayBtn.textContent = 'Rejouer';
replayBtn.style.marginTop = '8px';
replayBtn.addEventListener('click', () => location.reload());
form.appendChild(replayBtn);
return;
}
// Seul le chef d'équipe peut soumettre le score → les autres voient un message + Rejouer
if (!data.isHost) {
const info = document.createElement('div');
info.className = 'lb-host-only';
info.textContent = "Seul le chef d'équipe peut enregistrer le score dans le leaderboard.";
form.appendChild(info);
const replayBtn = document.createElement('button');
replayBtn.className = 'mc-btn';
replayBtn.textContent = 'Rejouer';
replayBtn.style.marginTop = '8px';
replayBtn.addEventListener('click', () => location.reload());
form.appendChild(replayBtn);
return;
}
// Champ nom d'équipe
const teamRow = document.createElement('div');
teamRow.className = 'lb-field';
teamRow.innerHTML = `<label>Nom d'équipe</label><input id="lb-team-name" type="text" maxlength="30" placeholder="Ex: Dark Angels" autocomplete="off">`;
form.appendChild(teamRow);
// Un champ Discord par joueur
data.players.forEach((p, i) => {
const icon = CLASS_ICONS[p.class] ?? '';
const row = document.createElement('div');
row.className = 'lb-field';
row.innerHTML = `
<label>${icon} ${_esc(p.username)} (${p.class}) — ${p.enemies_killed} kills · ${p.souls} âmes</label>
<input class="lb-discord-input" data-idx="${i}" type="text" maxlength="40"
placeholder="Discord (optionnel)" autocomplete="off">
<span class="lb-discord-error" data-idx="${i}"></span>`;
form.appendChild(row);
});
// Bouton soumettre
const submitBtn = document.createElement('button');
submitBtn.id = 'lb-submit-btn';
submitBtn.className = 'mc-btn mc-btn-gold';
submitBtn.textContent = 'Enregistrer dans le leaderboard';
form.appendChild(submitBtn);
// Bouton rejouer
const replayBtn = document.createElement('button');
replayBtn.className = 'mc-btn';
replayBtn.textContent = 'Rejouer';
replayBtn.style.marginTop = '8px';
replayBtn.addEventListener('click', () => location.reload());
form.appendChild(replayBtn);
// Vérification Discord à la saisie
form.querySelectorAll('.lb-discord-input').forEach(input => {
let _debounce;
input.addEventListener('input', () => {
clearTimeout(_debounce);
_debounce = setTimeout(() => _checkDiscord(input), 500);
});
});
submitBtn.addEventListener('click', () => _submitForm(data, form, submitBtn));
}
async function _checkDiscord(input) {
const tag = input.value.trim().replace(/^@/, '');
const errEl = input.parentElement.querySelector('.lb-discord-error');
if (!tag) { errEl.textContent = ''; input.classList.remove('taken'); return; }
try {
const res = await fetch(`${API}/leaderboard/check-discord/${encodeURIComponent(tag)}`);
const json = await res.json();
if (json.taken) {
errEl.textContent = '❌ Ce Discord est déjà dans le leaderboard — choisis-en un autre.';
input.classList.add('taken');
} else {
errEl.textContent = '✓';
input.classList.remove('taken');
}
} catch {
errEl.textContent = '';
}
}
async function _submitForm(data, form, btn) {
// Bloquer si un Discord est déjà pris
if (form.querySelector('.lb-discord-input.taken')) {
alert('Un ou plusieurs Discord sont déjà utilisés dans le leaderboard.');
return;
}
const teamName = document.getElementById('lb-team-name').value.trim();
if (!teamName) { alert("Entre un nom d'équipe."); return; }
const discordInputs = [...form.querySelectorAll('.lb-discord-input')];
const players = data.players;
const payload = {
team_name: teamName,
score: data.score,
time_seconds: data.timeSecs,
waves_completed: data.wavesCompleted,
victory: data.victory,
lobby_code: data.lobbyCode ?? '', // enforcement host-only côté serveur
submitter_id: data.submitterId ?? '',
p1_username: players[0]?.username ?? '', p1_class: players[0]?.class ?? '',
p1_discord: (discordInputs[0]?.value ?? '').trim().replace(/^@/, ''),
p2_username: players[1]?.username ?? '', p2_class: players[1]?.class ?? '',
p2_discord: (discordInputs[1]?.value ?? '').trim().replace(/^@/, ''),
p3_username: players[2]?.username ?? '', p3_class: players[2]?.class ?? '',
p3_discord: (discordInputs[2]?.value ?? '').trim().replace(/^@/, ''),
};
btn.disabled = true;
btn.textContent = 'Envoi...';
try {
const res = await fetch(`${API}/leaderboard/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const json = await res.json();
if (!res.ok) {
alert(json.detail ?? 'Erreur lors de l\'envoi.');
btn.disabled = false;
btn.textContent = 'Enregistrer dans le leaderboard';
return;
}
btn.textContent = `✓ Enregistré ! Vous êtes #${json.rank} au classement.`;
btn.style.background = '#2a7';
} catch {
alert('Erreur réseau — impossible de contacter le serveur.');
btn.disabled = false;
btn.textContent = 'Enregistrer dans le leaderboard';
}
}