Source: PlayScene-classes/Bird.js

import FlappyState from "./FlappyState";

const States = {
    IDLE: 0,
    ALIVE: 1,
    DEAD: 2,
}

/**
 * This class contains functions for manipulating the state of the player as well as manipulating
 * player movement and animations.
 * @classdesc This is the game object representing the player.
 * @author Christian P. Auman
 * @memberOf module:PlayScene
 * @class
 */
class Bird extends FlappyState {
	/**
	 * Sets up all default values for all class variables
	 * @param scene - the Phaser.Scene object used for adding images/sprites/physics/etc...
	 * @constructor
	 */
    constructor(scene) {
		super(States.IDLE);
        this.scene = scene;
        this.hasJumped = false;
        this.jumpVelocity = -800;
		this.time = 0;
		this.tiltDelayStart = 0;
		this.startPosition = {
			x: 416,
			y: 400
		};
		this.idleAnimDelayStart = 0;
		this.idleAnimOffset = 1;
		this.topBoundry = 0;
		this.bottomBoundry = 0;
		this.startSize = 64
		
		// whenever the death event is invoked, this will call the setDead() method
		window.addEventListener('death', () => {
			this.stopG()
		});
    }
	
	/**
	 * This function is called before anything is drawn on the canvas.
	 * This allows for asset preloading which prevents anything from drawing without
	 * it first loading the asset.
	 * Here it is loading the hit-spritesheet.png image from the assets folder
	 */
    preload() {
		this.scene.load.spritesheet('hit', 'assets/hit-spritesheet.png', {
			frameWidth: 64,
			frameHeight: 64,
			startFrame: 0,
			endFrame: 8,
		})
		this.scene.load.spritesheet('bird', 'assets/bird-spritesheet.png', {
			frameWidth: 717,
			frameHeight: 610,
			startFrame: 0,
			endFrame: 3,
		})
    }
	
	/**
	 * This function instantiates a sprite image of a bird and sets up all of the default values.
	 */
	setupBird() {
		this.bird = this.scene.physics.add.sprite(this.startPosition.x, this.startPosition.y, 'bird')
		this.bird.depth = 1000
		this.bird.displayWidth = this.startSize
		this.bird.displayHeight = this.startSize
		this.bird.setOrigin(0.5, 0.5)
		
		/**
		 * setting up the physics for the bird game-object
		 */
		// originally, the bird game-object had a circle collider. However, Phaser's implementation of this
		// certain collider is somewhat poorly designed and will occasionally fall through some pipes.
		// Either that, or I am implementing it improperly which is why it is now using a box collider.
		
		// this.bird.body.setSize(this.bird.body.width - 100, this.bird.body.height - 200)
		// this.bird.body.setOffset(100, 100)
		this.bird.body.collideWorldBounds = true;
		this.scene.physics.world.bounds.bottom = this.bottomBoundry + this.bird.displayHeight / 2.5
		this.bird.body
			.setCircle(Math.round(this.bird.body.sourceWidth / 3), this.bird.body.sourceWidth/4, this.bird.body.sourceWidth/20)
			.setMass(100)
			.setBounce(0, 0)
			.setAccelerationY(-10)
			.setMaxVelocityY(700)
			.setGravityY(2800)
			.setAllowGravity(false)
			.setAllowRotation(true)
			.setDragX(300)
			.setFrictionY(1)
	}
	
	/**
	 * This method is called once and is used to create animations and gameObjects such as the bird.
	 */
    create() {
		// this creates a one shot flap animation for the bird and is used everytime the player jumps
		this.scene.anims.create({
			key: 'fly',
			frames: this.scene.anims.generateFrameNumbers('bird', {frames: [0, 3, 2, 1, 0]}),
			duration: 500,
		})
		
		// this creates an infinite animation which does the same thing as fly, except it repeats on loop until
		// explicitly stopped. This is used for the idle game state before the game starts
		this.scene.anims.create({
			key: 'fly-infinite',
			frames: this.scene.anims.generateFrameNumbers('bird', {frames: [3, 2, 1, 0]}),
			duration: 1000,
			repeat: -1,
		})
		
		// this creates an animation for the hit sprite and its a dust cloud that appears beneath, above, or to the side of the player
		// on any collision that causes death
		this.scene.anims.create({
			key: 'hit-anim',
			frames: this.scene.anims.generateFrameNumbers('hit', {frames: [0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 7, 8]}),
			duration: 800,
		})
		this.createHitSprite()
		this.setupBird()
		this.setupIdle()
    }
	
