GameLevelForest Blog description; Sprint 4 Individual Portfolio β CS 111 objectives
π² The Whispering Forest
Sprint 4 β Ahmad Sediqi
Overview
This blog is my Sprint 4 individual portfolio entry. It demonstrates the CS 111 learning objectives through GameLevelForest, the final level of our group game. The level drops the player into a dark forest with three NPCs β The Wraith, the Dark Figure, and The Warden. The Warden guards a fork in the path and, when interacted with, launches a full sublevel (GameLevelForestSub) using a nested GameControl instance with a cinematic fade transition.
Below I explain how my code meets each CS 111 objective with annotated examples drawn directly from GameLevelForest.js.
π CS 111 Objectives Checklist
| Β | Objective |
|---|---|
| β | Writing Classes β GameLevelForest |
| β | Instantiation & Objects β all NPCs, player, and background in this.classes |
| β | Iteration β Object.assign(), array spread in this.classes |
| β | Conditionals β dialogue open check, dialogueSystem null checks |
| β | Nested Conditions β null guard then open-check inside every interact |
| β | Numbers β scale factors, animation rates, position coordinates, Math.PI |
| β | Strings β NPC IDs, image paths, dialogue lines, greeting text |
| β | Booleans β GRAVITY: false, mirror: true, isDialogueOpen() return |
| β | Arrays β dialogues[], this.classes[], addButtons([...]) |
| β | Objects (JSON) β every sprite data object, hitbox, orientation, keypress |
| β | Mathematical Operators β Math.PI / 16 for rotation, position arithmetic |
| β | String Operations β path + "/images/gamify/forest.png" throughout |
| β | Boolean Expressions β strikes >= MAX_STRIKES, child.id !== 'promptDropDown' |
β CS 111 Objectives
π¦ Object-Oriented Programming
Writing Classes β GameLevelForest is a custom class. All setup lives in the constructor so the game engine can instantiate it on demand.
class GameLevelForest {
constructor(gameEnv) {
this.gameEnv = gameEnv;
// all sprite data and class list built here
this.classes = [ ... ];
}
}
export default GameLevelForest;
Methods & Parameters β Every NPC defines two methods as properties of its sprite data object. These override the base NPC behaviour the engine provides.
// reaction() β called when the player walks near the NPC
reaction: function() {
if (this.dialogueSystem) this.showReactionDialogue();
else console.log(sprite_greet_wraith);
},
// interact() β called when the player presses E on the NPC
interact: function() {
if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
this.dialogueSystem.closeDialogue();
return;
}
if (!this.dialogueSystem) {
this.dialogueSystem = new DialogueSystem();
}
this.showRandomDialogue();
}
Instantiation & Objects β Every game object is expressed as an object literal inside this.classes. The engine reads this array and instantiates each class with the matching data.
this.classes = [
{ class: GameEnvBackground, data: image_data_forest }, // background
{ class: Player, data: sprite_data_octopus }, // player
{ class: Npc, data: sprite_data_wraith }, // NPC 1
{ class: Npc, data: sprite_data_figure }, // NPC 2
{ class: Npc, data: sprite_data_warden }, // NPC 3 (sublevel trigger)
];
π Control Structures
Conditionals & Nested Conditions β Every interact function follows the same two-step guard: first check if a dialogue is already open (and close it if so), then check if a DialogueSystem exists before calling methods on it. This prevents null reference errors and lets the same key toggle the dialogue on and off.
interact: function() {
// Outer condition β close if already open
if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) {
this.dialogueSystem.closeDialogue();
return; // early exit
}
// Nested null guard β create only if missing
if (!this.dialogueSystem) {
this.dialogueSystem = new DialogueSystem();
}
if (this.dialogueSystem) {
this.showRandomDialogue(); // safe to call now
}
}
Object.assign for fade overlay β Object.assign is used to apply all CSS styles to the fade element in one pass during the sublevel transition, rather than setting each property individually.
const fade = document.createElement('div');
Object.assign(fade.style, {
position: 'fixed', top: '0', left: '0',
width: '100%', height: '100%',
backgroundColor: '#000',
opacity: '0',
transition: 'opacity 0.8s ease-in-out',
zIndex: '9999',
pointerEvents: 'none'
});
π’ Data Types
Numbers β Scale factors, animation rates, and rotation angles all use numeric literals. Math.PI / 16 gives a small tilt on diagonal movement directions.
SCALE_FACTOR: 5,
ANIMATION_RATE: 50,
INIT_POSITION: { x: 0.05, y: 0.85 },
downLeft: { row: 0, start: 0, columns: 2, mirror: true, rotate: Math.PI / 16 },
downRight: { row: 0, start: 0, columns: 2, rotate: -Math.PI / 16 },
Strings β NPC IDs, greeting text, and every image path are strings. Path concatenation builds the src for every sprite.
id: 'The Wraith',
greeting: "...it took my family. Both paths lead somewhere.",
src: path + "/images/gamify/tux.png"
Booleans β GRAVITY: false keeps the octopus player from falling. mirror: true flips the sprite for left-facing movement. isDialogueOpen() returns a boolean used in every interact guard.
GRAVITY: false,
left: { row: 1, start: 0, columns: 2, mirror: true },
if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) { ... }
Arrays β Each NPC has a dialogues[] array. this.classes[] holds every game object. addButtons([...]) takes an array of button config objects.
dialogues: [
"...it took my family. Both paths lead somewhere. Not all somewheres are safe.",
"The trees shift when the fog comes in. I stopped trusting my eyes.",
"I wandered left. I ended up here. I cannot leave.",
"Follow the light... if you can find any."
],
Objects (JSON) β Every sprite is configured as a plain object literal. Nested objects handle pixel dimensions, orientation, hitbox size, and keypress bindings.
const sprite_data_octopus = {
id: 'Octopus',
pixels: { height: 250, width: 167 },
orientation: { rows: 3, columns: 2 },
hitbox: { widthPercentage: 0.45, heightPercentage: 0.2 },
keypress: { up: 87, left: 65, down: 83, right: 68 }
};
β Operators
Mathematical β Math.PI / 16 computes the rotation radians for diagonal sprite tilting. Position values use decimal arithmetic to express percentages of the canvas size.
downLeft: { row: 0, start: 0, columns: 2, mirror: true, rotate: Math.PI / 16 },
INIT_POSITION: { x: 0.5, y: 0.8 } // 50% across, 80% down the canvas
String Operations β Image sources are built by concatenating the engineβs base path with a relative image path, making the level portable across different deployments.
src: path + "/images/gamify/forest.png"
src: path + "/images/gamify/octopus.png"
Boolean Expressions β Compound boolean expressions guard NPC interactions and the sublevel transition.
if (this.dialogueSystem && this.dialogueSystem.isDialogueOpen()) { ... }
β¨οΈ Input / Output
Keyboard Input β WASD keys are bound to the player sprite via a keypress config object. Key codes map directly to movement directions.
keypress: { up: 87, left: 65, down: 83, right: 68 }
// W=87 A=65 S=83 D=68
Canvas Rendering β All sprites (background, player, three NPCs) are rendered through the engineβs draw pipeline. The level passes this.classes to the engine, which instantiates and draws each object each frame.
Sublevel Transition β When the player chooses to face the fork, the level pauses the primary GameControl, hides its canvas state, creates a new GameControl with GameLevelForestSub, and starts it. A CSS fade overlay covers the swap so the transition is seamless.
const primaryGame = gameEnv.gameControl;
primaryGame.pause();
primaryGame.hideCanvasState();
const levelArray = [GameLevelForestSub];
const gameInGame = new GameControl(gameEnv.game, levelArray, {
parentControl: primaryGame
});
gameInGame.start();
// Resume the forest when the sublevel ends
gameInGame.gameOver = function() {
primaryGame.resume();
};
π‘ Reflection
What I built: A final atmospheric level with three interactive NPCs that build tension through dialogue, leading to a cinematic transition into a sublevel. The Warden NPC drives the main story beat β the player must choose to enter the fork, which launches a fully nested game-within-a-game using a second GameControl instance.
Hardest problem: Getting the sublevel to launch cleanly without leaving stale canvases from the parent level. The fix was calling primaryGame.hideCanvasState() before starting gameInGame, and wiring gameInGame.gameOver to call primaryGame.resume() so control returns correctly when the sublevel ends.
What I learned: Closures are powerful for NPC design β each NPCβs interact and reaction close over the gameEnv and DialogueSystem they need without polluting the outer scope. The Wardenβs interact function closes over gameEnv.gameControl directly, which is what makes the sublevel launch possible from inside an NPC callback.