CS 111 Requirements — VoidStriker Code Guide

A complete walkthrough of every CS 111 learning objective with annotated examples pulled directly from GameLevelVoidStriker.js.


Object-Oriented Programming

Writing Classes

VoidStriker’s level is itself a class. The constructor receives the shared gameEnv (canvas size, asset path) and registers which game objects to build.

class GameLevelVoidStriker {
  constructor(gameEnv) {
    let width  = gameEnv.innerWidth;
    let height = gameEnv.innerHeight;
    let path   = gameEnv.path;

    // ...

    this.classes = [
      { class: GameEnvBackground, data: image_data_space },
    ];
  }
}

GameEnvBackground is a second class (imported at the top) — satisfying the 2+ custom classes requirement.


Methods & Parameters

Every major system is its own method with clear parameters. Example: spawnExplosion takes coordinates and a color string, then generates particle objects.

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,
    });
  }
}

Other examples with 2+ parameters: fireDirected(sx, sy), buildBackgroundScene() (uses module-level W, H).


Instantiation & Objects

Game objects are built as plain objects (JSON-style) inside factory functions and pushed into arrays — a data-driven approach the GameBuilder pattern relies on.

enemies.push({
  x:         rand(40, W - 40),
  y:         rand(-200, -30) - i * 35,
  r:         18,
  speed:     rand(1.0, 1.8 + wave * 0.25),
  hp:        1 + Math.floor(wave / 2),
  color:     `hsl(${hueBase},85%,55%)`,
  shootTimer: wave >= 5 ? randI(40, 120) : Infinity,
  chaser:    isChaser,
});

The boss object is instantiated similarly inside spawnBoss().


Inheritance (Basic)

GameLevelVoidStriker extends the engine’s level system by registering GameEnvBackground (imported base class) in this.classes. The engine then instantiates each class in the array, chaining constructors automatically.

import GameEnvBackground from '@assets/js/GameEnginev1.1/essentials/GameEnvBackground.js';

// GameEnvBackground is the parent class; this level composes it
this.classes = [
  { class: GameEnvBackground, data: image_data_space },
];

Method Overriding

The IIFE module pattern gives each system its own update + draw pair, mimicking the polymorphic lifecycle the engine expects. For example, enemies and the boss each have dedicated update/draw functions that replace default behavior:

// Standard enemies scroll downward
function updateEnemies() {
  enemies.forEach(e => {
    e.y  += e.speed * worldSpeed;
    e.x  += e.vx    * worldSpeed;
    // ...
  });
}

// Boss overrides movement — it chases the player using atan2
function updateBoss() {
  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;
}

Constructor Chaining

The level constructor delegates background setup to the engine via this.classes, which causes the engine to call new GameEnvBackground(data, gameEnv) — chaining through super() internally.

this.classes = [
  { class: GameEnvBackground, data: image_data_space },
];
// Engine calls: new GameEnvBackground(image_data_space, gameEnv)
// GameEnvBackground calls: super(data, gameEnv) internally

Control Structures

Iteration

Loops appear throughout. The particle, enemy, bullet, and asteroid arrays are all processed with forEach and filtered with filter every frame:

// Update every particle each frame
particles.forEach(p => {
  p.x     += p.vx;
  p.y     += p.vy;
  p.alpha -= 0.03;
  p.r     *= 0.97;
});

// Remove dead particles (filter = loop + condition)
particles = particles.filter(p => p.alpha > 0.02);

Wave spawning uses a for loop to create multiple enemies at once:

for (let i = 0; i < count; i++) {
  enemies.push({ /* ... */ y: rand(-200, -30) - i * 35 });
}

Conditionals

State transitions, hit detection, and feature flags all use if/else:

// Game state transition
if (lives <= 0) {
  gameState = 'dead';
  showDeadScreen();
}

// Chaser enemies use direction-vector movement; normal enemies scroll
if (e.chaser) {
  const dx = ship.x - e.x;
  const dy = ship.y - e.y;
  // ...
} else {
  e.y += e.speed * worldSpeed;
}

Nested Conditions

Collision detection checks multiple independent layers — bullet alive, boss present, then enemy list, then asteroids:

for (let bi = bullets.length - 1; bi >= 0; bi--) {
  const b = bullets[bi];
  if (b.life <= 0) continue;             // outer: bullet must be alive

  if (boss) {
    const dx = b.x - boss.x, dy = b.y - boss.y;
    if (Math.sqrt(dx*dx + dy*dy) < boss.r + 4) {  // inner: hit radius
      boss.hp--;
      b.life = 0;
      if (boss.hp <= 0) killBoss();      // innermost: kill condition
    }
  }
  // ... then check enemy array, then asteroid array
}

Data Types

Numbers

Position, velocity, health, wave index, score — nearly every game property is a number:

