CS111 Requirements — Void Striker Game Level
CS111 Requirements — Void Striker Game Level
Evidence of all CS111 learning objectives demonstrated in GameLevelVoidStriker.js.
Object-Oriented Programming
Writing Classes
The file defines two custom classes. GameLevelVoidStriker serves as the top-level level class consumed by the game engine, and VoidStrikerGame is structured as an IIFE module encapsulating the entire game.
class GameLevelVoidStriker {
constructor(gameEnv) {
let width = gameEnv.innerWidth;
let height = gameEnv.innerHeight;
let path = gameEnv.path;
// ...
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
}
}
Methods & Parameters
Methods throughout the game accept multiple parameters. spawnExplosion takes x, y, and color; fireDirected takes directional vector components sx and sy.
function spawnExplosion(x, y, color) {
for (let i = 0; i < 18; i++) {
const angle = rand(0, Math.PI * 2);
const speed = rand(1, 5);
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
r: rand(1.5, 4),
alpha: 1,
color,
});
}
}
function fireDirected(sx, sy) {
const len = Math.hypot(sx, sy) || 1;
const nx = sx / len, ny = sy / len;
const angle = Math.atan2(ny, nx);
// ...
}
Instantiation & Objects
Game objects (ship, enemies, asteroids, boss, bullets, particles) are instantiated as plain objects with defined properties inside their respective builder/spawner functions.
function buildShip() {
ship = {
x: W / 2, y: H * 0.78,
w: 28, h: 38,
speed: SHIP_CHARS[activeChar].speed,
shootCooldown: 0,
invincible: 0,
thrustFlicker: 0,
};
}
boss = {
x: W / 2,
y: -80,
r: 40 + tier * 2,
hp,
maxHp: hp,
speed: 2.7 + tier * 0.45,
pulse: 0,
tier,
palette: BOSS_PALETTES[tier % BOSS_PALETTES.length],
};
Inheritance (Basic)
GameLevelVoidStriker imports and uses GameEnvBackground, placing it inside this.classes so the game engine can instantiate it via the standard class hierarchy (GameEnvBackground extends the engine’s base class).
import GameEnvBackground from '@assets/js/GameEnginev1.1/essentials/GameEnvBackground.js';
// Inside constructor:
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
Constructor Chaining
The GameLevelVoidStriker constructor receives and immediately uses gameEnv (passed from the engine), which represents a chained initialization pattern — the engine calls new GameLevelVoidStriker(gameEnv) and this constructor delegates further setup to VoidStrikerGame.init(gameEnv).
constructor(gameEnv) {
let width = gameEnv.innerWidth;
let height = gameEnv.innerHeight;
let path = gameEnv.path;
// ...
setTimeout(() => VoidStrikerGame.init(gameEnv), 200);
}
Control Structures
Iteration
Multiple loop styles are used throughout. forEach iterates star layers and particles; a traditional for loop iterates the collision detection arrays in reverse to safely splice elements.
// forEach on layer arrays (updateLayers)
layerFarStars.forEach(s => { s.y = (s.y + s.speed) % H; });
layerMidStars.forEach(s => {
s.y = (s.y + s.speed) % H;
s.twinkleT += s.twinkleRate;
});
// Reverse for-loop for safe splice during collision checks
for (let bi = bullets.length - 1; bi >= 0; bi--) {
const b = bullets[bi];
if (b.life <= 0) continue;
// ...
}
// for-loop for boss tentacle drawing
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2 + Math.sin(boss.pulse + i) * 0.2;
ctx.beginPath();
ctx.moveTo(Math.cos(a) * r * 0.7, Math.sin(a) * r * 0.7);
ctx.lineTo(Math.cos(a) * r * 1.5, Math.sin(a) * r * 1.5);
ctx.stroke();
}
Conditionals
State transitions and game logic are controlled throughout with if/else.
if (gameState === 'playing' && !consoleActive) {
updateShip();
updateBullets();
updateEnemies();
updateBoss();
updateParticles();
checkCollisions();
updateHUD();
// ...
if (lives <= 0) {
gameState = 'dead';
showDeadScreen();
}
} else if (gameState === 'playing' && consoleActive) {
// Game paused — draw last frame frozen
drawEnemies();
drawBoss();
drawBullets();
drawParticles();
drawShip();
}
Nested Conditions
Complex game logic uses multi-level conditionals. The collision handler checks invincibility state, then checks bullet-hits, then contact-hits, all with nested conditions:
if (ship.invincible <= 0) {
for (let bi = enemyBullets.length - 1; bi >= 0; bi--) {
const b = enemyBullets[bi];
const dx = ship.x - b.x, dy = ship.y - b.y;
if (Math.sqrt(dx * dx + dy * dy) < 20) {
enemyBullets.splice(bi, 1);
lives--;
ship.invincible = 90;
spawnExplosion(ship.x, ship.y, '#00eeff');
if (lives <= 0) gameState = 'dead';
break;
}
}
}
The enemy chaser logic also uses nested conditions to choose between chasing and straight-line movement, then further nests shooting behavior:
if (e.chaser) {
const dx = ship.x - e.x;
const dy = ship.y - e.y;
const dist = Math.hypot(dx, dy) || 1;
e.chaseAcc = Math.min(e.speed, e.chaseAcc + 0.0006);
const spd = (e.speed + e.chaseAcc) * worldSpeed;
e.x += (dx / dist) * spd;
e.y += (dy / dist) * spd;
e.angle = Math.atan2(dy, dx);
} else {
e.y += e.speed * worldSpeed;
e.x += e.vx * worldSpeed;
if (e.x < 20 || e.x > W - 20) e.vx *= -1;
}
if (e.shootTimer !== Infinity) {
e.shootTimer -= worldSpeed;
if (e.shootTimer <= 0) {
enemyBullets.push({ x: e.x, y: e.y + e.r, vx: 0, vy: 4 + wave * 0.3, life: 80 });
e.shootTimer = randI(60, 140);
}
}
Data Types
Numbers
Position, velocity, size, health, and score are all tracked numerically.
let wave = 1, lives = 3;
let bestKills = 0;
let totalKills = 0;
let worldSpeed = 1;
ship = {
x: W / 2, y: H * 0.78,
w: 28, h: 38,
speed: SHIP_CHARS[activeChar].speed,
shootCooldown: 0,
invincible: 0,
};
Strings
Character names, sprite state strings, background scene labels, and HSL color strings are all stored and manipulated.
let gameState = 'title'; // 'title' | 'playing' | 'dead'
const BG_SCENES = ['nebula', 'deepspace', 'supernova'];
const SHIP_CHARS = [
{ name: 'Striker', body: '#a0d8ff', cockpit: '#00eeff', thrustRgb: '0,200,255', bullet: '#00eeff', speed: 4.5 },
{ name: 'Shadow', body: '#bb99ee', cockpit: '#cc55ff', thrustRgb: '160,0,255', bullet: '#cc55ff', speed: 5.2 },
// ...
];
// Dynamic color string from numeric hue
color: `hsl(${(hueBase + randI(-15, 15) + 360) % 360},85%,55%)`
Booleans
Boolean flags control invincibility, shooting cooldown checks, enemy type, and pause state.
let consoleActive = false;
const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);
// Boolean used as a guard in collision detection
if (ship.invincible <= 0) { ... }
// Boolean expression controls shooting
if (ship.shootCooldown === 0) {
let sx = 0, sy = 0;
if (keys['ArrowLeft']) sx -= 1;
if (keys['ArrowRight']) sx += 1;
if (sx !== 0 || sy !== 0) {
fireDirected(sx, sy);
ship.shootCooldown = 12;
}
}
Arrays
All game object collections are arrays, and Array.from generates star and asteroid data.
let bullets = [], enemies = [], asteroids = [], particles = [];
let enemyBullets = [];
layerFarStars = Array.from({ length: 200 }, () => ({
x: rand(0, W),
y: rand(0, H),
r: rand(0.3, 0.9),
alpha: rand(0.2, 0.5),
speed: rand(0.08, 0.18),
}));
// Array filter removes dead bullets each frame
bullets = bullets.filter(b => b.life-- > 0);
particles = particles.filter(p => p.alpha > 0.02);
Objects (JSON)
Configuration objects define character skins, boss palettes, and background cloud descriptors.
const SHIP_CHARS = [
{ name: 'Striker', body: '#a0d8ff', cockpit: '#00eeff', thrustRgb: '0,200,255', bullet: '#00eeff', speed: 4.5 },
{ name: 'Shadow', body: '#bb99ee', cockpit: '#cc55ff', thrustRgb: '160,0,255', bullet: '#cc55ff', speed: 5.2 },
{ name: 'Inferno', body: '#ffbb77', cockpit: '#ff5500', thrustRgb: '255,100,0', bullet: '#ff6600', speed: 4.0 },
{ name: 'Nova', body: '#99ffcc', cockpit: '#00ff99', thrustRgb: '0,255,140', bullet: '#00ff99', speed: 4.8 },
];
const image_data_space = {
id: 'VoidStriker-Background',
src: '',
pixels: { height: 570, width: 1025 }
};
Operators
Mathematical
Physics calculations use all four arithmetic operators — gravity accumulates on asteroids, velocity is updated each frame, and distances are computed with Math.sqrt / Math.hypot.
// Gravity system on asteroids
a.verticalVelocity -= a.gravityAcceleration;
if (-a.verticalVelocity > a.terminalVelocity) {
a.verticalVelocity = -a.terminalVelocity;
}
a.y += -a.verticalVelocity * worldSpeed;
// Boss pursuit: distance formula + trig
const dx = ship.x - boss.x;
const dy = ship.y - boss.y;
const angle = Math.atan2(dy, dx);
boss.x += Math.cos(angle) * boss.speed;
boss.y += Math.sin(angle) * boss.speed;
String Operations
Template literals are used to build dynamic CSS colors, HUD labels, and HTML content.
// Dynamic RGBA color string
glow.addColorStop(0, `rgba(${p.glow},0.55)`);
fill.style.background = `linear-gradient(90deg, rgba(${p.glow},1), ${p.bodyHi})`;
// HUD text
s.textContent = `KILLS: ${totalKills}`;
w.textContent = `WAVE: ${wave}`;
// HSL color from numeric hue band
color: `hsl(${(hueBase + randI(-15, 15) + 360) % 360},85%,55%)`
Boolean Expressions
Compound && and || conditions are used throughout for multi-condition guards.
// Compound AND: game must be playing AND console must be inactive
if (gameState === 'playing' && !consoleActive) { ... }
// Compound OR for movement keys
if (keys['a'] || keys['A']) ship.x -= ship.speed;
if (keys['d'] || keys['D']) ship.x += ship.speed;
// Compound: chaser enemies only appear from wave 2 with probability scaling
const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);
Input / Output
Keyboard Input
Arrow keys trigger directional shooting; WASD controls movement; P pauses the game. All handled via window.addEventListener.
function attachInput() {
window.addEventListener('keydown', e => {
if (e.key === 'p' || e.key === 'P') {
if (gameState !== 'playing') return;
if (consoleActive) {
closeConsole();
} else {
openConsole();
}
return;
}
if (!consoleActive) {
keys[e.key] = true;
}
// Prevent default page scroll for game keys
if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'ArrowDown' ||
e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
}
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
}
// Arrow key shooting direction built from pressed keys each frame
let sx = 0, sy = 0;
if (keys['ArrowLeft']) sx -= 1;
if (keys['ArrowRight']) sx += 1;
if (keys['ArrowUp']) sy -= 1;
if (keys['ArrowDown']) sy += 1;
if (sx !== 0 || sy !== 0) {
fireDirected(sx, sy);
ship.shootCooldown = 12;
}
Canvas Rendering
All game objects are drawn using the Canvas 2D API inside dedicated draw* functions. The main game loop calls ctx.clearRect each frame before redrawing.
// Main loop clears and redraws every frame
function loop() {
ctx.clearRect(0, 0, W, H);
updateLayers();
drawLayers();
// ...
drawEnemies();
drawBoss();
drawBullets();
drawParticles();
drawShip();
frameId = requestAnimationFrame(loop);
}
// Ship drawn with Canvas paths, transforms, and gradients
function drawShip() {
ctx.save();
ctx.translate(x, y);
// Thrust flame
const tg = ctx.createLinearGradient(0, h * 0.4, 0, h * 0.4 + fl);
tg.addColorStop(0, `rgba(${ch.thrustRgb},0.9)`);
tg.addColorStop(1, 'transparent');
ctx.fillStyle = tg;
ctx.beginPath();
ctx.moveTo(-w * 0.25, h * 0.35);
ctx.lineTo(0, h * 0.4 + fl);
ctx.lineTo(w * 0.25, h * 0.35);
ctx.fill();
// Hull
ctx.fillStyle = ch.body;
ctx.beginPath();
ctx.moveTo(0, -h / 2);
ctx.lineTo(w / 2, h / 2);
ctx.lineTo(0, h * 0.3);
ctx.lineTo(-w / 2, h / 2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
GameEnv Configuration
GameLevelVoidStriker reads canvas dimensions from gameEnv and configures the background image data, then passes the full gameEnv to VoidStrikerGame.init() for container detection and canvas setup.
constructor(gameEnv) {
let width = gameEnv.innerWidth;
let height = gameEnv.innerHeight;
let path = gameEnv.path;
const image_data_space = {
id: 'VoidStriker-Background',
src: '',
pixels: { height: 570, width: 1025 }
};
setTimeout(() => VoidStrikerGame.init(gameEnv), 200);
this.classes = [
{ class: GameEnvBackground, data: image_data_space },
];
}
// Inside init(), the engine's canvas and container are detected
function init(gameEnv) {
let engineCanvas = (gameEnv && gameEnv.canvas)
|| document.querySelector('canvas[id]')
|| document.querySelector('.game-container canvas')
|| document.querySelector('canvas');
container = engineCanvas
? (engineCanvas.parentElement || document.body)
: (document.querySelector('.game-container') || document.body);
const rect = container.getBoundingClientRect();
W = rect.width || (gameEnv && gameEnv.innerWidth) || 800;
H = rect.height || (gameEnv && gameEnv.innerHeight) || 500;
// ...
}
API Integration
Kill events are dispatched via a custom CustomEvent and could be picked up by a leaderboard integration. The cheat console also demonstrates event-driven output patterns.
// Dispatched on every enemy or boss kill
window.dispatchEvent(new CustomEvent('vs-kills', { detail: { total: totalKills } }));
// Also dispatched when the cheat code is entered
function applyCheat() {
totalKills = Math.max(totalKills, 30);
updateHUD();
window.dispatchEvent(new CustomEvent('vs-kills', { detail: { total: totalKills } }));
}
Note: The
vs-killsevent stream is the integration point for a LeaderboardfetchPOST — a listener can pick uptotalKillsand POST it to the backend API usingasync/await.
Documentation
Code Comments
Functions throughout the file include inline explanatory comments, particularly around non-obvious physics logic.
// Per-frame gravity loop (lesson: Gravity System):
// 1. Subtract gravity from vertical velocity (pulls asteroid down)
a.verticalVelocity -= a.gravityAcceleration;
// 2. Clamp at terminal velocity so asteroids don't fall infinitely fast
if (-a.verticalVelocity > a.terminalVelocity) {
a.verticalVelocity = -a.terminalVelocity;
}
// 3. Convert to engine velocity (flip sign): negative verticalVelocity = downward y motion
a.y += -a.verticalVelocity * worldSpeed;
// Boss respawn loop: first boss on wave 3, then every 2 waves after each kill.
let bossesDefeated = 0;
let nextBossWave = 3;
// While the Boss Alien is alive, all other moving objects tick at this fraction
// of their normal speed. Restored to 1 the instant the boss dies.
let worldSpeed = 1;
// ── Boss Alien (peer lesson: chasing-enemy update() loop) ───────────────
// Adapted from the "ocean" lesson on Math.atan2-based pursuit. Each frame,
// compute the angle from the boss to the ship and step toward the ship along
// (cos, sin). The boss eats 30 hits and counts as 5 kills on death.
Debugging
Console Debugging
Strategic logging opportunity points exist at key state transitions. The updateHUD() call at every frame renders live game state to the DOM. Placing console.log calls in checkCollisions, updateShip, or spawnWave provides per-frame debugging.
// Example: add console.log inside checkCollisions to trace hit detection
function checkCollisions() {
for (let bi = bullets.length - 1; bi >= 0; bi--) {
const b = bullets[bi];
if (boss) {
const dx = b.x - boss.x, dy = b.y - boss.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// console.log(`Bullet ${bi} → boss dist: ${dist.toFixed(1)}`);
if (dist < boss.r + 4) {
boss.hp--;
// console.log(`Boss hit! HP remaining: ${boss.hp}`);
}
}
}
}
Hit Box Visualization
Collision radii can be visualized by drawing debug circles over each object’s collision boundary directly in the draw* functions.
// Add to drawEnemies() to visualize collision radius
enemies.forEach(e => {
// ... existing draw code ...
// DEBUG: draw hit box
ctx.save();
ctx.strokeStyle = 'rgba(255,0,0,0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(e.x, e.y, e.r + 4, 0, Math.PI * 2); // +4 matches collision check
ctx.stroke();
ctx.restore();
});
The collision check uses radius e.r + 4 (enemy) and boss.r + 4 (boss), so debug circles should match those values exactly.
Source-Level Debugging
Set breakpoints in DevTools at the following high-value lines:
| Function | What to inspect |
|---|---|
checkCollisions() line 954 |
dx, dy, dist for bullet–enemy hits |
updateBoss() line 657 |
angle, boss.x/y chasing math |
spawnWave() line 716 |
wave, nextBossWave to verify boss trigger |
startGame() line 1185 |
Full reset — confirm all arrays are cleared |
Network Debugging
The vs-kills CustomEvent is the hook for leaderboard API calls. In the Network tab, look for POST requests to your score endpoint triggered when this event fires. Check:
- Request payload contains
{ total: totalKills } - Response status is
200/201 - CORS headers are present if calling a remote backend
// Listening for the event and posting to an API (to be implemented):
window.addEventListener('vs-kills', async (e) => {
try {
const res = await fetch('/api/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kills: e.detail.total }),
});
const data = await res.json();
console.log('Score saved:', data);
} catch (err) {
console.error('Leaderboard POST failed:', err);
}
});
Testing & Verification
Gameplay Testing
Key scenarios to test during a live playthrough:
| Test | What to verify |
|---|---|
| Move with WASD | Ship stays within canvas bounds (Math.max/Math.min clamping) |
| Shoot with Arrow keys | 3-way spread fires; 2-way spread activates during boss |
| Enemy reaches bottom | Enemy removed from array; no crash |
| Wave cleared | wave++ increments and spawnWave() is called |
| Boss spawns wave 3 | worldSpeed drops to 0.4, boss health bar appears |
| Boss killed | worldSpeed resets to 1, bar hides, bossesDefeated++ |
lives reaches 0 |
gameState = 'dead' and showDeadScreen() renders |
P key |
Pause overlay opens; game loop freezes on last frame |
Cheat code teambob |
totalKills jumps to 30 minimum |
Integration Testing
The vs-kills CustomEvent is the integration surface. A leaderboard listener can be wired to POST scores to a backend. Verify end-to-end by:
- Starting a game and killing an enemy — check the Network tab for the POST request
- Confirming the response payload is valid JSON
- Retrieving the leaderboard via GET and confirming the score was persisted
API Error Handling
Wrap any fetch calls in try/catch and surface errors gracefully.
window.addEventListener('vs-kills', async (e) => {
try {
const res = await fetch('/api/leaderboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kills: e.detail.total }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log('Leaderboard updated:', data);
} catch (err) {
console.error('Leaderboard error:', err.message);
// Optionally: show a non-blocking in-game toast
}
});