Source: PlayScene-classes/PipeGroup.js

import {Difficulty} from "../enums/States";

/**
 * @classdesc A class that enables easy management and manipulation of a pipe group
 * This class will allow for easy positioning and manipulation of pipes in the
 * flappy bird game. And paired with an array, you can easily create an infinite pool of
 * these PipeGroup objects through recycling
 * @author Christian P. Auman
 * @memberOf module:PlayScene
 * @class
 */
class PipeGroup {
	/**
	 * Setting up default values for the pipe group
	 * @param scene - the scene object from the current scene
	 * @param x - the x position for the pipes
	 * @param y - the y position that the pipes will center from
	 * @param offset - the distance between the top pipe and the bottom pipe
	 * @param velocityX - the speed at which the pipes will move using Phaser's arcade physics
	 */
	constructor(scene, {x=0, y=0, offset=0, velocityX=0, difficulty=Difficulty.MEDIUM}) {
		this.scene = scene
		this.offset = offset
		this.startingOffset = offset
		this.position = {
			x: x,
			y: y
		}
		this.velocityX = velocityX
		this.gotCheckpoint = false
		this.pipeColor = ''
		this.difficulty = difficulty
		if (difficulty === Difficulty.EASY) {
			this.pipeColorChancePicks = ['light-green', 'light-green', 'light-green', 'light-green', 'light-green', 'orange', 'orange']
		} else if (difficulty === Difficulty.MEDIUM) {
			this.pipeColorChancePicks = ['light-green', 'light-green', 'light-green', 'orange', 'orange', 'red']
		} else if (difficulty === Difficulty.HARD) {
			this.pipeColorChancePicks = ['orange', 'orange', 'red']
		} else if (difficulty === Difficulty.INSANE) {
			this.pipeColorChancePicks = ['purple']
		}
	}
	
	preload() {
	}
	
	/**
	 * This method will carry out the creation and setup for each of the pipe and the checkpoint game objects
	 */
	create() {
		// creates a group which allows for easy manipulation of a collection of Phaser Physics game objects
		this.pipeGroup = this.scene.physics.add.group({
			immovable: true,
			allowGravity: false
		})
		
		// creating top pipe
		const tp = this.scene.add.tileSprite(0, 0, 80, this.scene.game.canvas.height, 'pipe')
		this.topPipe = this.scene.physics.add.existing(tp)
		this.topPipe.body.allowGravity = false
		this.topPipe.body.setImmovable(true)
		this.topPipe.displayHeight = this.scene.game.canvas.height
		this.topPipe.setDepth(100)
		
		// creating bottom pipe
		const bp = this.scene.add.tileSprite(0, 0, 80, this.scene.game.canvas.height, 'pipe')
		this.bottomPipe = this.scene.physics.add.existing(bp)
		this.bottomPipe.body.allowGravity = false
		this.bottomPipe.body.setImmovable(true)
		this.bottomPipe.displayHeight = this.scene.game.canvas.height
		this.bottomPipe.setDepth(100)
		
		// creating top cap
		this.topCap = this.scene.physics.add.sprite(0, 0, 'cap')
		this.topCap.body.allowGravity = false
		this.topCap.body.setImmovable(true)
		this.topCap.flipY = true
		this.topCap.setDepth(101)
		this.topCap.displayWidth = this.topPipe.displayWidth - 1
		
		// creating bottom cap
		this.bottomCap = this.scene.physics.add.sprite(0, 0, 'cap')
		this.bottomCap.body.allowGravity = false
		this.bottomCap.body.setImmovable(true)
		this.bottomCap.setDepth(101)
		this.bottomCap.displayWidth = this.bottomPipe.displayWidth - 1
		
		// creating checkpoint
		this.checkpoint = this.scene.physics.add.image(0, 0, '')
		this.checkpoint.displayHeight = this.scene.game.canvas.height
		this.checkpoint.body.velocity.x = this.velocityX
		this.checkpoint.visible = false
		
		// setup group
		this.pipeGroup.add(this.topPipe, true)
		this.pipeGroup.add(this.bottomPipe, true)
		this.pipeGroup.add(this.topCap, true)
		this.pipeGroup.add(this.bottomCap, true)
		
		// updating and setup of initial position and velocity
		this.updatePosition()
		this.updateVelocityX()
	}
	
