CyberLord09 / CSSE1_Final

MIT License
0 stars 0 forks source link

N@TM #11

Open LiliWuu opened 1 month ago

LiliWuu commented 1 month ago

General Revisions to the Game

Enemy Movement

https://github.com/CyberLord09/CSSE1_Final/assets/142454293/50bb76a1-0a48-4e36-8f46-b1dbb3803a48

We wanted certain enemies to move back and forth. We first added new properties that handle the direction and animation of the enemy to the Enemy class.

export class Enemy extends Character {

    initEnvironmentState = {
        // Enemy
        animation: 'right',
        direction: 'right',
        isDying: false,
    };
// code hidden
}

For the collisions with the boundaries of the game environment, we created a method that checks for the boundaries of the enemy to the minPosition, which is set based on the GameEnv.innerWidth so that the direction of the enemy will change from left to right or right to left. The movement of the enemy is also set by changing the speed:

checkBoundaries(){
        // Check for boundaries
        if (this.x <= this.minPosition || (this.x + this.canvasWidth >= this.maxPosition)) {
            if (this.state.direction === "left") {
                this.state.animation = "right";
                this.state.direction = "right";
            }
            else if (this.state.direction === "right") {
                this.state.animation = "left";
                this.state.direction = "left";
            }
        };
    }

// code hidden
updateMovement(){
        if (this.state.animation === "right") {
            this.speed = Math.abs(this.speed)
        }
        else if (this.state.animation === "left") {
            this.speed = -Math.abs(this.speed);
        }
        else if (this.state.animation === "idle") {
            this.speed = 0
        }
        else if (this.state.animation === "death") {
            this.speed = 0
        }

        // Move the enemy\
        this.x += this.speed;

        this.playerBottomCollision = false;
    }
// code hidden

We also change the direction of the enemy when it collides with certain game objects:

 // Player action on collisions
    collisionAction() {
        if (this.collisionData.touchPoints.other.id === "finishline") {
            if (this.state.direction === "left" && this.collisionData.touchPoints.other.right) {
                this.state.animation = "right";
                this.state.direction = "right";
            }
            else if (this.state.direction === "right" && this.collisionData.touchPoints.other.left) {
                this.state.animation = "left";
                this.state.direction = "left";
            }

        }

        if (this.collisionData.touchPoints.other.id === "player") {
            // Collision: Top of Goomba with Bottom of Player
            //console.log(this.collisionData.touchPoints.other.bottom + 'bottom')
            //console.log(this.collisionData.touchPoints.other.top + "top")
            //console.log(this.collisionData.touchPoints.other.right + "right")
            //console.log(this.collisionData.touchPoints.other.left + "left")
            if (this.collisionData.touchPoints.other.bottom && this.immune == 0) {
                GameEnv.invincible = true;
                GameEnv.goombaBounce = true;
                this.canvas.style.transition = "transform 1.5s, opacity 1s";
                this.canvas.style.transition = "transform 2s, opacity 1s";
                this.canvas.style.transformOrigin = "bottom"; // Set the transform origin to the bottom
                this.canvas.style.transform = "scaleY(0)"; // Make the Goomba flat
                this.speed = 0;
                GameEnv.playSound("goombaDeath");

                setTimeout((function() {
                    GameEnv.invincible = false;
                    this.destroy();
                }).bind(this), 1500);

                // Set a timeout to make GameEnv.invincible false after 2000 milliseconds (2 seconds)
                setTimeout(function () {
                this.destroy();
                GameEnv.invincible = false;
                }, 2000);
            }
        }

        if (this.collisionData.touchPoints.other.id === "jumpPlatform") {
            if (this.state.direction === "left" && this.collisionData.touchPoints.other.right) {
                this.state.animation = "right";
                this.state.direction = "right";
            }
            else if (this.state.direction === "right" && this.collisionData.touchPoints.other.left) {
                this.state.animation = "left";
                this.state.direction = "left";
            }
        }
    }

Animations

