Source: src/scenes/MenuScene.js

import Phaser from "phaser";

import Bird from '../../PlayScene-classes/Bird'
import Backdrop from "../../PlayScene-classes/Backdrop";
import Floor from "../../PlayScene-classes/Floor";

import eventsCenter from "../../PlayScene-classes/EventsCenter";
import {Difficulty} from '../../enums/States'
import Utils from "../../helper-classes/Utils";
import CharacterCreator from "../../MenuScene-classes/CharacterCreator";

/**
 * @classdesc Main menu scene for the game
 * This class inherits the Scene object from phaser,
 * which allows for use of the methods preload, create, and update.
 * This scene is used for the main menu navigation and animation.
 * @author Christian P. Auman
 * @class
 * @module MenuScene
 */
class MenuScene extends Phaser.Scene {
	constructor() {
		super('MenuScene');
		this.exit = false
		this.difficulty = Difficulty.MEDIUM
		this.difficultyButtonWidth = 160
		this.startBirdSize = 120
		this.playButtonSize = {
			width: 145,
			height: 95
		}
	}
	
	/**
	 * 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, the method is also creating new objects from the
	 * actual game which allows for easy reuse of the previously written code and animations
	 */
	preload() {
		// this.sceneLoader = new SceneLoader()
		// this.sceneLoader.preload()
		
		const velocity = 100
		// object instantiation
		this.bird = new Bird(this)
		this.floor = new Floor(this, {x: 0, y: 0})
		this.background = new Backdrop(this)
		this.floor.velocity = velocity
		this.characterCreator = new CharacterCreator(this)
		
		// preloading
		this.load.spritesheet('bird', 'assets/bird-spritesheet.png', {
			frameWidth: 717,
			frameHeight: 610,
			startFrame: 0,
			endFrame: 3,
		})
		this.load.spritesheet('play-button', '/assets/play-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: this.playButtonSize.height,
			frameWidth: this.playButtonSize.width
		})
		this.load.spritesheet('character-button', '/assets/character-selection-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: this.playButtonSize.height,
			frameWidth: this.playButtonSize.width
		})
		this.load.spritesheet('back-button', '/assets/back-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: this.playButtonSize.height,
			frameWidth: this.playButtonSize.width
		})
		this.load.spritesheet('easy-button', '/assets/easy-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: 160,
			frameWidth: this.difficultyButtonWidth
		})
		this.load.spritesheet('medium-button', '/assets/medium-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: 160,
			frameWidth: this.difficultyButtonWidth
		})
		this.load.spritesheet('hard-button', '/assets/hard-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: 160,
			frameWidth: this.difficultyButtonWidth
		})
		this.load.spritesheet('insane-button', '/assets/insane-button.png', {
			startFrame: 0,
			endFrame: 1,
			frameHeight: 160,
			frameWidth: this.difficultyButtonWidth
		})
		
		// object preloading calls
		this.bird.preload()
		this.floor.preload()
		this.background.height = this.game.canvas.height * 3
		this.background.startingY = -this.game.canvas.height * 2
		this.background.starCount = 50
		this.background.preload()
		this.characterCreator.preload()
	}
	
	/**
	 * This method is called once and is used to create every game object
	 * that is used for animations and scene backdrop. This method is the main setup for the main menu
	 * and is used to create all the animations and UI the user will view and interact with.
	 */
	create() {
		this.characterCreatorY = -this.game.canvas.height * 1.5
		// setting up title text
		this.titleText = this.add.text(this.game.canvas.width / 2, this.game.canvas.height / 2, 'Flappers', {fontFamily: 'FlappyFont, sans-serif', fontSize: '5.5vw'})
		this.titleText.setShadow(0, 10, '#5e5e5e', 0, false, true)
		this.titleText.setOrigin(0.5, 0.5)
		this.offset = 300
		this.titleText.y -= this.titleText.displayHeight / 2 + this.offset / 2
		
		// object creation
		this.bird.create()
		this.floor.create()
		this.characterCreator.setup({bird: this.bird.bird})
		this.characterCreator.create()
		this.background.create()
		this.background.setPosition(0, this.game.canvas.height - this.floor.getHeight())
		this.floor.ground.displayHeight = this.game.canvas.height
		this.floor.ground.y = this.floor.floor.y + this.floor.ground.displayHeight
		
		// disabling the bird since the user will not be controlling it in the main menu
		// this.bird.bird.body.setEnable(false)
		this.bird.bird.body
			.setCollideWorldBounds(false)
			.setMaxVelocityY(2800)
		// positioning the bird
		this.bird.bird.setOrigin(0.5, 0.5)
		this.bird.bird.x = this.game.canvas.width / 2
		this.bird.bird.y = this.game.canvas.height / 2
		this.bird.bird.displayWidth = this.startBirdSize
		this.bird.bird.displayHeight = this.startBirdSize
		
		const BUTTON_OFFSET = 20
		// play button setup and creation
		this.playButton = this.add.image(this.game.canvas.width / 2, this.game.canvas.height / 2, 'play-button')
		this.playButton.setOrigin(0.5, 0.5)
		this.playButton.setInteractive({cursor: 'pointer'})
		this.playButton.y += this.titleText.displayHeight / 2 + this.offset / 2
		
		this.backButton = this.add.image(this.bird.bird.x, (this.characterCreatorY + this.game.canvas.height / 4), 'back-button')
			.setDepth(10000)
			.setScale(0)
			.setAlpha(0)
			.setInteractive({cursor: 'pointer'})
		
		/**
		 * Creating difficulty buttons and hiding them to be animatedIn in the future.
		 * @type {Phaser.GameObjects.Sprite}
		 */
		this.easyButton = this.add.sprite(this.playButton.x - this.difficultyButtonWidth - this.difficultyButtonWidth / 2, this.playButton.y, 'easy-button')
			.setScale(0, 0)
			.setAlpha(0)
			.setInteractive({cursor: 'pointer'})
		this.mediumButton = this.add.sprite(this.playButton.x - this.difficultyButtonWidth / 2, this.playButton.y, 'medium-button')
			.setScale(0, 0)
			.setAlpha(0)
			.setInteractive({cursor: 'pointer'})
		this.hardButton = this.add.sprite(this.playButton.x + this.difficultyButtonWidth / 2, this.playButton.y, 'hard-button')
			.setScale(0, 0)
			.setAlpha(0)
			.setInteractive({cursor: 'pointer'})
		this.insaneButton = this.add.sprite(this.playButton.x + this.difficultyButtonWidth + this.difficultyButtonWidth / 2, this.playButton.y, 'insane-button')
			.setScale(0, 0)
			.setAlpha(0)
			.setInteractive({cursor: 'pointer'})
		
		this.playButton.x += - this.playButtonSize.width / 2 - BUTTON_OFFSET / 2
		
		
		// Character creator button setup and creation
		this.characterCustomizationButon = this.add.image(this.game.canvas.width / 2 + this.playButtonSize.width / 2 + BUTTON_OFFSET / 2, this.game.canvas.height / 2, 'character-button')
			.setOrigin(0.5, 0.5)
			.setInteractive({cursor: 'pointer'})
		this.characterCustomizationButon.y += this.titleText.displayHeight / 2 + this.offset / 2
		
		// setting values for the animations to animate from
		this.titleText.alpha = 0
		this.titleText.scale = 0
		
		// whenever the "resetscene" event is called, this callback function will call the restartScene() method
		// if no data was pasted to the callback function. This is because whenever playscene wants to transition to
		// this scene, it will not pass anything to the loadscene function. However, whenever transitioning from this
		// scene to the PlayScene, this scene will pass a difficulty value and this will still be called again. This ensures
		// the scene wont be reset when it shouldn't need to.
		eventsCenter.on('resetscene', (e) => {
			if (!e) {
				this.restartScene()
			}
		})
		
		/**
		 * This.input.on('gameobjectdown') will execute the following callback function
		 * whenever the play button is clicked by the user.
		 */
		this.input.on('gameobjectdown', (pointer, gameobject) => {
			// sets the current frame of the play button to 1, which is an image of the button clicked down
			if (gameobject === this.backButton) {
				this.backButton.setFrame(1)
			}
		})
		/**
		 * this waits for the user to lift off of the button and will reset the button frame back to its original
		 * frame
		 */
		this.input.on('gameobjectup', (pointer, gameobject) => {
			if (gameobject === this.backButton) {
				this.tweens.killAll()
				this.backButton.setFrame(0)
				this.cameras.main.zoomTo(1, 2000, "Sine.easeInOut")
				this.floor.startG()
				this.background.startG()
				Utils.scaleOut(this, {from: {scale: 0.8, alpha: 1}}, this.backButton)
				setTimeout(() => {
					this.cameras.main.pan(this.cameras.main.centerX, this.game.canvas.height / 2, 1500, 'Sine.easeInOut', false, () => {
						this.titleText
							.setScale(0)
							.setAlpha(0)
						this.playButton
							.setScale(0)
							.setAlpha(0)
						this.characterCustomizationButon
							.setScale(0)
							.setAlpha(0)
						this.initiate()
					})
					setTimeout(() => {
						this.bird.bird.play('fly-infinite')
						this.animateBirdIn()
					}, 1300)
				}, 1000)
			}
		})
		// sets up animations and everything for the menu scene.
		this.initiate()
		if(!this.loaded) {
			this.loaded = true
			console.log('innintisn')
			eventsCenter.emit('loaded')
		}
	}
	
	initiate() {
		/**
		 * This creates an animation for the title text game object. It
		 * will animate from 0 opacity and 0 scale, to it's default values.
		 * @type {Phaser.Tweens.Tween}
		 */
		const titleTween = this.tweens.add({
			targets: [this.titleText],
			scale: 1,
			alpha: 1,
			duration: 3000,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
		
		/**
		 * This following code will add a looping animation to the title text object
		 * after the first initial animation is completed
		 */
		titleTween.on('complete',() => {
			if (this.titleTween) {
				this.titleTween.stop()
			}
			this.titleTween = this.tweens.add({
				targets: [this.titleText],
				scale: 1.1,
				duration: 1000,
				ease: 'Back.out',
				easeParams: [4, 3],
				loop: -1,
				yoyo: true
			})
		})
		
		// setting up values for the buttons to animate from
		this.playButton.alpha = 0
		this.playButton.scale = 0
		this.characterCustomizationButon.alpha = 0
		this.characterCustomizationButon.scale = 0
		
		/**
		 * This Phaser Tween animates the play button from 0 opacity and scale to
		 * its default values
		 * @type {Phaser.Tweens.Tween}
		 */
		const playButtonTween = this.tweens.add({
			targets: [this.playButton],
			scale: 1,
			alpha: 1,
			duration: 1000,
			delay: 1000,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
		const characterCustomizationButtonTween = this.tweens.add({
			targets: [this.characterCustomizationButon],
			scale: 1,
			alpha: 1,
			duration: 1000,
			delay: 1200,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
		/**
		 * This section will add a looping animation after the initial
		 * animation is completed.
		 */
		playButtonTween.on('complete', () => {
			this.tweens.add({
				targets: [this.playButton],
				scale: 1.1,
				duration: 1000,
				ease: 'Sine.easeInOut',
				loop: -1,
				yoyo: true
			})
		})
		characterCustomizationButtonTween.on('complete', () => {
			this.tweens.add({
				targets: [this.characterCustomizationButon],
				scale: 1.1,
				duration: 1000,
				ease: 'Sine.easeInOut',
				loop: -1,
				yoyo: true
			})
		})
		/**
		 * This.input.on('gameobjectdown') will execute the following callback function
		 * whenever the play button is clicked by the user.
		 */
		this.input.once('gameobjectdown', (pointer, gameobject) => {
			// sets the current frame of the play button to 1, which is an image of the button clicked down
			if (gameobject === this.playButton || gameobject === this.easyButton || gameobject === this.mediumButton || gameobject === this.hardButton || gameobject === this.insaneButton || gameobject === this.characterCustomizationButon) {
				gameobject.setFrame(1)
			}
		})
		/**
		 * this waits for the user to lift off of the button and will reset the button frame back to its original
		 * frame
		 */
		const stopMenuButtonAnimations = (gameobject) => {
			gameobject.setFrame(0)
			playButtonTween.stop()
			characterCustomizationButtonTween.stop()
		}
		this.input.once('gameobjectup', (pointer, gameobject) => {
			if (gameobject === this.playButton) {
				stopMenuButtonAnimations(gameobject)
				const tween = this.animateMenuOut()
				tween.on('complete', () => {
					this.createDifficultyButtons()
				})
			} else if (gameobject === this.characterCustomizationButon) {
				stopMenuButtonAnimations(gameobject)
				this.onCharacterCustomizationButtonHit()
			}
		})
	}
	
	animateMenuOut() {
		return this.tweens.add({
			targets: [this.playButton, this.characterCustomizationButon],
			scale: 0,
			alpha: 0,
			ease: 'Elastic.out',
			duration: 300,
			easeParams: [1, 3]
		})
	}
	
	onCharacterCustomizationButtonHit() {
		const tween = this.animateMenuOut()
		tween.on('complete', () => {
			this.bird.bird.depth = 10000
			this.floor.stopG()
			this.background.stopG()
			this.bird.stopIdleTween()
			const y = -this.game.canvas.height * 1.5
			setTimeout(() => {
				this.cameras.main.pan(this.cameras.main.centerX, y, 2000, 'Sine.easeInOut', false, () => {
				
				})
			}, 2000)
			this.cameras.main.zoomTo(1.2, 2000, "Sine.easeInOut")
			// const birdZoomTween = Utils.scale(this, {duration: 700, scaleX: this.bird.bird.scaleX -0.05, scaleY: this.bird.bird.scaleY -0.05}, this.bird.bird)
			
			// const birdTranslateTween = Utils.translateY(this, {duration: 800, y: "-= 100", ease: 'Sine.easeInOut', easeParams: [5, 3]}, this.bird.bird)
			this.bird.stopIdleTween()
			this.bird.bird.anims.stop()
			this.animateBirdOut()
			setTimeout(() => {
				this.tweens.killAll()
				Utils.scaleIn(this, {scale: 0.8, delay: 2000}, () => {
					this.backButtonTween = this.oscillate({scale: 0.9}, this.backButton)
				}, this.backButton)
				this.bird.bird.body
					.setAllowGravity(false)
					.setEnable(false)
				this.bird.bird.y = y
				this.bird.bird.x = this.game.canvas.width / 2
				this.bird.bird.angle = 0
				this.bird.bird.setFrame(1)
			}, 2200)
			
		})
	}
	animateBirdIn() {
		this.bird.bird.y = -this.bird.bird.displayHeight / 2
		this.bird.bird.x = this.game.canvas.width / 2 - 200
		this.bird.bird.displayHeight = this.startBirdSize
		this.bird.bird.displayWidth = this.startBirdSize
		const tween = Utils.translateY(this, {y: this.game.canvas.height / 2, ease: 'Back.out', easeParams: [1, 1]}, this.bird.bird)
		const tween2 = Utils.translateX(this, {x: this.game.canvas.width / 2, ease: 'Sine.easeInOut', easeParams: [3, 1]}, this.bird.bird)
		tween.on('complete', () => {
			this.bird.setupIdle()
		})
	}
	
	/**
	 * Moved to a method in order to reuse this segment of code. This will setup all of the difficulty button values,
	 * and will animate each one in with a certain delay. After those animations complete, it will animate the buttons
	 * scaling up and down for an idle animation. This also sets up a callback function thats called whene the user
	 * clicked on one of the buttons
	 */
	createDifficultyButtons() {
		// setting default values for each button
		this.easyButton.alpha = 0
		this.easyButton.scale = 0
		this.mediumButton.alpha = 0
		this.mediumButton.scale = 0
		this.hardButton.alpha = 0
		this.hardButton.scale = 0
		this.insaneButton.alpha = 0
		this.insaneButton.scale = 0
		
		// animating each button in with an incremented delay to create a chain animation
		let easyTween = this.scaleIn({delay: 0}, this.easyButton)
		let mediumTween = this.scaleIn({delay: 100}, this.mediumButton)
		let hardTween = this.scaleIn({delay: 200}, this.hardButton)
		let insaneTween = this.scaleIn({delay: 300}, this.insaneButton)
		
		// after each button completes it's animation, it will create a new oscillation animation independent to each button
		easyTween.once('complete', () => {
			easyTween = this.oscillate({}, this.easyButton)
		})
		mediumTween.once('complete', () => {
			mediumTween = this.oscillate({}, this.mediumButton)
		})
		hardTween.once('complete', () => {
			hardTween = this.oscillate({}, this.hardButton)
		})
		insaneTween.once('complete', () => {
			insaneTween = this.oscillate({}, this.insaneButton)
		})
		
		/**
		 * If user's mouse or input device is down on a gameobject that is interactable, the callback method is called
		 * which sets the texture's frame to a button down texture
		 */
		this.input.once('gameobjectdown', (pointer, gameobject) => {
			gameobject.setFrame(1)
		})
		/**
		 * once the user releases the input on any of the interactable gameobjects, the callback function will be called
		 * and will start the animations required to transition to the next scene
		 */
		this.input.once('gameobjectup', (pointer, gameobject) => {
			gameobject.setFrame(0)
			// stops all current Phaser.Tween animations
			this.tweens.killAll()
			// finding which button was selected and setting the difficulty accordingly
			if (gameobject === this.easyButton) {
				this.difficulty = Difficulty.EASY
			} else if (gameobject === this.mediumButton) {
				this.difficulty = Difficulty.MEDIUM
			} else if (gameobject === this.hardButton) {
				this.difficulty = Difficulty.HARD
			} else {
				this.difficulty = Difficulty.INSANE
			}
			// animation out every button and starting the transition to next scene
			this.scaleOut({}, this.insaneButton)
			this.scaleOut({delay: 100}, this.hardButton)
			this.scaleOut({delay: 200}, this.mediumButton)
			this.scaleOut({delay: 300}, this.easyButton)
			this.start()
			
		})
	}
	
	/**
	 * DEV ONLY, SHOULD REFACTOR TO USE THE UTILS SCALE IN METHOD
	 * @param delay
	 * @param targets
	 * @returns {Phaser.Tweens.Tween}
	 */
	scaleIn({delay=0}, ...targets) {
		return this.tweens.add({
			targets: targets,
			scale: 1,
			alpha: 1,
			delay: delay,
			ease: 'Elastic.out',
			easeParams: [1, 3]
		})
	}
	/**
	 * DEV ONLY, SHOULD REFACTOR TO USE THE UTILS SCALE OUT METHOD
	 * @param delay
	 * @param targets
	 * @returns {Phaser.Tweens.Tween}
	 */
	scaleOut({delay=0}, ...targets) {
		return this.tweens.add({
			targets: targets,
			scale: 0,
			alpha: 0,
			delay: delay,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
	}
	
	/**
	 * DEV ONLY, SHOULD REFACTOR THIS INTO THE UTILS CLASS
	 * @param delay
	 * @param scale
	 * @param easeParams
	 * @param duration
	 * @param targets
	 * @returns {Phaser.Tweens.Tween}
	 */
	oscillate({delay=0, scale=1.05, easeParams=[3, 1], duration=500}, ...targets) {
		return this.tweens.add({
			targets: targets,
			scale: scale,
			delay: delay,
			ease: 'Sine.easeOut',
			easeParams: easeParams,
			duration: duration,
			loop: -1,
			yoyo: true
		})
	}
	
	/**
	 * The update method is called every frame and is used here to update the floor and background to
	 * simulate a parallax movement horizontally
	 * @param time - time since the scene has started
	 * @param delta - time between each frame
	 */
	update(time, delta) {
		if (this.floor.floor && this.background.buildings) {
			this.floor.update(time, delta)
			this.background.update(time, delta)
		}
	}
	
	/**
	 * Starts all required animations to transition to the next scene. Must have all values set to animate
	 * from before calling this method.
	 */
	start() {
		// after 800 milliseconds, the callback function will execute. This will then stop the previous
		// bird animation and sets the frame to a frame displaying the bird wings in the upright position
		this.bird.bird.stop()
		// 100 milliseconds after the previous timeout, this will play a flap animation simulating the bird
		// flying up into the sky
		setTimeout(() => {
			this.bird.bird.play('fly')
		}, 900)
		// This if statement is to make sure that the following code is only executed once
		if (!this.exit) {
			this.exit = true
			/**
			 * This will wait for half the amount of time for the bird animation, and
			 * will start the loading overlay animation. It also tells the SceneLoader to load the next scene
			 */
			setTimeout(() => {
				this.exit = false
				this.tweens.killAll()
				eventsCenter.emit('loadscene', {sceneToLoad: 'PlayScene', currentScene: this.scene, data: {difficulty: this.difficulty}})
			}, 1500)
			this.animateBirdOut()
			/**
			 * this animation will animate out the title text by changing it's opacity and scale to 0
			 */
			this.tweens.add({
				targets: [this.titleText],
				scale: 0,
				alpha: 0,
				duration: 2000,
				ease: 'Elastic.out',
				easeParams: [1, 1]
			})
		}
	}
	
	animateBirdOut() {
		this.bird.bird.setFrame(3)
		setTimeout(() => {
			this.bird.bird.setFrame(0)
		}, 500)
		setTimeout(() => {
			this.bird.bird.setFrame(3)
		}, 1000)
		/**
		 * This animation is the longest animation out of all the other ones and is used as a
		 * way to know when to animate the loading screen. This animation animates the y position
		 * of the bird past the top of the screen after waiting 1 second
		 * @type {Phaser.Tweens.Tween}
		 */
		this.tweens.add({
			targets: [this.bird.bird],
			y: -300,
			duration: 3000,
			delay: 1000,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
		
		/**
		 * This animation animates the bird flying upwards one more time before shooting up past the top of the screen
		 * Starts animation after 1 second
		 */
		this.tweens.add({
			targets: [this.bird.bird],
			y: "-= 50",
			duration: 1000,
			delay: 0,
			ease: 'Sine.easeOut',
			easeParams: [3, 1]
		})
		
		/**
		 * This animation will animate the bird flying to the right of the screen. Paired with the previous
		 * animations, this will allow a simulation of the bird flying upwards at a curve.
		 */
		this.tweens.add({
			targets: [this.bird.bird],
			x: this.game.canvas.width / 2 + 200,
			duration: 1000,
			delay: 1000,
			ease: 'Elastic.out',
			easeParams: [1, 1]
		})
		
		/**
		 * This animation will rotate the bird while it is flying upwards
		 */
		this.tweens.add({
			targets: [this.bird.bird],
			angle: -100,
			duration: 1000,
			delay: 1000,
			ease: 'Sine.out',
			easeParams: [1, 1]
		})
	}
	
	/**
	 * This will reset the scene to how it started.
	 */
	restartScene() {
		this.bird.resetG()
		this.bird.bird.x = this.game.canvas.width / 2
		this.bird.bird.y = this.game.canvas.height / 2
		this.bird.angle = 0
		this.bird.bird.play('fly-infinite')
		this.initiate()
	}
}

export default MenuScene