ship = {
  x: W / 2, y: H * 0.78,  // position
  w: 28, h: 38,            // dimensions
  speed: 4.5,              // velocity scalar
  shootCooldown: 0,        // counter
  invincible: 0,           // timer
};

Strings

Ship names, CSS color strings, game state flags, and DOM text are all strings:

const SHIP_CHARS = [
  { name: 'Striker',  body: '#a0d8ff', cockpit: '#00eeff', thrustRgb: '0,200,255' },
  { name: 'Shadow',   body: '#bb99ee', cockpit: '#cc55ff', thrustRgb: '160,0,255' },
];

let gameState = 'title'; // 'title' | 'playing' | 'dead'

// Template literal to build dynamic color strings
e.color = `hsl(${hueBase},85%,55%)`;

Booleans

Flags control flow throughout the game loop:

let consoleActive = false;  // paused?

const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);

// Boolean used in guard clause
if (ship.invincible <= 0) { /* take damage */ }

Arrays

All dynamic game objects live in arrays that shrink and grow every frame:

let bullets = [], enemies = [], asteroids = [], particles = [];
let enemyBullets = [];

// Add
bullets.push({ x: ship.x, y: ship.y, vx, vy, life: 60 });

// Remove dead entries (non-destructive filter)
bullets = bullets.filter(b => b.life-- > 0);

Objects (JSON)

Configuration data is stored as plain object literals — easy to read and swap:

const image_data_space = {
  id:     'VoidStriker-Background',
  src:    '',
  pixels: { height: 570, width: 1025 }
};

const BOSS_PALETTES = [
  { glow: '255,40,80', bodyHi: '#ff7799', bodyLo: '#660022', tentacle: '#aa1144', eye: '#ffee44' },
  // ...
];

Operators

Mathematical

Physics, geometry, and scoring all rely on arithmetic operators:

// Asteroid gravity system (acceleration → velocity → position)
a.verticalVelocity -= a.gravityAcceleration;          // subtract
a.y += -a.verticalVelocity * worldSpeed;              // multiply + add

// Distance formula for collision detection
const dist = Math.sqrt(dx * dx + dy * dy);            // multiply + add + sqrt

// Score tracking
totalKills += 5;  // boss is worth 5 kills

String Operations

Template literals build color and UI strings dynamically:

// Dynamic HSL color per enemy wave
`hsl(${(hueBase + randI(-15,15) + 360) % 360},85%,55%)`

// Dynamic CSS gradient from palette data
`linear-gradient(90deg, rgba(${p.glow},1), ${p.bodyHi})`

// HUD text update
s.textContent = `KILLS: ${totalKills}`;
w.textContent = `WAVE: ${wave}`;

Boolean Expressions

Compound conditions guard damage, spawning, and shooting:

// Boss spawns only when conditions both true
if (wave === nextBossWave && !boss) spawnBoss();

// Chaser spawns only in wave 2+ AND random roll passes
const isChaser = wave >= 2 && Math.random() < Math.min(0.45, 0.08 * wave);

// Player can only shoot when cooldown expired AND a direction key is held
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); }
}

Input / Output

Keyboard Input

attachInput() registers keydown and keyup listeners on window. The keys object acts as a real-time snapshot of held keys:

const keys = {};

function attachInput() {
  window.addEventListener('keydown', e => {
    if (e.key === 'p' || e.key === 'P') {
      // Toggle pause/console
    }
    if (!consoleActive) keys[e.key] = true;

    // Prevent page scroll while playing
    if (e.key === 'ArrowUp' || e.key === 'ArrowDown') e.preventDefault();
  });
  window.addEventListener('keyup', e => { keys[e.key] = false; });
}

// Consumed each frame in updateShip()
if (keys['a'] || keys['A']) ship.x -= ship.speed;

Canvas Rendering

All visuals are drawn to a <canvas> element every frame via the 2D context. Gradients, arcs, and transforms compose the ship, enemies, and particles:

// Ship body (triangle)
ctx.fillStyle = ch.body;
ctx.beginPath();
ctx.moveTo(0, -h / 2);
ctx.lineTo(w / 2,  h / 2);
ctx.lineTo(-w / 2, h / 2);
ctx.closePath();
ctx.fill();

// Bullet glow (radial gradient)
const g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, 5);
g.addColorStop(0,   '#fff');
g.addColorStop(0.4, bColor);
g.addColorStop(1,   'transparent');
ctx.fillStyle = g;
ctx.arc(b.x, b.y, 5, 0, Math.PI * 2);
ctx.fill();

GameEnv Configuration

The constructor reads gameEnv properties to size and position everything relative to the host engine’s canvas:

