274 lines
7.9 KiB
JavaScript
274 lines
7.9 KiB
JavaScript
// Debug client — gestion WebSocket, lobby et affichage live du game state
|
|
|
|
const WS_URL = `ws://${location.hostname}:8000/ws`;
|
|
|
|
let ws = null;
|
|
let isHost = false;
|
|
let inLobby = false;
|
|
let gameStarted = false;
|
|
let myConnId = null; // identifié via lobby_created / lobby_joined
|
|
let lastTickTime = null;
|
|
let lastTick = null;
|
|
|
|
|
|
// Connexion
|
|
|
|
function connect() {
|
|
ws = new WebSocket(WS_URL);
|
|
ws.onopen = () => setStatus("Connecté", "ok");
|
|
ws.onclose = () => {
|
|
setStatus("Déconnecté", "error");
|
|
gameStarted = false;
|
|
inLobby = false;
|
|
};
|
|
ws.onerror = () => setStatus("Erreur WebSocket", "error");
|
|
ws.onmessage = (e) => {
|
|
try {
|
|
handleMessage(JSON.parse(e.data));
|
|
} catch {
|
|
log("⚠ message non-JSON reçu");
|
|
}
|
|
};
|
|
}
|
|
|
|
function send(type, data = {}) {
|
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify({ type, ...data }));
|
|
}
|
|
}
|
|
|
|
|
|
// Dispatch des messages
|
|
|
|
function handleMessage(msg) {
|
|
if (msg.type === "game_state") {
|
|
updateGameState(msg);
|
|
return; // pas de log à 20 Hz
|
|
}
|
|
if (msg.type === "error") {
|
|
log(`⚠ ${msg.code}: ${msg.message}`, "err");
|
|
return;
|
|
}
|
|
log(`← ${msg.type}: ${JSON.stringify(msg).slice(0, 100)}`);
|
|
|
|
switch (msg.type) {
|
|
case "username_set": onUsernameSet(msg); break;
|
|
case "lobby_created": onLobbyCreated(msg); break;
|
|
case "lobby_joined": onLobbyJoined(msg); break;
|
|
case "player_joined": onPlayerJoined(msg); break;
|
|
case "player_left": onPlayerLeft(msg); break;
|
|
case "class_selected": onClassSelected(msg); break;
|
|
case "player_ready": onPlayerReady(msg); break;
|
|
case "game_starting": onGameStarting(msg); break;
|
|
}
|
|
}
|
|
|
|
|
|
// Handlers lobby
|
|
|
|
function onUsernameSet(msg) {
|
|
$("create-btn").disabled = false;
|
|
$("join-btn").disabled = false;
|
|
log(`Pseudo défini : ${msg.username}`, "ok");
|
|
}
|
|
|
|
function onLobbyCreated(msg) {
|
|
isHost = true;
|
|
inLobby = true;
|
|
$("code-input").value = msg.code;
|
|
$("class-kael").disabled = false;
|
|
$("class-seris").disabled = false;
|
|
$("class-aldric").disabled = false;
|
|
$("start-btn").disabled = false;
|
|
log(`Lobby créé — code : ${msg.code}`, "ok");
|
|
renderLobbyPlayers([{ username: $("username-input").value, player_class: null, ready: false, host: true }]);
|
|
}
|
|
|
|
function onLobbyJoined(msg) {
|
|
inLobby = true;
|
|
$("code-input").value = msg.code;
|
|
$("class-kael").disabled = false;
|
|
$("class-seris").disabled = false;
|
|
$("class-aldric").disabled = false;
|
|
renderLobbyPlayers(msg.players);
|
|
}
|
|
|
|
function onPlayerJoined(msg) {
|
|
refreshLobbyFromEvent("player_joined", msg.player);
|
|
}
|
|
|
|
function onPlayerLeft(msg) {
|
|
refreshLobbyFromEvent("player_left", { id: msg.player_id });
|
|
}
|
|
|
|
function onClassSelected(msg) {
|
|
// met à jour le bouton de classe si c'est nous
|
|
if (msg.player_id === myConnId || !myConnId) {
|
|
["kael", "seris", "aldric"].forEach(c => {
|
|
$(`class-${c}`).classList.toggle("active", c === msg.class);
|
|
});
|
|
}
|
|
}
|
|
|
|
function onPlayerReady(msg) {
|
|
if (msg.player_id === myConnId || !myConnId) {
|
|
$("ready-btn").classList.add("active");
|
|
$("ready-btn").textContent = "✓ Prêt";
|
|
}
|
|
}
|
|
|
|
function onGameStarting(msg) {
|
|
gameStarted = true;
|
|
log(`▶ Partie démarrée ! (countdown: ${msg.countdown}s)`, "ok");
|
|
$("streaming").textContent = "● streaming 20 Hz";
|
|
$("streaming").style.color = "#4caf50";
|
|
}
|
|
|
|
|
|
// Game state
|
|
|
|
function updateGameState(msg) {
|
|
// Tick + estimation du tick rate
|
|
const now = performance.now();
|
|
if (lastTick !== null && lastTickTime !== null) {
|
|
const dt = now - lastTickTime;
|
|
const rate = (1000 / dt).toFixed(0);
|
|
$("tick-rate").textContent = `~${rate}/s`;
|
|
}
|
|
lastTick = msg.tick;
|
|
lastTickTime = now;
|
|
$("tick").textContent = msg.tick;
|
|
|
|
// Soulgate
|
|
const sg = msg.soulgate;
|
|
const sgPct = Math.round(sg.hp / sg.max_hp * 100);
|
|
$("sg-hp").textContent = `${sg.hp} / ${sg.max_hp}`;
|
|
$("sg-pct").textContent = `${sgPct}%`;
|
|
$("sg-bar").style.width = `${sgPct}%`;
|
|
|
|
// Vague
|
|
const w = msg.wave;
|
|
const badge = $("wave-info");
|
|
badge.innerHTML = `<span class="wave-badge ${w.state}">Vague ${w.number} — ${w.state}${w.enemies_remaining > 0 ? ` (${w.enemies_remaining} ennemis)` : ""}</span>`;
|
|
|
|
// Joueurs
|
|
const container = $("players-list");
|
|
container.innerHTML = msg.players.map(p => {
|
|
const pct = p.alive ? Math.round(p.hp / p.max_hp * 100) : 0;
|
|
return `
|
|
<div class="player-card ${p.alive ? "" : "dead"}">
|
|
<div class="phead">
|
|
<span class="pname">${p.username}</span>
|
|
<span class="pclass">${p.class}</span>
|
|
</div>
|
|
<div style="display:flex; justify-content:space-between">
|
|
<span class="php">${p.hp} / ${p.max_hp} HP</span>
|
|
<span style="color:#444; font-size:11px">${p.alive ? pct + "%" : "mort"}</span>
|
|
</div>
|
|
<div class="hp-bar">
|
|
<div class="hp-fill ${p.alive ? p.class : "dead"}" style="width:${pct}%"></div>
|
|
</div>
|
|
</div>`;
|
|
}).join("");
|
|
}
|
|
|
|
|
|
// Helpers UI
|
|
|
|
function setStatus(text, cls = "") {
|
|
const el = $("status");
|
|
el.textContent = text;
|
|
el.className = "status " + cls;
|
|
}
|
|
|
|
function log(text, cls = "") {
|
|
const pre = $("log");
|
|
const ts = new Date().toLocaleTimeString("fr");
|
|
const line = document.createElement("span");
|
|
line.className = cls;
|
|
line.textContent = `[${ts}] ${text}\n`;
|
|
pre.appendChild(line);
|
|
pre.scrollTop = pre.scrollHeight;
|
|
}
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
|
|
let _lobbyPlayers = [];
|
|
|
|
function renderLobbyPlayers(players) {
|
|
_lobbyPlayers = players;
|
|
const container = $("lobby-players");
|
|
if (!players.length) {
|
|
container.innerHTML = `<span style="color:#333">Aucun joueur</span>`;
|
|
return;
|
|
}
|
|
container.innerHTML = players.map(p => `
|
|
<div class="lobby-player">
|
|
<span class="pname">${p.username ?? p.id ?? "?"}</span>
|
|
<span class="pclass">${p.class ?? p.player_class ?? "—"}</span>
|
|
${p.ready ? `<span class="pready">✓</span>` : ""}
|
|
${p.host ? `<span class="phost">[chef]</span>` : ""}
|
|
</div>
|
|
`).join("");
|
|
}
|
|
|
|
function refreshLobbyFromEvent(eventType, data) {
|
|
// re-render best-effort sans refetch complet
|
|
if (eventType === "player_joined") {
|
|
_lobbyPlayers.push(data);
|
|
} else if (eventType === "player_left") {
|
|
_lobbyPlayers = _lobbyPlayers.filter(p => p.id !== data.id);
|
|
}
|
|
renderLobbyPlayers(_lobbyPlayers);
|
|
}
|
|
|
|
|
|
// Event listeners
|
|
|
|
$("username-btn").addEventListener("click", () => {
|
|
const v = $("username-input").value.trim();
|
|
if (v) send("set_username", { username: v });
|
|
});
|
|
|
|
$("username-input").addEventListener("keydown", e => {
|
|
if (e.key === "Enter") $("username-btn").click();
|
|
});
|
|
|
|
$("create-btn").addEventListener("click", () => send("create_lobby"));
|
|
|
|
$("join-btn").addEventListener("click", () => {
|
|
const code = $("code-input").value.trim().toUpperCase();
|
|
if (code) send("join_lobby", { code });
|
|
});
|
|
|
|
$("code-input").addEventListener("keydown", e => {
|
|
if (e.key === "Enter") $("join-btn").click();
|
|
});
|
|
|
|
["kael", "seris", "aldric"].forEach(cls => {
|
|
$(`class-${cls}`).addEventListener("click", () => {
|
|
send("select_class", { class: cls });
|
|
});
|
|
});
|
|
|
|
$("ready-btn").addEventListener("click", () => {
|
|
$("ready-btn").disabled = true;
|
|
send("ready");
|
|
});
|
|
|
|
$("start-btn").addEventListener("click", () => send("start_game"));
|
|
|
|
// Activer Ready dès qu'une classe est sélectionnée
|
|
["kael", "seris", "aldric"].forEach(cls => {
|
|
$(`class-${cls}`).addEventListener("click", () => {
|
|
$("ready-btn").disabled = false;
|
|
$("ready-btn").classList.remove("active");
|
|
$("ready-btn").textContent = "Prêt";
|
|
});
|
|
});
|
|
|
|
|
|
// Init
|
|
|
|
connect();
|