	/**
	 * This method will reposition each pipe and checkpoint to the newly set position class variable while also
	 * the given offset class variable.
	 */
	updatePosition() {
		// top half
		this.topPipe.y = this.position.y - this.topPipe.displayHeight / 2 - this.offset / 2
		this.topPipe.x = this.position.x + this.topPipe.displayWidth / 2
		this.topCap.y = this.topPipe.y + this.topPipe.displayHeight / 2 - this.topCap.displayHeight / 2
		this.topCap.x = this.topPipe.x
		
		// bottom half
		this.bottomPipe.y = this.position.y + this.bottomPipe.displayHeight / 2 + this.offset / 2
		this.bottomPipe.x = this.position.x + this.bottomPipe.displayWidth / 2
		this.bottomCap.y = this.bottomPipe.y - this.bottomPipe.displayHeight / 2 + this.topCap.displayHeight / 2
		this.bottomCap.x = this.bottomPipe.x
		
		// checkpoint
		this.checkpoint.x = this.topPipe.x
		this.checkpoint.y = this.checkpoint.displayHeight / 2
	}
	
	/**
	 * In case the pipes need to be destroyed for any reason, this method will destroy
	 * and remove every game-object in the current PipeGroup object
	 */
	destroy() {
		this.pipeGroup.destroy()
		this.topPipe.destroy()
		this.topCap.destroy()
		this.bottomPipe.destroy()
		this.bottomCap.destroy()
		this.checkpoint.destroy()
	}
	
	/**
	 * This will be called whenever the bird overlaps the checkpoint and will fire
	 * a checkpoint event for points to be added.
	 */
	handleCheckpointOverlap() {
		// the if statement is here so the event won't be fired every frame the bird is overlapping.
		// if gotCheckpoint is false, it will set it to true and will fire a checkpoint event.
		if (!this.gotCheckpoint) {
			this.gotCheckpoint = true
			window.dispatchEvent(new Event('checkpoint'))
		}
	}
	
	/**
	 * This method handles resetting this current object back to its original values or, whatever values it has currently setup
	 */
	reset() {
		// resets velocity
		this.pipeGroup.setVelocityX(0)
		// disables the moving pipe animations
		this.stopTween()
		// resetting the pipe colors
		this.setPipeColor('')
		// resetting offset
		this.setOffset(this.startingOffset)
	}
	
	/**
	 * This method will set the current pipe's textures using the given color parameter
	 * @param color - the string variable describing the color of the textured pipe that all the pipes in this object will update to.
	 */
	setPipeColor(color) {
		this.pipeColor = color
		this.topPipe.setTexture(`${color?color + '-':''}pipe`)
		this.bottomPipe.setTexture(`${color?color + '-':''}pipe`)
		this.topCap.setTexture(`${color?color + '-':''}cap`)
		this.bottomCap.setTexture(`${color?color + '-':''}cap`)
	}
	
	/**
	 * This method will pick a random color out of the chancePicks array with weighted randomization.
	 * The start of the array will be chosen more often then the end.
	 * @param chancePicks - an array of pipe-colors that the method will pick from with the most common color that will be picked in the start of the array and the least common in the back
	 * @returns {{min: number, max: number}}
	 */
	setupTween(chancePicks) {
		const durations = {
			min: 600,
			max: 1000
		}
		const pick = Phaser.Math.RND.weightedPick(chancePicks)
		this.setPipeColor(pick)
		if (pick === 'light-green') {
			this.setOffset(this.startingOffset + 30)
			durations.min = 1300
			durations.max = 2000
		} else if (pick === 'orange') {
			this.setOffset(this.startingOffset + 25)
			durations.min = 1000
			durations.max = 1300
		} else {
			this.setOffset(this.startingOffset + 20)
			durations.min = 800
			durations.max = 1000
		}
		return durations
	}
	
