// game.js - round management, timer, submit import { submitScore } from "./api.js"; import { getRandomCountries, loadCountries } from "./countries.js"; import { clear, getNormalizedRings, init as initDrawing, setCities, showReferenceOutline, } from "./drawing.js"; import { calculateScore, getGrade } from "./scoring.js"; import { getLobbyName, getPlayerName, saveGameState } from "./storage.js"; const TOTAL_ROUNDS = 3; const ROUND_DURATION = 60; // seconds /** @type {import('./countries.js').Country[]} */ let roundCountries = []; let currentRound = 0; /** @type {number[]} */ let scores = []; let timerInterval = null; let timeLeft = ROUND_DURATION; let elCountryName; let elCountryHint; let elRoundNum; let elTimerNum; let elTimerBar; let elTimerWrap; let elBtnClear; let elBtnSubmit; let elBtnNext; let roundSubmitted = false; /** Initialise DOM refs, drawing, events, countries, and the first round. */ async function initGame() { elCountryName = document.getElementById("country-name"); elCountryHint = document.getElementById("country-hint"); elRoundNum = document.getElementById("round-num"); elTimerNum = document.getElementById("timer-num"); elTimerBar = document.getElementById("timer-bar"); elTimerWrap = document.querySelector(".game-timer"); elBtnClear = document.getElementById("btn-clear"); elBtnSubmit = document.getElementById("btn-submit"); elBtnNext = document.getElementById("btn-next"); const canvas = document.getElementById("draw-canvas"); const wrap = document.getElementById("canvas-wrap"); initDrawing(canvas); document.getElementById("player-name-display").textContent = getPlayerName(); canvas.addEventListener("pointerdown", () => { wrap.classList.add("has-drawing"); }); elBtnClear.addEventListener("click", () => clear()); elBtnSubmit.addEventListener("click", () => submitRound(false)); elBtnNext.addEventListener("click", goToNextRound); await loadCountries(); roundCountries = getRandomCountries(TOTAL_ROUNDS); currentRound = 0; scores = []; startRound(); } /** Set up UI and timer for the current round. */ function startRound() { const country = roundCountries[currentRound]; elRoundNum.textContent = currentRound + 1; elCountryName.textContent = country.name; elCountryHint.textContent = country.hint || ""; updateRoundPips(currentRound + 1); document.getElementById("canvas-wrap")?.classList.remove("has-drawing"); hideScoreFeedback(); setCities(country.cities || []); clear(); roundSubmitted = false; timeLeft = ROUND_DURATION; updateTimerUI(); clearInterval(timerInterval); timerInterval = setInterval(tickTimer, 1000); elBtnSubmit.disabled = false; elBtnSubmit.hidden = false; elBtnSubmit.textContent = "Submit"; elBtnClear.disabled = false; elBtnNext.disabled = true; elBtnNext.textContent = currentRound < TOTAL_ROUNDS - 1 ? "Next Round →" : "See Results →"; } /** Decrement timer by one second and auto-submit when time runs out. */ function tickTimer() { timeLeft--; updateTimerUI(); if (timeLeft <= 0) { clearInterval(timerInterval); submitRound(true); } } /** * Sync timer bar width and apply urgency CSS classes. * Uses `.timer--warning` and `.timer--danger` instead of inline styles. */ function updateTimerUI() { elTimerNum.textContent = timeLeft; elTimerBar.style.width = `${(timeLeft / ROUND_DURATION) * 100}%`; elTimerWrap.classList.toggle("timer--danger", timeLeft <= 10); elTimerWrap.classList.toggle( "timer--warning", timeLeft > 10 && timeLeft <= 20, ); } /** * Submit the current round, record the score, and reveal the reference outline. * @param {boolean} [auto=false] - True when triggered by timer expiry. */ function submitRound(auto = false) { if (roundSubmitted) return; clearInterval(timerInterval); roundSubmitted = true; elBtnSubmit.disabled = true; elBtnClear.disabled = true; const country = roundCountries[currentRound]; const rings = getNormalizedRings(); const score = calculateScore(rings, country.outline); scores.push(score); showReferenceOutline(country.outline); updateScoreDisplay(currentRound, score); showScoreFeedback(score); elBtnNext.disabled = false; elBtnNext.focus(); if (auto) { elBtnNext.textContent = currentRound < TOTAL_ROUNDS - 1 ? "Time's up - Next Round" : "Time's up - See Results"; } } /** Move to the next round or finish the game after a submitted round. */ function goToNextRound() { if (!roundSubmitted) return; elBtnNext.disabled = true; if (currentRound < TOTAL_ROUNDS - 1) { currentRound++; startRound(); } else { finishGame(); } } /** * Display the score grade overlay on the canvas until the next round starts. * @param {number} score */ function showScoreFeedback(score) { const grade = getGrade(score); const el = document.getElementById("score-feedback"); el.textContent = `${score}% ${grade.label}`; el.style.color = grade.color; el.style.opacity = "1"; el.style.transform = "translateY(0)"; } /** Hide the score grade overlay. */ function hideScoreFeedback() { const el = document.getElementById("score-feedback"); el.style.opacity = "0"; el.style.transform = "translateY(-10px)"; } /** Persist game state, update leaderboard, and navigate to results. */ async function finishGame() { const totalScore = scores.reduce((sum, s) => sum + s, 0); const state = { currentRound: TOTAL_ROUNDS, scores, totalScore, countries: roundCountries.map((c) => c.name), }; saveGameState(state); try { await submitScore({ lobbyName: getLobbyName(), playerName: getPlayerName(), totalScore, scores, countries: state.countries, }); } catch (error) { console.warn("Could not submit score to backend.", error); } location.href = "results.html"; } /** @param {number} round */ function updateRoundPips(round) { for (let i = 1; i <= 3; i++) { const pip = document.getElementById(`pip-${i}`); pip.className = `round-pip${i < round ? " done" : ""}${ i === round ? " active" : "" }`; } } /** * @param {number} roundIdx * @param {number} score */ function updateScoreDisplay(roundIdx, score) { const el = document.getElementById(`score-r${roundIdx + 1}`); if (el) { el.textContent = `${score}%`; el.classList.add("filled"); } } window.addEventListener("DOMContentLoaded", initGame);