We wanted the animations to be smoother for some animating game objects. We did this in the Boss.js file by adding two new attributes:

 constructor(canvas, image, data, xPercentage, yPercentage, name, minPosition) {
        super(canvas, image, data, xPercentage, yPercentage, name, minPosition);
        this.animationSpeed = data?.animationSpeed || 1; //higher "animationSpeed" means slower animation
        this.counter = data?.animationSpeed;
       // code hidden
    }

We created a new method that used the counter to control frame change based on animation speed. Then, we added animationSpeed as a property to the boss character in GameSetterBoss.js:

updateFrameX() {
        // Update animation frameX of the object
        if(!this.state.isDying || this.state.animation != "death"){
            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;
            }
        }

// code hidden
}
boss: {
      src: "/images/platformer/sprites/boss.png",
      width: 64,
      height: 64,
      scaleSize: 320,
      speedRatio: 0.6,
      animationSpeed: 6,
      idleL: { row: 9, frames: 0, idleFrame: { column: 1, frames: 0 } },
      idleR: { row: 11, frames: 0, idleFrame: { column: 1, frames: 0 } },
      left: { row: 9, frames: 8, idleFrame: { column: 7, frames: 0 } },
      right: { row: 11, frames: 8, idleFrame: { column: 7, frames: 0 } },
      attackL: { row: 13, frames: 5 },
      attackR: { row: 15, frames: 5 },
      death: { row: 20, frames: 5 },
      hitbox: { widthPercentage: 0.3, heightPercentage: 0.8 }
    },

Transitions Between Levels

We refactored transitions to use level tag rather level number. This is so that if any levels get reordered around, they would look nicer with their specifically-matched transition. The way this code works is by creates a constant names index, which finds the index of the level with the tag named "Greece." It then transitions to index, which in this case, is "Greece." We implemented this for all levels:

const index = GameEnv.levels.findIndex(level => level.tag === "Greece")  
GameControl.transitionToLevel(GameEnv.levels[index]);
LiliWuu commented 1 month ago

Team Level

Creating Level

Image We created a new team level called Greece and added new game objects in GameSetterGreece.js.

// Hills Game Level defintion...
  const objects = [
    // GameObject(s), the order is important to z-index...
    { name: 'greece', id: 'background', class: Background, data: assets.backgrounds.greece },
    { name: 'grass', id: 'platform', class: Platform, data: assets.platforms.grass },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.2, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.2368, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.2736, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.3104, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.3472, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.384, yPercentage: 0.76 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.4208, yPercentage: 0.70 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.5090, yPercentage: 0.64 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.5642, yPercentage: 0.34 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.5274, yPercentage: 0.34 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.4906, yPercentage: 0.34 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 1 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.94 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.88 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.76 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.70 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.64 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.58 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.52 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.46 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.40 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.34 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.28 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.22 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6368, yPercentage: 0.64 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.16 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.1 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.6, yPercentage: 0.06 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 1 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.94 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.88 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.82 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.76 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.70 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.64 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.58 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.52 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.46 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.40 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.34 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.28 },
    { name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.22 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.16 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.1 },
    //{ name: 'sandstone', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.sandstone, xPercentage: 0.75, yPercentage: 0.06 },
    { name: 'cerberus', id: 'cerberus', class: Cerberus, data: assets.enemies.cerberus, xPercentage: 0.2, minPosition: 0.09, difficulties: ["normal", "hard", "impossible"] },
    { name: 'cerberus', id: 'cerberus', class: Cerberus, data: assets.enemies.cerberus, xPercentage: 0.5, minPosition: 0.3, difficulties: ["normal", "hard", "impossible"] },
    { name: 'cerberus', id: 'cerberus', class: Cerberus, data: assets.enemies.cerberus, xPercentage: 0.7, minPosition: 0.1, difficulties: ["normal", "hard", "impossible"] },//this special name is used for random event 2 to make sure that only one of the Goombas ends the random event
    { name: 'dragon', id: 'dragon', class: Dragon, data: assets.enemies.dragon, xPercentage: 0.5, minPosition: 0.05 },
    { name: 'knight', id: 'player', class: PlayerGreece, data: assets.players.knight },
    { name: 'flyingIsland', id: 'flyingIsland', class: FlyingIsland, data: assets.platforms.island, xPercentage: 0.82, yPercentage: 0.55 },
    { name: 'tubeU', id: 'minifinishline', class: FinishLine, data: assets.obstacles.tubeU, xPercentage: 0.66, yPercentage: 0.9 },
    { name: 'flag', id: 'finishline', class: FinishLine, data: assets.obstacles.flag, xPercentage: 0.875, yPercentage: 0.275 },
    { name: 'hillsEnd', id: 'background', class: BackgroundTransitions, data: assets.transitions.hillsEnd },
    { name: 'lava', id: 'lava', class: Lava, data: assets.platforms.lava, xPercentage: 0, yPercentage: 1 },
  ];

