2026-06-01 08:40:23 +02:00

205 lines
5.2 KiB
JavaScript

// 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);