constructor(gameEnv) {
  let width  = gameEnv.innerWidth;
  let height = gameEnv.innerHeight;
  let path   = gameEnv.path;

  const image_data_space = {
    id:     'VoidStriker-Background',
    pixels: { height: 570, width: 1025 }
  };
  // ...
}

Inside init(), the canvas is sized to match the container:

const rect = container.getBoundingClientRect();
W = rect.width  || gameEnv.innerWidth  || 800;
H = rect.height || gameEnv.innerHeight || 500;
canvas.width  = W;
canvas.height = H;

API Integration

The game fires a custom DOM event every time the kill count changes. An external leaderboard listener can catch this and POST the score:

// Dispatched on every kill (enemies, asteroids, boss)
window.dispatchEvent(new CustomEvent('vs-kills', {
  detail: { total: totalKills }
}));

// Example leaderboard integration (async/await pattern)
window.addEventListener('vs-kills', async (e) => {
  try {
    const res = await fetch('/api/leaderboard', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ score: e.detail.total })
    });
    const data = await res.json();
    console.log('Score saved:', data);
  } catch (err) {
    console.error('Leaderboard error:', err);
  }
});

Asynchronous I/O

The setTimeout in the constructor defers init() until the engine has finished mounting its canvas — a simple async pattern ensuring DOM readiness:

constructor(gameEnv) {
  // Defer init so the engine canvas exists before we query the DOM
  setTimeout(() => VoidStrikerGame.init(gameEnv), 200);
}

For leaderboard or NPC AI calls, async/await handles the promise chain (see API Integration above).


JSON Parsing

API responses are parsed with JSON.parse() (or res.json() which does the same). The cheat console also parses user input:

// Typical fetch + parse pattern
const res  = await fetch('/api/leaderboard');
const data = await res.json();           // parses JSON body
const topScores = data.scores;           // destructure the array

Documentation

Code Comments

Every major system in the file has a purpose comment. Inline comments explain the why of non-obvious math:

// 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;

Target: comment density > 10% — count comment lines vs. total lines in your file.


Debugging

Console Debugging

Add console.log at key state transitions and collision checks:

function killBoss() {
  console.log(`Boss tier ${boss.tier} killed. Total kills: ${totalKills}`);
  // ...
}

function checkCollisions() {
  // Temporary: log every frame to track collision state
  if (boss) console.log(`Boss HP: ${boss.hp}/${boss.maxHp}`);
}

Hit Box Visualization

Toggle a debug draw mode to see collision radii:

// Add to drawEnemies() during development
if (DEBUG_HITBOX) {
  ctx.strokeStyle = 'lime';
  ctx.lineWidth   = 1;
  ctx.beginPath();
  ctx.arc(e.x, e.y, e.r, 0, Math.PI * 2);
  ctx.stroke();
}

Source-Level Debugging

Set a breakpoint inside checkCollisions() in Chrome DevTools → Sources tab. Step through the nested loops to watch dx, dy, and the distance calculation in real time.


Network Debugging

After posting a score, open DevTools → Network tab. Find the POST /api/leaderboard request and inspect:

  • Status — 200 OK vs. 4xx/5xx
  • Payload — confirm { score: 42 } was sent correctly
  • Response — check what the server returned
  • CORS headersAccess-Control-Allow-Origin must be present for cross-origin calls

Application Debugging

If the game stores session data (login token, saved settings), DevTools → Application → Local Storage / Cookies shows the raw key-value pairs. Use this to confirm a user token is present before making authenticated leaderboard requests.


Element Inspection

Right-click the game canvas → Inspect. In the Elements panel you can:

  • Confirm voidstriker-canvas has position: absolute; z-index: 9999
  • Check that the HUD elements (#vs-ui, #vs-lives) overlay the canvas correctly
  • Live-edit CSS to test layout fixes without reloading

Testing & Verification

Gameplay Testing

Play through at least three waves to verify:

  • Wave counter increments only after all enemies and asteroids are cleared
  • Boss spawns on wave 3; health bar appears and fills correctly
  • Death screen shows correct kill count and best score
  • Retry resets wave, lives, and kill counter to starting values

Integration Testing

After adding the leaderboard listener, test end-to-end:

  1. Kill enough enemies to trigger a vs-kills event
  2. Open Network tab — confirm the POST fires with the right payload
  3. Refresh the leaderboard GET endpoint — confirm the score appears

API Error Handling

Wrap every fetch in try/catch and handle both network failures and bad status codes:

try {
  const res = await fetch('/api/leaderboard', { method: 'POST', body: JSON.stringify(payload) });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  console.log('Saved:', data);
} catch (err) {
  // Graceful degradation — game continues even if the leaderboard is down
  console.error('Leaderboard unavailable:', err.message);
}

All code examples above are drawn directly from GameLevelVoidStriker.js. Line numbers will vary as the file evolves — search by function name to locate each snippet.