Code from GameSetterGreece.js is then passed through the function in GameSetup.js that initializes the game levels:

// Initialize Game Levels
    function GameLevelSetup(GameSetter, path, callback, passive = false) {
      var gameObjects = new GameSet(GameSetter.assets, GameSetter.objects, path);
      return new GameLevel({ tag: GameSetter.tag, callback: callback, objects: gameObjects.getGameObjects(), passive: passive });
    }

// code hidden
GameLevelSetup(GameSetterGreece, this.path, this.playerOffScreenCallBack);

Chimera (dragon)

Image

We created a new dragon enemy that changes direction every time the dragon changes direction:

if (this.speed < 0) { 
this.canvas.style.transform = 'scaleX(1)'; } 
else { this.canvas.style.transform = 'scaleX(-1)'; }

Rising Lava Feature

https://github.com/CyberLord09/CSSE1_Final/assets/142454293/d0cef575-f3ee-4648-826a-80d13a6a1b81

Image

Before the lava erupts, there are a couple of things that the game uses to inform the player that lava is rising. The first is a large warning symbol.

this.warningSymbol = document.createElement('img');
this.warningSymbol.src = "/platformer3x/images/platformer/sprites/alert.gif";
this.warningSymbol.style.position = 'absolute';
this.warningSymbol.style.top = '35%';
this.warningSymbol.style.left = '50%';
this.warningSymbol.style.transform = 'translate(-50%, -50%)';
this.warningSymbol.style.display = 'none'; // Initially hidden
document.body.appendChild(this.warningSymbol);

Image

https://github.com/CyberLord09/CSSE1_Final/assets/142454293/30589952-0e06-4d46-8179-d980121d8fce

In order for the player to interact with the lava, we added a case in playerGreece.js for lava collisions. The HP bar is drawn in the drawHPbox function, and the parameters are placed in the constructor. Each time the player collides with the lava, 1/3 of the player health is lost (33 out of 99 health) and the player is sent up in the air as a jump. Upon the last collision, the player loses all HP (with the HP bar cleared) and dies.

