// world.class.js
/**
* @class World
*
* Represents the main game world, including all game entities, objects, and the rendering loop.
* Manages game logic such as collision detection, object spawning, and camera movement.
*/
class World {
/**
* The main player character instance.
* @type {Character}
*/
character = new Character();
/**
* The shadow effect that follows the character.
* @type {ObjectShadow}
*/
characterShadow = new ObjectShadow(this.character.x);
/**
* The current level configuration loaded into the world.
* @type {Level}
*/
level = level1;
/**
* Reference to the HTML canvas element used for rendering.
* @type {HTMLCanvasElement}
*/
canvas;
/**
* 2D rendering context for the canvas, used to draw all visual elements.
* @type {CanvasRenderingContext2D}
*/
ctx;
/**
* Tracks the state of keyboard inputs for controlling the game.
* @type {Keyboard}
*/
keyboard;
/**
* Horizontal camera offset in pixels.
* Affects how the world is rendered relative to the viewport.
* @type {number}
*/
cameraX = 0;
/**
* Indicates whether throwing bottles is currently on cooldown.
* Prevents immediate consecutive throws.
* @type {boolean}
*/
bottleOnCooldown = false;
/**
* Indicates whether enemy or object spawning is currently on cooldown.
* @type {boolean}
*/
spawnOnCooldown = false;
/**
* Current number of bottles (ammunition) available to the player.
* @type {number}
*/
bottleAmmo = 0;
/**
* Current number of coins the player is holding.
* This value increases with each collected coin and resets when coins are converted into health
* after the player has taken damage.
* @type {number}
*/
coinAmount = 0;
/**
* Flag indicating whether the game is in a state where the player can start playing.
* @type {boolean}
*/
readyToPlay = true;
/**
* Flag indicating whether the boss fight has been triggered.
* @type {boolean}
*/
bossTriggered = false;
/**
* Flag indicating whether the endscreen sequence has been triggered.
* @type {boolean}
*/
endscreenTriggered = false;
/**
* Flag indicating whether the intro sequence has been played.
* @type {boolean}
*/
introPlayed = false;
/**
* UI health bar representing the player's current health.
* @type {HealthBar}
*/
healthBar = new HealthBar(this.character.health);
/**
* UI element showing the player's coin progress towards earning back health.
* The bar is filled based on {@link World#coinAmount}.
* @type {CoinBar}
*/
coinBar = new CoinBar(0);
/**
* UI element showing the player's current bottle ammunition.
* Each bottle represents 20% of the bar.
* @type {BottleBar}
*/
bottleBar = new BottleBar(this.bottleAmmo * 20);
/**
* Collection of status bars that are always displayed in the UI.
* Typically includes the health bar, coin bar, and bottle bar.
* @type {Array<HealthBar|CoinBar|BottleBar>}
*/
statusBars = [this.healthBar, this.coinBar, this.bottleBar];
/**
* Collection of health bars for bosses, shown during boss fights.
* Empty if no boss is active.
* @type {Array<BossHealthBar>}
*/
bossHealthBars = [];
/**
* All throwable objects currently active in the world, such as bottles in flight.
* @type {Array<ThrowableObject>}
*/
throwableObjects = [];
/**
* All objects displayed on the endscreen after the game ends.
* @type {Array<DrawableObject>}
*/
endscreenObjects = [];
/**
* Horizontal range (in pixels) from the player at which the boss encounter is triggered.
* @type {number}
*/
BOSS_TRIGGER_RANGE = 700;
/**
* Duration of the intro sequence in milliseconds before gameplay resumes.
* @type {number}
*/
INTRO_LENGTH = 4000;
/**
* Flags indicating whether specific minion enemies are currently alive.
* Used to track minion state during boss fights.
* @type {boolean}
*/
minion1IsAlive = false;
minion2IsAlive = false;
minion3IsAlive = false;
/**
* Timestamp of the last frame for delta time calculation.
* @type {number}
*/
lastFrameTime = performance.now();
/**
* Timestamp of the last frame that exceeded minimum frame time.
* @type {number}
*/
lastFrameMax = performance.now();
/**
* Time elapsed since the last frame in normalized units (delta time).
* Used for frame-rate independent animations and updates.
* @type {number}
*/
deltaTime = 0;
/**
* Maximum delta time between frames that exceeded minimum frame time.
* Used for synchronization checks.
* @type {number}
*/
animationDeltaMax = 0;
/**
* Minimum frame time threshold in milliseconds.
* Used to determine if animations are in sync.
* @type {number}
*/
FRAME_TIME_MIN = 1.5;
/**
* Creates a new game world instance and initializes rendering, entities, and game loop.
*
* @param {HTMLCanvasElement} canvas - The HTML canvas element used for rendering the game.
* @param {Keyboard} keyboard - The keyboard input handler for player controls.
*/
constructor(canvas, keyboard) {
this.ctx = canvas.getContext("2d");
this.canvas = canvas;
this.keyboard = keyboard;
this.setWorld();
this.draw();
this.run();
}
/**
* Links the world instance to its main entities, such as the player character and boss.
* This allows these entities to access world data and interact with other objects.
*/
setWorld() {
this.character.setWorld(this);
this.characterShadow.setWorld(this);
this.level.bosses[0].setWorld(this);
this.worldIsReady = true;
}
/**
* Starts the main game loop, synchronized with the display refresh rate via requestAnimationFrame.
* Handles core game logic updates including:
* - Object throwing mechanics
* - Collision detection
* - Event triggers
* - Enemy spawning
* - Frame timing and synchronization
*/
run() {
this.checkThrowObjects();
this.checkCollisions();
this.checkBossTrigger();
this.checkEnemyDefeat();
this.checkBossDefeat();
this.checkCharacterDefeat();
this.checkEnemySpawn();
this.checkChickenSoundTrigger();
this.setDeltas();
setStoppableRAF(() => this.run());
}
/**
* Updates delta time values for frame-rate independent animations.
* Calculates both regular delta time and maximum animation delta time.
*/
setDeltas() {
const now = performance.now();
this.deltaTime = (now - this.lastFrameTime) / 10;
this.animationDeltaMax = (now - this.lastFrameMax) / 10;
this.lastFrameTime = now;
if (this.animationDeltaMax > this.FRAME_TIME_MIN) {
this.lastFrameMax = now;
}
}
/**
* Checks if the game animations are running in sync with the desired frame rate.
* @returns {boolean} True if animations are in sync, false otherwise.
*/
isInSync() {
return this.animationDeltaMax > this.FRAME_TIME_MIN;
}
/**
* Reverses the horizontal movement direction of a given object by inverting its speed.
*
* @param {MovableObject} object - The object whose speed should be reversed.
*/
reverseSpeed(object) {
object.speed = object.speed * -1;
}
/**
* Toggles the sprite's facing direction flag for rendering.
* Switches {@link MovableObject#otherDirection} between `true` and `false`.
*
* @param {MovableObject} object - The object whose sprite direction should be toggled.
*/
toggleSpriteDirection(object) {
if (!object.otherDirection) {
object.otherDirection = true;
} else if (object.otherDirection) {
object.otherDirection = false;
}
}
/**
* Renders all visual elements of the world onto the canvas.
* Clears the canvas, applies camera translations, draws background and movable objects,
* then renders fixed UI elements and schedules the next frame via `requestAnimationFrame`.
*/
draw() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.translate(this.cameraX, 0);
// --- Space for background objects ---
this.addObjectsToMap(this.level.backgroundObjects);
// --- Space for movable objects ---
this.addObjectsToMap(this.level.clouds);
this.addObjectsToMap(this.level.collectibleObjects);
this.addToMap(this.characterShadow);
this.addToMap(this.character);
this.addObjectsToMap(this.level.obstacles);
this.addObjectsToMap(this.level.enemies);
this.addObjectsToMap(this.level.bosses);
this.addObjectsToMap(this.throwableObjects);
this.addObjectsToMap(this.bossHealthBars);
// back
this.ctx.translate(-this.cameraX, 0);
// --- Space for fixed objects ---
this.addObjectsToMap(this.statusBars);
this.addObjectsToMap(this.endscreenObjects);
// forwards
this.ctx.translate(this.cameraX, 0);
this.ctx.translate(-this.cameraX, 0);
requestAnimationFrame(() => {
this.draw();
});
}
/**
* Draws all given drawable objects onto the canvas.
*
* @param {DrawableObject[]} objects - An array of drawable objects to render.
*/
addObjectsToMap(objects) {
objects.forEach((o) => {
this.addToMap(o);
});
}
/**
* Draws a single movable object to the canvas.
* If the object faces the opposite direction, it is flipped before rendering
* and flipped back afterward. Also contains a commented-out call to
* {@link MovableObject#drawFrame} for debugging hitboxes.
*
* @param {MovableObject} mo - The movable object to render.
*/
addToMap(mo) {
if (mo.otherDirection) {
this.flipImage(mo);
}
mo.draw(this.ctx);
// mo.drawFrame(this.ctx);
if (mo.otherDirection) {
this.flipImageBack(mo);
}
}
/**
* Flips the drawing of an object horizontally on the canvas.
* This is done by translating the context by the object's width (to prevent the object
* from appearing to "teleport" when flipped) and scaling horizontally by `-1`,
* then inverting its x-position.
*
* @param {MovableObject} mo - The movable object to flip.
*/
flipImage(mo) {
this.ctx.save();
this.ctx.translate(mo.width, 0);
this.ctx.scale(-1, 1);
mo.x = mo.x * -1;
}
/**
* Restores an object's original horizontal orientation after drawing.
* Reverses both the x-position inversion and the canvas context transformation
* applied in {@link World#flipImage}, ensuring the object is rendered correctly
* for subsequent frames.
*
* @param {MovableObject} mo - The movable object to restore.
*/
flipImageBack(mo) {
mo.x = mo.x * -1;
this.ctx.restore();
}
}