CSSE 2 Final Week 2
Personal blog for CSSE 2 Final Week 22
Day 1
Decided to add some options (one) to damage the boss. Here, I give to ability to collect “shards” (mind the shamelessly copied Coin.js
code):
// Shard.js
import BossFight from './BossFight.js';
import GameControl from './GameControl.js';
import GameEnv from './GameEnv.js';
import GameObject from './GameObject.js';
export class Shard extends GameObject {
constructor(canvas, image, data) {
super(canvas, image, data, 0.5, 0.5);
this.coinX = Math.random() * GameEnv.innerWidth;
this.coinY = 0.5 + Math.random() * 0.5; // Limit to bottom half of screen
this.size();
this.id = this.initiateId();
}
initiateId() {
const currentCoins = GameEnv.gameObjects
return currentCoins.length //assign id to the coin's position in the gameObject Array (is unique to the coin)
}
// Required, but no update action
update() {
this.collisionChecks()
}
// Draw position is always 0,0
draw() {
// Save the current transformation matrix
this.ctx.save();
// Rotate the canvas 90 degrees to the left
this.ctx.rotate(-Math.PI / 2);
// Draw the image at the rotated position (swap x and y)
this.ctx.drawImage(this.image, -this.image.height, 0);
// Restore the original transformation matrix
this.ctx.restore();
}
// Center and set Coin position with adjustable height and width
size() {
if (this.id) {
if (GameEnv.claimedCoinIds.includes(this.id)) {
this.hide()
}
}
const scaledWidth = this.image.width * 0.2;
const scaledHeight = this.image.height * 0.169;
const coinX = this.coinX;
const coinY = (GameEnv.bottom - scaledHeight) * this.coinY;
// Set variables used in Display and Collision algorithms
this.bottom = coinY;
this.collisionHeight = scaledHeight;
this.collisionWidth = scaledWidth;
this.canvas.style.width = `${scaledWidth}px`;
this.canvas.style.height = `${scaledHeight}px`;
this.canvas.style.position = 'absolute';
this.canvas.style.left = `${coinX}px`;
this.canvas.style.top = `${coinY}px`;
}
collisionAction() {
// check player collision
if (this.collisionData.touchPoints.other.id === "player") {
if (this.id) {
GameEnv.claimedCoinIds.push(this.id);
}
this.destroy();
GameControl.gainCoin(5);
GameEnv.playSound("coin");
console.log("Shard colleted");
const boss = GameEnv.gameObjects.find(obj => obj instanceof BossFight);
if (boss) {
boss.currentHp -= 50;
console.log("Boss damaged by shard");
}
}
}
// Method to hide the coin
hide() {
this.canvas.style.display = 'none';
}
// Method to show the coin
show() {
this.canvas.style.display = 'block';
}
reset() {
this.coinX =Math.random() * GameEnv.innerWidth;
this.coinY = 0.5 + Math.random() * 0.5;
this.size();
this.show();
}
}
export default Shard;
And you’ll probably notice that this is shamelessly recycled Coin code with added randomness and boss damage. Why, yes it is! Good eye!
Anyhow, here two other changes I made:
Loading the coins into the level
// GameSetterBossFight.js
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
and also
// BossFight.js
// Method to handle player death (recycled from SkibidiTitan.js)
kill(target) {
target.canvas.style.transition = "transform 0.5s";
target.canvas.style.transform = "rotate(-90deg) translate(-26px, 0%)";
GameEnv.playSound("PlayerDeath");
if (target.state.isDying === false) {
target.state.isDying = true;
setTimeout(async () => {
await GameControl.transitionToLevel(GameEnv.levels[GameEnv.levels.indexOf(GameEnv.currentLevel)]);
console.log("level restart");
GameEnv.gameObjects.forEach(obj => {
if (obj instanceof Shard) {
obj.reset();
}
});
target.state.isDying = false;
}, 900);
}
}
What I had to do was connect Shard.js
and BossFight.js
(easy enough, just import one into the other), then invoke one of the methods (for hp loss in this case). And done! You can damage the boss.
Next step is to fix the hp bar still appearing even after the boss is killed, but this was someone else’s code I can’t really be bothered to look at it right now – preoccupied with Rust :3
Day 2
Wrote a quick notebook on how to add music to platformer here. Basically it. As of now, I’m looking for projects to write in Rust. Burned out working on platformer, but I’ve taken interest in RedoxOS.
Day 3
Some QoL and shards
First I updated BossFight.js
to remove the health bar on death by adding this under handleDeath()
:
this.titanHealthBar.destroy();
Also added this under update to reset the shards upon player death (so the player can actually complete the level):
GameEnv.gameObjects.forEach(obj => {
if (obj instanceof Shard) {
obj.reset();
}
});
And of course have to add these imports to GameSetterBossFight.js
:
import Shard from './Shard.js';
import { PlayerBossFight } from './PlayerBossFight.js';
with a few additions to shards:
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'shard', id: 'coin', class: Shard, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
{ name: 'laser', id: 'Laser', class: Laser, data: assets.obstacles.laser, xPercentage: 0.75, yPercentage: 0.5 },
{ name: 'skibidiTitan', id: 'skibidiTitan', class: BossFight, data: assets.enemies.skibidiTitan, xPercentage: 0.35, yPercentage: 0.5, minPosition: 0.5 },
{ name: 'escaper', id: 'player', class: PlayerBossFight, data: assets.players.escaper },
Also shamelessly copied PlayerSkibidi.js
, refactored, added and removed a few features, and voila:
import GameEnv from './GameEnv.js';
import GameControl from './GameControl.js';
import PlayerBaseOneD from './PlayerBaseOneD.js'; ///With this you can change the direction of the sprite sheet with just the sprite rows.
/**
* @class PlayerBossFight class
* @description PlayerBossFight.js key objective is to eent the user-controlled character in the game.
*
* The Player class extends the Character class, which in turn extends the GameObject class.
* Animations and events are activated by key presses, collisions, and gravity.
* WASD keys are used by user to control The Player object.
*
* @extends PlayerBase
*/
export class PlayerBossFight extends PlayerBaseOneD { /// Using PlayerBaseOneD added the sprite mirror but deleted the sprite not showing the animations
/** GameObject instantiation: constructor for PlayerSkibidi object
* @extends Character
* @param {HTMLCanvasElement} canvas - The canvas element to draw the player on.
* @param {HTMLImageElement} image - The image to draw the player with.
* @param {Object} data - The data object containing the player's properties.
*/
constructor(canvas, image, data) {
super(canvas, image, data);
this.invincible = true;
this.timer = false;
GameEnv.invincible = false; // Player is NOT invincible
this.animationSpeed = data?.animationSpeed;
this.counter = this.animationSpeed;
// Goomba variables, deprecate?
this.timer = false;
GameEnv.invincible = false; // Player is not invincible
}
/**
* @override
* gameLoop helper: Update Player jump height, replaces PlayerBase updateJump using settings from GameEnv
*/
updateJump() {
let jumpHeightFactor;
console.log("Current Difficulty:", GameEnv.difficulty); // Debugging output
if (GameEnv.powerUpCollected) {
jumpHeightFactor = 1.20; // Ensure this is used when power-up is collected
} else if (GameEnv.difficulty === "easy") {
jumpHeightFactor = 0.50;
} else if (GameEnv.difficulty === "super_easy") {
jumpHeightFactor = 0.90;
} else if (GameEnv.difficulty === "normal") {
jumpHeightFactor = 0.40;
} else {
jumpHeightFactor = 0.30;
}
console.log("Jump Height Factor Set To:", jumpHeightFactor); // Debugging output
console.log("Old Y Position:", this.y);
this.setY(this.y - (this.bottom * jumpHeightFactor));
console.log("New Y Position:", this.y);
}
updateFrameX(){
if (this.frameX < this.maxFrame) {
if(this.counter > 0){
this.frameX = this.frameX;
this.counter--;
}
else{
this.frameX++;
this.counter = this.animationSpeed;
}
} else {
this.frameX = this.minFrame;
}
}
/**
* @override
* gameLoop: Watch for Player collision events
*/
handleCollisionStart() {
super.handleCollisionStart(); // calls the super class method
// adds additional collision events
this.handleCollisionEvent("finishline");
this.handleCollisionEvent("SkibidiToilet");
this.handleCollisionEvent("laser");
this.handleCollisionEvent("powerup"); // created a new case where it detects for collision between player and power-up
}
handleKeyUp(event) {
const key = event.key;
if (key in this.pressedKeys) {
delete this.pressedKeys[key];
if (Object.keys(this.pressedKeys).length > 0) {
// If there are still keys in pressedKeys, update the state to the last one
const lastKey = Object.keys(this.pressedKeys)[Object.keys(this.pressedKeys).length - 1];
this.updateAnimationState(lastKey);
//GameEnv.updateParallaxDirection(lastKey)
} else {
// If there are no more keys in pressedKeys, update the state to null
GameEnv.playerAttack = false;
this.updateAnimationState(null);
// GameEnv.updateParallaxDirection(null)
}
}
}
/**
* @override
*/
updateAnimationState(key) {
switch (key) {
case 'a':
case 'd':
this.state.animation = 'walk';
GameEnv.playerAttack = false;
break;
case 'w':
if (this.state.movement.up == false) {
this.state.movement.up = true;
this.state.animation = 'jump';
}
GameEnv.playerAttack = false;
break;
case 's':
if ("a" in this.pressedKeys || "d" in this.pressedKeys) {
this.state.animation = 'run';
}
GameEnv.playerAttack = false;
break;
case 'b':
this.state.animation = 'attack'; // Always trigger attack when b is pressed
GameEnv.playerAttack = true;
break;
default:
this.state.animation = 'idle';
GameEnv.playerAttack = false;
break;
}
}
/**
* @override
* gameloop: Handles additional Player reaction / state updates to the collision for game level
*/
handlePlayerReaction() {
super.handlePlayerReaction(); // calls the super class method
// handles additional player reactions
switch (this.state.collision) {
case "finishline":
// 1. Caught in finishline
if (this.collisionData.touchPoints.this.onTopofOther || this.state.isFinishing ) {
if (GameEnv.titan && GameEnv.titan.state.isDead) {
// Position player in the center of the finishline
this.x = this.collisionData.newX;
this.state.movement = { up: false, down: false, left: false, right: false, falling: false};
this.state.isFinishing = true;
this.gravityEnabled = true;
// Using natural gravity wait for player to reach floor
if (Math.abs(this.y - this.bottom) <= GameEnv.gravity) {
// Force end of level condition
this.x = GameEnv.innerWidth + 1;
}
} else {
alert("Titan is not dead. You may not proceed");
break;
}
// 2. Collision between player right and finishline
} else if (this.collisionData.touchPoints.this.right) {
this.state.movement.right = false;
this.state.movement.left = true;
// 3. Collision between player left and finishline
} else if (this.collisionData.touchPoints.this.left) {
this.state.movement.left = false;
this.state.movement.right = true;
}
break;
case "SkibidiToilet": // Note: Goomba.js and Player.js could be refactored
// 1. Player jumps on goomba, interaction with Goomba.js
if (this.collisionData.touchPoints.this.top && this.collisionData.touchPoints.other.bottom && this.state.isDying == false) {
// GoombaBounce deals with player.js and goomba.js
if (GameEnv.goombaBounce === true) {
GameEnv.goombaBounce = false;
this.y = this.y - 100;
}
if (GameEnv.goombaBounce1 === true) {
GameEnv.goombaBounce1 = false;
this.y = this.y - 250
}
// 2. Player touches goomba sides of goomba
} else if (this.collisionData.touchPoints.this.right || this.collisionData.touchPoints.this.left) {
if (GameEnv.difficulty === "normal" || GameEnv.difficulty === "hard") {
if (this.state.isDying == false) {
this.state.isDying = true;
this.canvas.style.transition = "transform 0.5s";
this.canvas.style.transform = "rotate(-90deg) translate(-26px, 0%)";
GameEnv.playSound("PlayerDeath");
setTimeout(async() => {
await GameControl.transitionToLevel(GameEnv.levels[GameEnv.levels.indexOf(GameEnv.currentLevel)]);
}, 900);
}
} else if (GameEnv.difficulty === "easy" && this.collisionData.touchPoints.this.right) {
this.x -= 10;
} else if (GameEnv.difficulty === "easy" && this.collisionData.touchPoints.this.left) {
this.x += 10;
}
}
break;
case "laser": //
if (this.collisionData.touchPoints.this.right || this.collisionData.touchPoints.this.left) {
if (GameEnv.difficulty === "normal" || GameEnv.difficulty === "hard") {
if (this.state.isDying == false) {
this.state.isDying = true;
this.canvas.style.transition = "transform 0.5s";
this.canvas.style.transform = "rotate(-90deg) translate(-26px, 0%)";
GameEnv.playSound("PlayerDeath");
setTimeout(async() => {
await GameControl.transitionToLevel(GameEnv.levels[GameEnv.levels.indexOf(GameEnv.currentLevel)]);
}, 900);
}
} else if (GameEnv.difficulty === "easy" && this.collisionData.touchPoints.this.right) {
this.x -= 10;
} else if (GameEnv.difficulty === "easy" && this.collisionData.touchPoints.this.left) {
this.x += 10;
}
}
break;
case "powerup":
if (GameEnv.powerUpCollected) {
console.log("Power-up collision detected! Changing difficulty...");
GameEnv.difficulty = "super_easy"; // Force change
jumpHeightFactor = 1.20;
}
if (this.collisionData.touchPoints.this.right && GameEnv.powerUpCollected) {
this.state.movement.right = false;
this.state.movement.left = true;
jumpHeightFactor = 1.20; // Updating the jump factor to make player jump higher
} else if (this.collisionData.touchPoints.this.left && GameEnv.powerUpCollected) {
this.state.movement.left = false;
this.state.movement.right = true;
jumpHeightFactor = 0.80; // Updating the jump factor to make player jump higher
}
GameEnv.update();
console.log("Power Up",GameEnv.gameObjects[GameEnv.gameObjects.length - 1]);
break;
}
}
}
export default PlayerBossFight;
Final addition here is the most important part, shards, stored in Shard.js
:
import BossFight from './BossFight.js';
import GameControl from './GameControl.js';
import GameEnv from './GameEnv.js';
import GameObject from './GameObject.js';
export class Shard extends GameObject {
constructor(canvas, image, data) {
super(canvas, image, data, 0.5, 0.5);
this.coinX = Math.random() * GameEnv.innerWidth;
this.coinY = 0.5 + Math.random() * 0.5; // Limit to bottom half of screen
this.size();
this.id = this.initiateId();
}
initiateId() {
const currentCoins = GameEnv.gameObjects
return currentCoins.length //assign id to the coin's position in the gameObject Array (is unique to the coin)
}
// Required, but no update action
update() {
this.collisionChecks()
}
// Draw position is always 0,0
draw() {
// Save the current transformation matrix
this.ctx.save();
// Rotate the canvas 90 degrees to the left
this.ctx.rotate(-Math.PI / 2);
// Draw the image at the rotated position (swap x and y)
this.ctx.drawImage(this.image, -this.image.height, 0);
// Restore the original transformation matrix
this.ctx.restore();
}
// Center and set Coin position with adjustable height and width
size() {
if (this.id) {
if (GameEnv.claimedCoinIds.includes(this.id)) {
this.hide()
}
}
const scaledWidth = this.image.width * 0.2;
const scaledHeight = this.image.height * 0.169;
const coinX = this.coinX;
const coinY = (GameEnv.bottom - scaledHeight) * this.coinY;
// Set variables used in Display and Collision algorithms
this.bottom = coinY;
this.collisionHeight = scaledHeight;
this.collisionWidth = scaledWidth;
this.canvas.style.width = `${scaledWidth}px`;
this.canvas.style.height = `${scaledHeight}px`;
this.canvas.style.position = 'absolute';
this.canvas.style.left = `${coinX}px`;
this.canvas.style.top = `${coinY}px`;
}
collisionAction() {
// check player collision
if (this.collisionData.touchPoints.other.id === "player") {
if (this.id) {
GameEnv.claimedCoinIds.push(this.id);
}
this.destroy();
GameControl.gainCoin(5);
GameEnv.playSound("coin");
console.log("Shard colleted");
const boss = GameEnv.gameObjects.find(obj => obj instanceof BossFight);
if (boss) {
boss.currentHp -= 50;
console.log("Boss damaged by shard");
}
}
}
// Method to hide the coin
hide() {
this.canvas.style.display = 'none';
}
// Method to show the coin
show() {
this.canvas.style.display = 'block';
}
reset() {
this.coinX =Math.random() * GameEnv.innerWidth;
this.coinY = 0.5 + Math.random() * 0.5;
this.size();
this.show();
}
}
export default Shard;
Annnnnd done. Submitted PR #123
Day 4
Guess what else I realized? Apparently my testing didn’t catch the fact that you couldn’t exit the level killing the boss (because I only tested for without killing the boss). Alas, I tried adding some state checks (using BossFight.isDead
), but of course this doesn’t work: was getting a return of undefined
. This should’ve been obvious, but you can’t just check that on the class itself (unless it’s static or something). Fix was simple, just find the object then return the property of that object created using the test template:
// GameSetterBossFight.js
{ name: 'titan', id: 'skibidiTitan', class: BossFight, data: assets.enemies.skibidiTitan, xPercentage: 0.35, yPercentage: 0.5, minPosition: 0.5 },
// PlayerBossFight.js
if (this.collisionData.touchPoints.this.onTopofOther || this.state.isFinishing ) {
const titan = GameEnv.gameObjects.find(obj => obj.name === 'titan');
if (titan.currentHp <= 0) {
// Position player in the center of the finishline
this.x = this.collisionData.newX;
this.state.movement = { up: false, down: false, left: false, right: false, falling: false};
this.state.isFinishing = true;
this.gravityEnabled = true;
// Using natural gravity wait for player to reach floor
if (Math.abs(this.y - this.bottom) <= GameEnv.gravity) {
// Force end of level condition
this.x = GameEnv.innerWidth + 1;
}
} else {
alert("Titan is not dead. You may not proceed");
this.setX(0);
this.state.animation = 'idle';
break;
}
Day 5
The transition from Skibidi to Boss was broken, so I just removed Boss. (Boss wasn’t loading correctly due to absence of some dependencies that I couldn’t be bothered to fix).
Changes here were pretty simple.
Remove this line from GameSetup.js
:
GameLevelSetup(GameSetterBoss, this.path, this.playerOffScreenCallBack);
Then update imports in the files:
GameSetterBoss.js
GameSetterHills.js
PlayerBoss.js
And move all Boss files to /archive