case "lava": // Note: Goomba.js and Player.js could be refactored

                if (this.collisionData.touchPoints.other.id === "lava") {
                    if (GameEnv.difficulty === "normal" || GameEnv.difficulty === "hard") {
                        if (this.state.isDying == false) {
                            if(this.currentHp == 33){
                                this.currentHp -= 33;
                                this.drawHpBox();
                                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{
                                this.setY(this.y - (this.bottom * 0.6));
                                this.currentHp -= 33;
                            }
                        }
LiliWuu commented 1 month ago

Mini Level

Creating Level

Image We created a new mini level that acts as a level that allows the player to collect coins without getting killed by enemies. We did this by adding new game objects to our Greece mini level in GameSetterGreeceMini.js:

const objects = [
    { name: 'mini', id: 'background', class: Background, data: assets.backgrounds.mini },
    { name: 'rockslava', id: 'platform', class: Platform, data: assets.platforms.rockslava },
    // { name: 'rock', id: 'platform', class: Platform, data: assets.platforms.rock },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.59, yPercentage: 0.35 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.6268, yPercentage: 0.35 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3, yPercentage: 0.35 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3368, yPercentage: 0.35 },

    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3, yPercentage: 0.85 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3368, yPercentage: 0.85 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.4684, yPercentage: 0.85 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.6, yPercentage: 0.85 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.6368, yPercentage: 0.85 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3736, yPercentage: 0.35 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3736, yPercentage: 0.4334 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3736, yPercentage: 0.5167 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.3736, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.4104, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.4472, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.484, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.5208, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.5576, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.5576, yPercentage: 0.5167 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.5576, yPercentage: 0.4334 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.5576, yPercentage: 0.35 },

    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8576, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8576, yPercentage: 0.5167 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8576, yPercentage: 0.4334 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8576, yPercentage: 0.35 },

    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8576, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.8208, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.784, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.7472, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.7104, yPercentage: 0.6 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.6736, yPercentage: 0.6 },

    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.1736, yPercentage: 0.35 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.1736, yPercentage: 0.4334 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.1736, yPercentage: 0.5167 },
    { name: 'blocks', id: 'jumpPlatform', class: BlockPlatform, data: assets.platforms.lava, xPercentage: 0.1736, yPercentage: 0.6 },

    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.28, yPercentage: 0.25 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.32, yPercentage: 0.25 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.29, yPercentage: 0.75 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.33, yPercentage: 0.75 },
    { name: 'star', id: 'star', class: Star, data: assets.obstacles.star, xPercentage: 0.4584, yPercentage: 0.75 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.40, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.42, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.44, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.46, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.48, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.5, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.59, yPercentage: 0.75 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.63, yPercentage: 0.75 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.58, yPercentage: 0.25 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.62, yPercentage: 0.25 },

    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.6475, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.6675, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.6875, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.7075, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.7275, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.7475, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.7675, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.7875, yPercentage: 0.5 },
    { name: 'coin', id: 'coin', class: Coin, data: assets.obstacles.coin, xPercentage: 0.8075, yPercentage: 0.5 },
    { name: 'knight', id: 'player', class: PlayerMini, data: assets.players.knight },
    { name: 'tubeD', id: 'finishline', class: FinishLine, data: assets.obstacles.tubeD, xPercentage: 0, yPercentage: 0.0685 },
    { name: 'tubeU', id: 'finishline', class: FinishLine, data: assets.obstacles.tubeU, xPercentage: 0.85, yPercentage: 0.85 },
    { name: 'greeceEnd', id: 'background', class: BackgroundTransitions,  data: assets.transitions.greeceEnd },
  ];

Tubes

Image The mini level has two tubes, one at the top left corner and the other at the bottom right corner. We created a new property in PlayerMini.js to have the player's y position start at the top of the screen and drop down from where the first tube is:

constructor(canvas, image, data) {
        super(canvas, image, data);
        const scaledHeight = GameEnv.innerHeight * (100 / 832);
        const finishlineX = .01 * GameEnv.innerWidth;
        this.setX(finishlineX);
        this.hillsStart = true;

        // Goomba variables, deprecate?
        this.timer = false;
        GameEnv.invincible = false; // Player is not invincible 
    }

// code hidden
update(){
            super.update();
        if (this.hillsStart) {
                this.setY(0);
                this.hillsStart = false;
            }
        }

The second tube in the mini level should then transition from the mini level back to the Greece level.

LiliWuu commented 1 month ago

Boss Level

Creating Boss

Image We created a new boss level that acts as the final boss at the end of all the levels. First, we added code to GameSetterBoss.js so that there is a Enemy called boss with its properties and spritesheet:

boss: {
      src: "/images/platformer/sprites/boss.png",
      width: 64,
      height: 64,
      scaleSize: 320,
      speedRatio: 0.6,
      animationSpeed: 6,
      idleL: { row: 9, frames: 0, idleFrame: { column: 1, frames: 0 } },
      idleR: { row: 11, frames: 0, idleFrame: { column: 1, frames: 0 } },
      left: { row: 9, frames: 8, idleFrame: { column: 7, frames: 0 } },
      right: { row: 11, frames: 8, idleFrame: { column: 7, frames: 0 } },
      attackL: { row: 13, frames: 5 },
      attackR: { row: 15, frames: 5 },
      death: { row: 20, frames: 5 },
      hitbox: { widthPercentage: 0.3, heightPercentage: 0.8 }
    },

Creating a RandomEvent

To understand how the random event work, first we need to know what is the function "setTimeout()"

The setTimeout(func, delay) Javascript method is used to call a function after a certain period of time. The time after which the function will be called is given by the user in milliseconds.