	/**
	 * The update method is called every frame and if the currentState variable = RUNNING, it calls the
	 * handlePipePool() method to check if pipe needs to be repositioned
	 * and updating the position if needed.
	 */
    update(time, delta) {
		// setting the time class variable to the time given from the Phaser's update method
		this.time = time
		// checking to see if the bird's body has hit a boundary
		this.checkBoundaries()
        if (super.getCurrentState() === States.ALIVE) {
			this.handleTilt(time, delta)
        } else if (super.getCurrentState() === States.DEAD) {
        } else if (super.getCurrentState() === States.IDLE) {
		}
    }
	
	/**
	 * This method is for development purposes and would be removed if the game was going to be
	 * in a production environment. Useful to test the difficulty curve without having to play the game through
	 * @ignore
	 * @private
	 */
	enableGodMode() {
		this.bird.body.setEnable(false)
	}
	
	/**
	 * This method sets up the bird object for it's idle state. It will setup it's looping flying animation,
	 * and will change the currentState class variable to IDLE
	 */
	setupIdle() {
		this.bird.play('fly-infinite')
		this.idleTween = this.scene.tweens.add({
			targets: [this.bird],
			y: `-=${25}`,
			duration: 500,
			ease: 'Sine.easeOut',
			repeat: -1,
			yoyo: true
		})
	}
	
	/**
	 * This method will create a sprite game-object which is used as a feedback animation for when
	 * the bird dies/hits a pipe or the ground.
	 */
	createHitSprite() {
		this.hitSprite = this.scene.add.sprite(0, 0, 'hit')
		this.hitSprite.depth = 100
		this.hitSprite.setOrigin(0.5, 1)
		this.hitSprite.visible = false
	}
	
	/**
	 * This method is for rotating the bird to simulate a more organic movement.
	 * @param time - the time from when the game started.
	 */
	handleTilt(time, delta) {
		// if 600 milliseconds has passed from when the bird last jumped, it will start rotating the
		// bird facing the ground.
		if (time - this.tiltDelayStart > 500) {
			if (this.bird.rotation <= 1) {
				this.bird.rotation += 0.006 * delta
				this.bird.body.rotation += 0.006 * delta
			}
		// If the bird has jumped and the rotation has not met the max of -0.45, it will rotate the bird
		// back up until it reaches -0.45
		} else {
			if (this.bird.rotation >= -0.45) {
				this.bird.rotation -= 0.008 * delta
				this.bird.body.rotation -= 0.008 * delta
			}
		}
	}
	
	/**
	 * Do not use this function any longer. This was the original way that the bird animated up and down
	 * in its IDLE state. However, a better more organic implementation of an IDLE state animation has been
	 * set up.
	 * @deprecated
	 * @param time - the time from when the game started and is given from the Phaser update method
	 * @returns {number} - the offset to use either 1 or -1
	 */
	getIdleAnimOffset(time) {
		let difference = time - this.idleAnimDelayStart
		if (difference > 500) {
			if (this.idleAnimOffset === 1) {
				this.idleAnimOffset = -1
			} else if (this.idleAnimOffset === -1) {
				this.idleAnimOffset = 1
			}
			this.idleAnimDelayStart = time
		}
		return this.idleAnimOffset
	}
	
	/**
	 * This method makes sure that whenever the bird hits the floor, it will fire the death event. This used to
	 * also check for the ceiling, however a newer implementation of that feature has been made which uses the default
	 * Phaser physics to automatically prevent the bird from escaping the ceiling.
	 */
	checkBoundaries() {
		if (this.bird.body.onFloor() && super.getCurrentState() !== States.DEAD) {
			window.dispatchEvent(new Event('death'))
		}
	}
	
