// game.js - round management, timer, submit import { submitScore } from "./api.js"; import { getRandomCountries, loadCountries } from "./countries.js"; import { clear, getPoints, init as initDrawing, setCities } 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; /** 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"); 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)); 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"); setCities(country.cities || []); clear(); timeLeft = ROUND_DURATION; updateTimerUI(); clearInterval(timerInterval); timerInterval = setInterval(tickTimer, 1000); elBtnSubmit.disabled = false; elBtnSubmit.textContent = currentRound < TOTAL_ROUNDS - 1 ? "Submit & Next Round →" : "Submit & 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 advance or finish. * @param {boolean} [auto=false] - True when triggered by timer expiry. */ function submitRound(auto = false) { clearInterval(timerInterval); elBtnSubmit.disabled = true; const points = getPoints(); const score = calculateScore(points); scores.push(score); updateScoreDisplay(currentRound, score); showScoreFeedback(score); setTimeout( () => { if (currentRound < TOTAL_ROUNDS - 1) { currentRound++; startRound(); } else { finishGame(); } }, auto ? 400 : 1200, ); } /** * Briefly display the score grade overlay on the canvas. * @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)"; setTimeout(() => { el.style.opacity = "0"; el.style.transform = "translateY(-10px)"; }, 900); } /** 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);