Here, we first set up a variable called "randomNum" which will be assigned a random number from 3 to 10 (represent 3 to 10 sec after the game starts) And we have two setTimeout(), one inside another. This causes setTimeout() to run forever and cause the random event (the code inside the setTimeout() function) to run repeatedly and randomly for each 3 to 10 sec.

function RandomEvent() {
let randomNum = Math.floor(Math.random() * 8) + 3; // random num from 3 to 10 sec

let startRandomEvent = setTimeout(function request() {
    GameControl.startRandomEvent("boss")
    randomNum = Math.floor(Math.random() * 8) + 3; // reset the random num from 3 to 10 sec
    GameControl.startRandomEvent("narwhalboss")
    randomNum = Math.floor(Math.random() * 8) + 3; // reset the random num from 3 to 10 sec
    startRandomEvent = setTimeout(request, randomNum * 1000);
}, randomNum * 1000); //each random event will happen each 3 to 10 sec 
}

We also added to the random event code in GameControl.js. Because the game already has a Global random event, we can assign the original function an "input" called "event" to make a random event for a specific object. So that for each specific input to the function, the game will run a specific if-statement inside the function and change the variable to a specific number which will run the specific random event we want::

startRandomEvent(event) {
        if(event === "game"){ //game random event
            this.randomEventState = 1;
            this.randomEventId = Math.floor(Math.random() * 3) + 1; //The number multiplied by Math.random() is the number of possible events.
            /**Random Event Key
             * 1: Inverts the Color of the Background
             * 2: Time Stops all Goombas
             * 3: Kills a Random Goomba
            */
        }
        else if(event === "boss"){ //zombie event
            this.randomEventState = 2;
            this.randomEventId = Math.floor(Math.random() * 3) + 1; //The number multiplied by Math.random() is the number of possible events.
            /**Random Event Key
             * 1: Stop the Zombie
             * 2: Let the Zombie to walk left
             * 3: Let the Zombie to walk right
            */
        }
    },
    if (GameControl.randomEventId === 1 && GameControl.randomEventState === 2){ //event: stop the zombie
            this.direction = "idle"; 
            GameControl.endRandomEvent();
        }
        if (GameControl.randomEventId === 2 && GameControl.randomEventState === 2){ //event: stop the zombie
            this.direction = "a"; 
            GameControl.endRandomEvent();
        }
        if (GameControl.randomEventId === 3 && GameControl.randomEventState === 2){ //event: stop the zombie
            this.direction = "d"; 
            GameControl.endRandomEvent();
        }

Death Animation

If we want an Enemy to play a Death animation after it has been hit on top by the player. We can't just simply change the animation to "death". Because different devices have a unique speed to play the animation frame by frame, so if we just change the animation to "death" and use a setTimeout() function and destroy function to set up a delayed death, hoping it will play the completed animation before it was destroyed, probably it won't work and for some devices, the death animation may play twice and for other it not even able to play once.

if(this.collisionData.touchPoints.other.bottom && this.immune == 0){
                this.state.animation = "death";
                if(!this.state.isDying && this.state.animation == "death"){
                    this.frameX = 0;
                }
                this.state.isDying = true;
                GameEnv.invincible = true;
                GameEnv.goombaBounce = true;
                GameEnv.playSound("goombaDeath");
            }

We can add an if-statement to check if the enemy has been hit on top by the player if it is, then we set the animation to "death", and frameX to 0 by checking the property "this.state.isDying" (Finite State Machine). We turn the this.state.isDying to true for the next check.

 //overwrite the method
    updateFrameX() {
        // Update animation frameX of the object
        if(!this.state.isDying || this.state.animation != "death"){
            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;
            }
        }
        else if(this.state.isDying && this.state.animation == "death"){
            this.animationSpeed = 50;
            if (this.frameX < this.maxFrame) {
                if(this.counter > 0){
                    this.frameX = this.frameX; 
                    this.counter--;
                }
                else{
                    this.frameX++
                    this.counter = this.animationSpeed;
                }
            } else {
                this.destroy();
            }
        }

    }

HP Bar