	/**
	 * In case the bird had to stop animating, this method will kill the current idle animation
	 */
	stopIdleTween() {
		this.idleTween.stop()
	}
	
	
	/**
	 * This method is called whenever the player presses the space-bar. It's named onJump in order to
	 * allow for future implementation of different controls such as a mouse click. This method will
	 * check the current state of the game and if it is IDLE, it will call the initiate and jump methods
	 * to simulate the bird jumping and starts the game loop. If the state is ALIVE, then we know that
	 * the bird is no longer in the IDLE state and therefore will only jump. The last thing that this method
	 * will do is prevent the player from holding down the inputs which would break the game by making it
	 * super easy to navigate the pipes.
	 */
    onJump() {
        if (super.getCurrentState() === States.ALIVE) {
            if (this.hasJumped === false) {
				this.jump()
                this.hasJumped = true
            }
        } else if (super.getCurrentState() === States.IDLE) {
			this.startG()
			this.jump()
			this.hasJumped = true
        }
    }
	
	/**
	 * This method will reset the tilt delay start. It will give the physics body a velocityY, and will play the flap animation
	 */
	jump() {
		this.tiltDelayStart = this.time
		this.bird.body.velocity.y = this.jumpVelocity
		this.bird.play('fly')
		
	}
	
	/**
	 * This method should be called to start the game loop for the bird game-object.
	 * It will set the class variable currentState to ALIVE and will set up the bird
	 * game-object, so it will have gravity, will be able to collide, and will respond to player inputs.
	 */
	startG() {
		super.start()
		this.bird.body.allowGravity = true
		this.bird.anims.stop()
		if (this.idleTween)
			this.stopIdleTween()
	}
	
	/**
	 * This method is called whenever the bird has died by either a collision from the pipes or the floor.
	 * It will set the class variable currentState to DEAD. It will position the hitSprite game-object and will play the hit animation using
	 * the hitSprite game-object.
	 */
	stopG() {
		if (super.getCurrentState() === States.ALIVE && this.bird.body) {
			super.stop()
			this.hitSprite.visible = true
			this.bird.body.setVelocityX(200)
			this.hitSprite.scale = 2
			if (this.bird.body.onWall()) {
				this.hitSprite.setAngle(-90)
				this.bird.body.velocity.x = -200
				this.hitSprite.x = this.bird.x + this.bird.displayWidth / 2
				this.hitSprite.y = this.bird.y
			} else if (this.bird.body.onCeiling()) {
				this.bird.body.setVelocityY(-300)
				this.hitSprite.setAngle(-180)
				this.hitSprite.x = this.bird.x
				this.hitSprite.y = this.bird.y - this.bird.displayHeight / 2
			} else {
				if (this.bird.y >= this.bottomBoundry - 10) {
					this.hitSprite.scale = 2.5
					this.bird.body.velocity.x = 200
				}
				this.hitSprite.setAngle(0)
				this.hitSprite.x = this.bird.x
				this.hitSprite.y = this.bird.y + this.bird.displayHeight / 2
			}
			this.hitSprite.play('hit-anim')
		}
	}
	
	/**
	 * This method is used to reset the bird object to its original position/values. Used
	 * for when the bird has "died"/collided with the floor or a pipe
	 */
	resetG() {
		super.reset()
		this.bird.x = this.startPosition.x
		this.bird.y = this.startPosition.y
		this.bird.body.allowGravity = false
		this.bird.body.velocity.y = 0
		this.bird.body.velocity.x = 0
		this.bird.rotation = 0
		this.hitSprite.visible = false
		this.setupIdle()
	}
	
	
	/**
	 * Whenever the given input has been unpressed, for example, taking finger off of the mouse button, it will set has jumped to false,
	 * allowing the bird to jump again.
	 */
    onJumpRelease() {
		this.hasJumped = false
    } 
}

export default Bird;