	/**
	 * If there is no current vertical animation on this object, it will pick a random pipe texture to start with and based on that,
	 * will animate the pipe with the returned durations. However, if there is already an animation, it will see what color it has currently,
	 * and will pick a new color out of the ones that are left. This way adds more complexity to the game as time goes on. By having three different
	 * colors, this object has 3 different difficulties.
	 */
	startVerticalMovement() {
		if (!this.tween) {
			const durations = this.setupTween(this.pipeColorChancePicks)
			this.addTween(durations)
		} else if (this.difficulty === Difficulty.EASY) {
			if (this.pipeColor === 'light-green') {
				const durations = this.setupTween(['orange'])
				this.stopTween()
				this.addTween(durations)
			}
		} else {
			this.stopTween()
			if (this.pipeColor === 'light-green') {
				const durations = this.setupTween(['orange', 'orange', 'orange', 'red'])
				this.addTween(durations)
			} else if (this.pipeColor === 'orange' || this.pipeColor === 'red') {
				const durations = this.setupTween(['red'])
				this.addTween(durations)
			} else {
				const durations = this.setupTween(['purple'])
				this.addTween(durations)
			}
		}
	}
	
	addTween(durations) {
		let ease
		let easeParams = [1, 3]
		if (this.difficulty  === Difficulty.INSANE) {
			ease = 'Sine.easeInOut'
		} else if (this.difficulty === Difficulty.HARD) {
			ease = 'Elastic.inOut'
		} else {
			ease = "Quadratic.easeInOut"
		}
		this.tween = this.scene.tweens.add({
			targets: [this.topPipe, this.bottomPipe, this.topCap, this.bottomCap],
			y: `+=${Phaser.Math.RND.between(100, 150) * Phaser.Math.RND.pick([-1, 1])}`,
			duration: Phaser.Math.RND.between(durations.min, durations.max),
			ease: ease,
			repeat: -1,
			easeParams: easeParams,
			yoyo: true
		})
	}
	
	/**
	 * If there is an animation object on this game-object, this
	 * method will terminate the current animation and will set the
	 * reference to the variable equal to null
	 */
	stopTween() {
		if (this.tween) {
			this.tween.stop()
			this.tween = null
		}
	}
	
	/**
	 * This will set the current offset of the pipe and update the pipes positions
	 * accordingly
	 * @param offset - the offset to animate to
	 */
	setOffset(offset) {
		this.offset = offset
		this.position.x = this.topPipe.x - this.topPipe.displayWidth / 2
		this.updatePosition()
	}
	
	/**
	 * This method will set the class variable velocityX to the given x variable and
	 * will update the physics game-objects in this class accordingly
	 * @param x - the value to set velocityX with
	 */
	setVelocityX(x) {
		this.velocityX = x
		this.updateVelocityX()
	}
	
	/**
	 * This method will set the physics velocityX to the class velocityX value
	 */
	updateVelocityX() {
		this.pipeGroup.setVelocityX(this.velocityX)
		this.checkpoint.body.setVelocityX(this.velocityX)
	}
	
	/**
	 * This method will set the class position variable's x and y to the given x and y values. Then
	 * it will update the pipes positions using those new values accordingly
	 * @param x - x value to position to
	 * @param y - y value to position to
	 */
	setPosition(x, y) {
		this.position.x = x
		this.position.y = y
		this.updatePosition()
	}
	
	/**
	 * Gets and returns the current x and y position of where the current pipe group is located
	 * @returns {{x: number, y: number}}
	 */
	getPosition() {
		return {x: this.topPipe.x, y: this.topPipe.y + this.topPipe.displayHeight/2 + this.offset / 2}
	}
	
	/**
	 * Gets and returns the current width of the PipeGroup
	 * @returns {number}
	 */
	getWidth() {
		return this.topPipe.displayWidth
	}
	
	update() {
	}
	
}

export default PipeGroup