Image We first created the canvas for the HP bar:

this.maxHp = 100; // Maximum health points
this.currentHp = 100; // Current health points
this.hpBar = document.createElement("canvas");
this.hpBar.width = 100; 
this.hpBar.height = 15;
document.querySelector("#canvasContainer").appendChild(this.hpBar);

Then, we ceated a function called drawHPBox which gets called repeatedly to draw the HPBox on the screen:

drawHpBox() { //Hp box

    // Position and size of the health bar
    const hpBarWidth  =  this.hpBar.width; // The width of the health bar matches the boss's width
    const hpBarHeight  =  this.hpBar.height; // A fixed height for the health bar
    const hpBarX  = (this.x  +  this.canvasWidth/2  -  this.hpBar.width/2); // Position above the boss
    const hpBarY  =  this.y  -  this.canvasHeight/40; // 20 pixels above the boss
    this.hpBar.id  =  "hpBar"; // Calculate health percentage
    const hpPercentage  =  this.currentHp  /  this.maxHp;

    this.hpBar.getContext('2d').fillStyle  =  'gray'; // Draw the background (gray)
    this.hpBar.getContext('2d').fillRect(0, 0, hpBarWidth, hpBarHeight); // Draw the health bar (green, based on current health)
    this.hpBar.getContext('2d').fillStyle  =  'green';
    this.hpBar.getContext('2d').fillRect(0, 0, hpBarWidth  *  hpPercentage, hpBarHeight);

    this.hpBar.style.position  =  'absolute'; //code from Flag.js, define the style of the Hp Bar
    this.hpBar.style.left  =  `${hpBarX}px`;
    this.hpBar.style.top  =  `${hpBarY}px`;
    this.hpBar.style.borderRadius  =  '5px';
    this.hpBar.style.width  =  `${hpBarWidth}px`;
    this.hpBar.style.height  =  `${hpBarHeight}px`;
    this.hpBar.style.border  =  '2px solid black';
}

Finally, we had to reduce the HP whenever a collision occurs with the player and the boss. Naturally, this code will exist in the collision action section. Here, we check for collisions with the player. First, we check if the currentHp is 0 (which means that the boss has been hit 5 times). If the currentHP is 0, we set the current animation state to be death. We then set the isDying state, invincible, goombaBounce to be true. Then, we play the GoombaDeath sound as the boss dies:

Else, we reduce the currentHP by 20, and in the loop, the drawHPBox function gets called again, so the amount of Green in the Bar gets reduced and the Gray shows.

else if(this.collisionData.touchPoints.other.bottom  &&  this.immune  ==  0){
    if(this.currentHp  ==  0){
        this.state.animation  =  "death";
        if(!this.state.isDying  &&  this.state.animation  ==  "death"){
            this.frameX  =  0;
        }
        this.state.isDying  =  true;
        GameEnv.invincible  =  true;
        GameEnv.goombaBounce  =  true;
        GameEnv.playSound("goombaDeath");
    }
else{
    this.currentHp  -=  20;
    GameEnv.goombaBounce  =  true;
    }
}

Attack Animation

We added an attack range property to the boss so that the player can attack the boss from further away. This is so that the player can press the shift key and actually do damage to the Boss' HP, and makes the already challenging level a little bit easier.

this.attackRange  =  50;
if (GameEnv.playerAttack  && (Math.abs((this.x  +  this.canvasWidth)/2-(GameEnv.x  +  GameEnv.canvasWidth)/2) < (this.canvasWidth/2  +  this.attackRange))) {
    this.currentHp  -=  1;
}
Hypernova101 commented 1 month ago

Base File for Flying Enemy

Same as the Base file for Enemy, we can have a new base file for FlyingEnemy to clean up the code Before we create the Base file, we can see that the codes for the FlyingEnemy are not consistent from level to level Some of them just copy and paste all the code from FlyingGoomba.js and some of them choose to extend the FlyingGoomba

Both ways have repeated code segments that aren't necessary and are easier to confuse others (CSSE students for next year).

How to fix?:

