248 lines
9.6 KiB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
|
|
// 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';
|
|
}
|
|
}
|