// game.js
/**
* The game's rendering surface
* @type {HTMLCanvasElement}
*/
let canvas;
/**
* The main game world instance
* @type {World}
*/
let world;
/**
* Stores the current input state of all relevant keys
* @type {Keyboard}
*/
let keyboard = new Keyboard();
/**
* Timestamp of the most recent user interaction.
* Used for detecting idle time (e.g. long idle animation).
* Starts with a 15s offset to delay initial idle checks.
* @type {number}
*/
let lastInput = new Date().getTime() + 15000;
/**
* Counter for tracking how many images have finished loading.
* Used to control loading screens or game start conditions.
* @type {number}
*/
let loadedImageCount = 0;
/**
* The total number of images required before the game can start.
* @type {number}
*/
let REQUIRED_IMAGE_COUNT = 260;
/**
* Flag indicating whether the world is fully initialized and ready for animation loops.
* Used to synchronize the start of various animation and game loops.
* @type {boolean}
*/
let worldIsReady = false;
/**
* Initializes the game by assigning the canvas element
* and triggering the initial UI instruction toggle sequence.
* Called once on page load.
*/
function init() {
canvas = getElementByIdHelper("canvas");
toggleInstructions();
toggleInstructions();
}
/**
* Increments the loaded image counter and checks if all required images are loaded.
* Once loading is complete, hides the loading screen, starts background music,
* and resets the counter for future loading sequences.
*/
function handleImageLoad() {
loadedImageCount++;
if (loadedImageCount === REQUIRED_IMAGE_COUNT) {
worldIsReady = true;
world.character.initCharacterLoops();
world.characterShadow.initShadowLoops();
hideLoadingScreen();
SoundManager.playOne(SoundManager.MUSIC_BACKGROUND, 1, 0.04, 0, true);
}
}
/**
* Starts the game by preparing the state, initializing the world,
* and setting up all presentation-related elements such as sound, UI, and idle timing.
*/
function startGame() {
loadedImageCount = 0;
worldIsReady = false;
prepareGameState();
initializeWorld();
initializePresentation();
}
/**
* Resets essential game state components before a new round starts.
* Clears previous intervals, restores local storage data, and stops all sounds.
*/
function prepareGameState() {
showLoadingScreen();
loadFromLocalStorage();
SoundManager.stopAll();
stopAllLoops();
}
/**
* Initializes the game world and its level data,
* then creates a new World instance using the canvas and keyboard input.
*/
function initializeWorld() {
intiLevel();
world = new World(canvas, keyboard);
}
/**
* Initializes the visual game state including UI reset and input timing.
* Prevents premature long idle detection by delaying the idle timer.
*/
function initializePresentation() {
resetUi();
blurButton(".btn");
lastInput = new Date().getTime() + 15000;
}
/**
* Restarts the game during active play unless the endscreen has already been triggered.
*/
function restartDuringPlay() {
if (world.endscreenTriggered) return;
startGame();
}
/**
* Quits the game during active play unless the endscreen has already been triggered.
*/
function quitDuringPlay() {
if (world.endscreenTriggered) return;
quitGame();
}
/**
* Quits the game and returns to the start screen.
* Stops all sounds and intervals, resets the UI, and re-enables start interaction.
*/
function quitGame() {
SoundManager.stopAll();
stopAllLoops();
showElementById("start-screen");
resetUi();
blurButton(".btn");
showInstructions();
}
/**
* Clears all active animation frames and intervals.
* Cancels both setInterval-based timers and requestAnimationFrame loops
* to ensure clean game state transitions.
*/
function stopAllLoops() {
intervalIds.forEach(clearInterval);
rAFIds.forEach(cancelAnimationFrame);
}
/**
* Sets up touch event listeners for mobile controls after the page has fully loaded.
* Maps touch interactions to virtual keyboard input (LEFT, RIGHT, SPACE, D),
* and updates the idle timer to avoid triggering long idle animations.
*/
window.addEventListener("load", () => {
getElementByIdHelper("btn-move-left").addEventListener("touchstart", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.LEFT = true;
});
getElementByIdHelper("btn-move-left").addEventListener("touchend", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.LEFT = false;
lastInput = new Date().getTime();
});
getElementByIdHelper("btn-move-right").addEventListener("touchstart", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.RIGHT = true;
});
getElementByIdHelper("btn-move-right").addEventListener("touchend", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.RIGHT = false;
lastInput = new Date().getTime();
});
getElementByIdHelper("btn-jump").addEventListener("touchstart", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.SPACE = true;
});
getElementByIdHelper("btn-jump").addEventListener("touchend", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.SPACE = false;
lastInput = new Date().getTime();
});
getElementByIdHelper("btn-throw").addEventListener("touchstart", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.D = true;
});
getElementByIdHelper("btn-throw").addEventListener("touchend", (e) => {
if (e.cancelable) e.preventDefault();
keyboard.D = false;
lastInput = new Date().getTime();
});
});
/**
* Listens for keyboard keydown events and maps them to the game's virtual keyboard state.
* Enables movement, jumping, and throwing via arrow keys, spacebar, and D key.
*/
document.addEventListener("keydown", (event) => {
switch (event.key) {
case "ArrowRight":
keyboard.RIGHT = true;
break;
case "ArrowLeft":
keyboard.LEFT = true;
break;
case " ":
keyboard.SPACE = true;
break;
case "d":
case "D":
keyboard.D = true;
break;
}
});
/**
* Listens for keyboard keyup events and resets the corresponding input state.
* Also updates the lastInput timestamp to prevent long idle animations.
*/
document.addEventListener("keyup", (event) => {
lastInput = new Date().getTime();
switch (event.key) {
case "ArrowRight":
keyboard.RIGHT = false;
break;
case "ArrowLeft":
keyboard.LEFT = false;
break;
case " ":
keyboard.SPACE = false;
break;
case "d":
case "D":
keyboard.D = false;
break;
}
});