Create a javascript file called FlyingEnemy, this file is for FlyingEnemy that has a symmetrical sprite/image like Goomba. Create a javascript file called FlyingEnemyOneD, this file is for FlyingEnemy that doesn't have a symmetrical sprite/image like Dragon. Copy and paste the code from FlyingGoomba into Both FlyingEnemy.js and FlyingEnemyOneD.js, we will use this code as basic to build our Base file. We need to take out all the code inside the update() function and apply the single responsibility principle, creating different functions and categorizing the take-out code into the functions based on their purposes. Change the class extended for each flying enemy to either FlyingEnemy or FlyingEnemyOneD (if your sprite/image is symmetrical, then extend the FlyingEnemy, if not, extend the FlyingEnemyOneD Lastly, remove all other code except the update function, and for the update function, it should include only super.update() And we will have a simple code for our flying enemy which is easier to read and more pleasing our eyes.

import FlyingEnemy from './FlyingEnemy.js';

export class FlyingGoomba extends FlyingEnemy {

    // constructors sets up Character object 
    constructor(canvas, image, data, xPercentage, yPercentage, name, minPosition){
        super(canvas, image, data, xPercentage, yPercentage, name, minPosition);
        this.enemySpeed();
    }
    update() {
        super.update();

    }
}

export default FlyingGoomba;
TianbinLiu commented 1 month ago

Player change

Image Image

As you see, the player will change to a zombie when it hits the item block.

At first, we try to achieve that by removing the original player(Mario) and replacing it with our zombie player during the gameplay. We tried, and we failed. Because of the time limit, we don't want to spend more time on this hard way. So according to Mr.M's advice, we plan to make the "parallel players".

Parallel players

Image Instead of replacing the player, we plan to add two players to our level and make one, the zombie player, invisible in the beginning.

  1. We create PlayerZombie.js for our zombie player and PlayerBoss.js for the Mario player(therefore we don't need to change the PlayerHills.js)

  2. And because our zombie sprite sheet has only one direction, so we added a new Base Player file, PlayerBaseOneD.js, for sprites that only have one direction.

The only difference between PlayerBaseOneD.js and PlayerBase.js is we added a if-statement to the updateAnimation() function.

    if(this.state.direction == "left"){
          this.canvas.style.transform = 'scaleX(-1)';
    }
    else{
          this.canvas.style.transform = 'scaleX(1)';
    }
    updateAnimation() {
        switch (this.state.animation) {
            case 'idle':
                if(this.state.direction == "left"){
                    this.canvas.style.transform = 'scaleX(-1)';
                }
                else{
                    this.canvas.style.transform = 'scaleX(1)';
                }
                this.setSpriteAnimation(this.playerData.idle);
                break;
//...code hidden
        }
    }

The PlayerZombie.js extends the new base file PlayerBaseOneD.js we created instead of extending the old PlayerBase.js.

  1. To make the player invisible, we create a new property to the constructor called "this.invisible" for both player files and we set it equal to true.

    constructor(canvas, image, data) {
        super(canvas, image, data);
    
        this.invisible = true;
    
        // Goomba variables, deprecate?
        this.timer = false;
        GameEnv.invincible = false; // Player is not invincible 
    }

And then we overwrite the function draw() and add an if-statement to it so that when "this.invisible"=true, the player image won't draw/won't appear and therefore the player is invisible.

        if (!this.invisible) {
            this.ctx.fillText(this.name, 0, this.canvas.height / 4);
            this.ctx.drawImage(
                this.image,
                this.frameX * this.spriteWidth,
                this.frameY * this.spriteHeight,
                this.spriteWidth,
                this.spriteHeight,
                0,
                0,
                this.canvas.width,
                this.canvas.height
            );
        }
  1. Last, we create a new file "BossItem.js" for our itemBlock. Everything is the same except we change "GameControl.startRandomEvent("game");" to "GameControl.startRandomEvent("zombie");"

startRandomEvent("game") is the global event includes time stop, background change, and randomly kill a Goomba in the Hill level.

startRandomEvent("zombie"); is the new event we create to change the "this.invisible"

So that when Mario player hits our itemBlock, it will run the function startRandomEvent("zombie"); and change "this.invisible" to false and let the zombie player appear and the Mario player disappear/invisible.