Writing Classes · Methods & Parameters · Instantiation · Inheritance · Method Overriding · Constructor Chaining
Inheritance lets one class extend another, so it automatically gets all of the parent's properties and methods without rewriting them. The game has two inheritance chains. One controls the boss enemy, one controls the levels. Without inheritance, every level would need its own copy of the laser, HUD, and collision code — that's hundreds of duplicate lines.
Run this to see how a child class automatically gets the parent's methods. Try adding your own method to PeppaBossEnemy.
A class is a blueprint. You define it once, and then you can create as many objects from it as you want. Each object gets its own copy of all the properties. In the game, PeppaBossEnemy is the blueprint for the boss — it stores the boss's health, speed, and defeat state. The constructor runs once when the boss is created and sets all the starting values.
class PeppaBossEnemy extends Enemy {
constructor(data, gameEnv) {
super(data, gameEnv); // run the parent's setup first
this.health = data?.health ?? 3; // how many hits the boss can take
this.maxHealth = this.health; // saved so the HUD can show "3/3"
this.moveSpeed = data?.moveSpeed ?? 0.8; // pixels per frame toward the player
this.isDefeated = false; // turns true when health hits 0
this.isVulnerable = true; // false during the 300ms cooldown after a hit
}
}
data?.health ?? 3 means: "use data.health if it exists, otherwise default to 3." This lets George Pig and Daddy Pig both use the same class but with different health values — you just pass a different data object when creating each one.
Run this to see how a class acts as a blueprint. Try creating a third boss with different values.
When a child class has its own constructor, the very first thing it must do is call super(). super() runs the parent's constructor — it passes the arguments up the chain so the parent can do its setup before the child adds anything on top. Without calling super() first, this doesn't exist yet and JavaScript throws a ReferenceError.
super(data, gameEnv) here means: "run Enemy's constructor with these same two arguments." Enemy then calls super() again, which runs GameObject's constructor. By the time that chain finishes, the boss has a canvas element, a loaded sprite image, and a position on screen — all set up by the parent classes. Only after that does PeppaBossEnemy's own constructor run and add this.health, this.moveSpeed, etc.
class PeppaBossEnemy extends Enemy {
constructor(data, gameEnv) {
super(data, gameEnv); // runs Enemy's constructor → which runs GameObject's constructor
// after this line: canvas, sprite image, and position all exist
this.health = data?.health ?? 3;
this.moveSpeed = data?.moveSpeed ?? 0.8;
this.isDefeated = false;
}
}
Each level passes a different config object to super(). The base class constructor reads those values and sets up the laser system, HUD, and collisions once. PeppaLevel1 and PeppaLevel3 are completely different fights but their entire constructor is just one line.
class PeppaLevel1 extends PeppaBattleLevelBase {
constructor(gameEnv) {
super(gameEnv, { enemyName: 'George Pig', enemyHealth: 3, enemySpeed: 0.8 });
// super() passes this config up — PeppaBattleLevelBase reads it and builds the level
}
}
class PeppaLevel3 extends PeppaBattleLevelBase {
constructor(gameEnv) {
super(gameEnv, { enemyName: 'Daddy Pig', enemyHealth: 12, enemySpeed: 1.4 });
// same base class, different numbers = completely different fight
}
}
PeppaBattleLevelBase. The child classes don't repeat any of it — they just pass different numbers through super() to get a different fight.
Run this to see constructor chaining with super(). Try changing the name or adding a new property in the child constructor.
Overriding means writing a method in the child class with the same name as one in the parent. When that method is called, the child's version runs instead of the parent's. The game engine calls update() on every object 60 times per second. The parent's update() just draws the sprite. By overriding it, I add boss-specific behavior — chasing the player, stopping when defeated, and drawing the hitbox overlay.
// OVERRIDES Enemy.update() — custom chase + hitbox draw behavior
update() {
this.draw(); // inherited from GameObject
if (this.isDefeated) return; // early exit if dead
const player = this.findPlayer();
if (!player) return;
const moved = this.moveTowardPlayer(player);
if (moved) this.stayWithinCanvas(); // inherited method
if (this.showHitbox) this.drawHitbox();
}
// OVERRIDES Enemy.handleCollisionEvent() — delegated to level logic
handleCollisionEvent() {
// Collision managed by PeppaBattleLevelBase, not the enemy itself
}
update() runs 60 times per second. The override adds boss-specific behavior each frame — draw sprite, stop if defeated, chase the player. Without it, the boss just stands still. handleCollisionEvent() is intentionally empty to prevent the engine's default collision from interfering with the custom damage system.
Run this to see how the child's update() replaces the parent's. Try setting isDefeated = true before calling update.
Instantiation means creating an actual object from a class blueprint using new. Until you write new PeppaBossEnemy(data), the class is just a description — nothing exists in the game yet. The engine reads the this.classes array at the start of each level and calls new on every entry. If PeppaBossEnemy isn't in that array, there is no boss.
// Inside PeppaBattleLevelBase constructor — instantiates all game objects
this.classes = [
{ class: GamEnvBackground, data: image_data_background }, // Object literal
{ class: Player, data: sprite_data_ishan }, // Object literal
{ class: PeppaBossEnemy, data: sprite_data_enemy } // Object literal
];
// sprite_data_enemy is a JSON config object — all required properties
const sprite_data_enemy = {
id: config.enemyName,
greeting: config.enemyGreeting, // String
src: `${path}/images/gamify/${config.enemyImage}`,
SCALE_FACTOR: config.enemyScale ?? 4, // Number
ANIMATION_RATE: 18, // Number
INIT_POSITION: { x: width * 0.72, y: height * 0.66 }, // Object
health: config.enemyHealth, // Number
moveSpeed: config.enemySpeed, // Number
hitbox: { widthPercentage: 0.45, heightPercentage: 0.6 }
};
data. The constructor reads data.health, data.moveSpeed etc. Changing these numbers is all it takes to make a different boss — no other code changes needed.
Run this to see how new creates separate objects from one class. Notice changing george's HP doesn't affect daddy's.
Classes and Methods Homework - Demonstrates object-oriented programming fundamentals, including class design, method implementation, and inheritance — the foundation of the game's class hierarchy (PeppaBossEnemy, PeppaBattleLevelBase, etc.).
A method is a function inside a class. Parameters are its inputs. Below are five methods from the game — each takes specific inputs, does one job, and returns a value other code uses.
// param: target — the player object (has .position, .width, .height)
// returns: true if the boss moved, false if it's already on top of the player
moveTowardPlayer(target) {
const dx = (target.position.x + target.width / 2) - (this.position.x + this.width / 2);
const dy = (target.position.y + target.height / 2) - (this.position.y + this.height / 2);
const distance = Math.hypot(dx, dy);
if (distance > 1) {
this.position.x += (dx / distance) * this.moveSpeed; // divide by distance = normalize
this.position.y += (dy / distance) * this.moveSpeed;
return true;
}
return false;
}
distance before multiplying by moveSpeed keeps the boss at a constant speed regardless of how far away the player is. The return value tells update() whether to call stayWithinCanvas() — no point doing that if the boss didn't move.
// param: amount — how much HP to subtract (default 1)
// returns: true if this hit caused the boss to be defeated
takeDamage(amount = 1) {
if (this.isDefeated) { console.log('[Boss] Already defeated.'); return true; }
if (!this.isVulnerable) { console.log('[Boss] Invulnerable, ignoring.'); return false; }
this.health = Math.max(0, this.health - amount);
this.isVulnerable = false; // start 300ms cooldown window
console.log(`[Boss] Hit! HP: ${this.health}/${this.maxHealth}`);
if (this.health === 0) this.isDefeated = true;
return this.isDefeated;
}
takeDamage on every frame it overlapped — that's up to 10 hits per laser. The 300ms window means each laser can only land one hit.
// isPlayerLaser: true = player fired it (damages boss), false = boss fired it (damages player)
spawnLaser(fromX, fromY, targetX, targetY, isPlayerLaser) {
const dx = targetX - fromX, dy = targetY - fromY;
const len = Math.hypot(dx, dy) || 1;
this.lasers.push({
x: fromX, y: fromY,
vx: (dx / len) * this.laserSpeed, // velocity — added to x each frame
vy: (dy / len) * this.laserSpeed, // velocity — added to y each frame
isPlayerLaser, life: 60
});
}
isPlayerLaser is what tells the collision system which side takes damage. Without it, every laser would hit both the player and the boss.
drawHitbox() {
const ctx = this.gameEnv?.ctx;
if (!ctx) return;
ctx.save();
ctx.strokeStyle = 'rgba(255, 0, 0, 0.85)';
ctx.strokeRect(this.position.x, this.position.y, this.width, this.height);
ctx.restore();
console.log(`[Hitbox] pos=(${Math.round(this.position.x)},${Math.round(this.position.y)})`);
}
widthPercentage: 0.45 fixed it.
toggleHitbox() {
this.showHitbox = !this.showHitbox; // ! flips true→false or false→true
console.log(`[Hitbox] now ${this.showHitbox ? 'ON' : 'OFF'}`);
return this.showHitbox; // caller can read the new state
}
update() checks showHitbox every frame. When true, the overlay draws. Toggling without restarting the game was essential for tuning hitbox sizes during playtesting.
Run this to see takeDamage(amount) in action. Try passing different amounts or setting isVulnerable = false to see the blocked path.
Classes and Methods Homework - Demonstrates object-oriented programming fundamentals, including class design, method implementation, and inheritance — the foundation of the game's class hierarchy (PeppaBossEnemy, PeppaBattleLevelBase, etc.).