238 lines
6.1 KiB
JavaScript
238 lines
6.1 KiB
JavaScript
// 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);
|