Source: PlayScene-classes/PipeManager.js

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

const States = {
	IDLE: 0,
	RUNNING: 1,
	PAUSED: 2,
}


/**
 * @classdesc Allows for easy manipulation and creation of PipeGroup objects.
 * PipeManager is used to create infinite
 * unique scrolling obstacles in the form of pipes that can
 * have a difficulty ramp as well as difficulty settings.
 * @author Christian P. Auman
 * @class
 * @memberOf module:PlayScene
 */
class PipeManager extends FlappyState {
	/**
	 * Sets up all default values for all class variables
	 * @param scene - the Phaser.Scene object used for adding images/sprites/physics/etc...
	 * @param pipeCount - the amount of pipes that gets created
	 * @param horizontalOffset - the distance between sets of pipes on the horizontal axis
	 * @param verticalOffset - the distance between the top and bottom pipe inside a PipeGroup on the vertical axis
	 * @param startingX - where on the canvas on the x-axis the pipes start from
	 * @param velocityX - how fast the collection of pipes move using Phaser's physics
	 * @constructor
	 */
	constructor(scene, {pipeCount=5, finalHorizontalOffset=0, horizontalOffset=finalHorizontalOffset+200, verticalOffset=0, startingX=null, velocityX=0, difficulty=1}) {
		super(States.IDLE)
		this.scene = scene
		this.pipes = []
		// this.pipeCount = pipeCount
		// this.finalHorizontalOffset = finalHorizontalOffset
		// this.currentHorizontalOffset = horizontalOffset
		// this.startingHorizontalOffset = horizontalOffset
		// this.verticalOffset = verticalOffset
		// this.startingX = startingX
		// this.initialX = startingX
		// this.velocityX = velocityX
		// this.currentState = States.IDLE
		// this.difficulty = difficulty
		// // this.changeOffsetDelayStart = 0
		// // this.changeOffsetDelay = changeOffsetDelay
		//
		// this.checkRecycleDelayStart = 0
		// this.checkRecycleDelay = 100
		//
		// // this will calculate the number of pipes to draw based on the screen width and the horizontal offset. It adds
		// // 2 because it needs one extra to fit the width but also another one as a sort of padding in case the next pipe
		// // doesn't recycle exactly on time.
		// this.pipeCount = Math.ceil(this.scene.game.canvas.width / (finalHorizontalOffset + 80)) + 2
		// console.log(this.pipeCount)
		// this.anims = new Array(pipeCount)
		// if (this.difficulty === Difficulty.EASY) {
		// 	this.horizontalDecrementAmount = 5
		// 	this.movingPipeChanceArray = [1, 1, 2]
		// } else if (this.difficulty === Difficulty.MEDIUM) {
		// 	this.horizontalDecrementAmount = 10
		// 	this.movingPipeChanceArray = [1, 2]
		// } else if (this.difficulty === Difficulty.HARD) {
		// 	this.horizontalDecrementAmount = 15
		// 	this.movingPipeChanceArray = [1, 2, 2]
		// } else if (this.difficulty === Difficulty.INSANE) {
		// 	this.horizontalDecrementAmount = 20
		// 	this.movingPipeChanceArray = [2]
		// }
		// console.log(difficulty)
	}
	
	setConfig({pipeCount=5, finalHorizontalOffset=0, horizontalOffset=finalHorizontalOffset+200, verticalOffset=100, startingX=null, velocityX=0, difficulty=1}) {
		if (this.pipes) {
			this.pipes.map(pipe => {
				pipe.destroy()
			})
		}
		this.pipes.length = 0
		this.pipeCount = pipeCount
		this.finalHorizontalOffset = finalHorizontalOffset
		this.currentHorizontalOffset = horizontalOffset
		this.startingHorizontalOffset = horizontalOffset
		this.verticalOffset = verticalOffset
		this.startingX = startingX
		this.initialX = startingX
		this.velocityX = velocityX
		this.currentState = States.IDLE
		this.difficulty = difficulty
		
		this.checkRecycleDelayStart = 0
		this.checkRecycleDelay = 100
		
		// this will calculate the number of pipes to draw based on the screen width and the horizontal offset. It adds
		// 2 because it needs one extra to fit the width but also another one as a sort of padding in case the next pipe
		// doesn't recycle exactly on time.
		this.pipeCount = Math.ceil(this.scene.game.canvas.width / (finalHorizontalOffset + 80)) + 2
		if (this.anims) {
			this.anims.map(item => {
				item.destroy()
			})
		}
		this.anims = new Array(pipeCount)
		if (this.difficulty === Difficulty.EASY) {
			this.horizontalDecrementAmount = 5
			this.movingPipeChanceArray = [1, 1, 2]
		} else if (this.difficulty === Difficulty.MEDIUM) {
			this.horizontalDecrementAmount = 10
			this.movingPipeChanceArray = [1, 2]
		} else if (this.difficulty === Difficulty.HARD) {
			this.horizontalDecrementAmount = 15
			this.movingPipeChanceArray = [1, 2, 2]
		} else if (this.difficulty === Difficulty.INSANE) {
			this.horizontalDecrementAmount = 20
			this.movingPipeChanceArray = [2]
		}
		// if no starting value for x was given,
		// it will start the pipes at the far right side of the canvas
		if (!this.startingX) {
			this.startingX = this.scene.game.canvas.width
			this.initialX = this.startingX
		}
		this.preloadCreatePipes()
		this.createPipes()
	}
	
	/**
	 * 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.
	 */
	preload() {
		// preloading assets
		// green pipe
		this.scene.load.image('pipe', '/assets/pipe.png')
		this.scene.load.image('cap', '/assets/pipe-top.png')
		// red pipe
		this.scene.load.image('red-pipe', '/assets/pipe-red.png')
		this.scene.load.image('red-cap', '/assets/pipe-top-red.png')
		// light-green pipe
		this.scene.load.image('light-green-pipe', '/assets/pipe-light-green.png')
		this.scene.load.image('light-green-cap', '/assets/pipe-top-light-green.png')
		// orange pipe
		this.scene.load.image('orange-pipe', '/assets/pipe-orange.png')
		this.scene.load.image('orange-cap', '/assets/pipe-top-orange.png')
		// purple pipe
		this.scene.load.image('purple-pipe', '/assets/pipe-purple.png')
		this.scene.load.image('purple-cap', '/assets/pipe-top-purple.png')
		
		// if no starting value for x was given,
		// it will start the pipes at the far right side of the canvas
		if (!this.startingX) {
			this.startingX = this.scene.game.canvas.width
			this.initialX = this.startingX
		}
		this.preloadCreatePipes()
	}
	
	preloadCreatePipes() {
		// creating PipeGroup objects and adding them to an array that will be used farther down in the code.
		// it's also preloading each PipeGroup for anything that needs done before drawing to canvas.
		for (let i = 0; i < this.pipeCount; i++) {
			const pipe = new PipeGroup(this.scene, {x: this.startingX, y: this.scene.game.canvas.height / 2, offset: this.verticalOffset, difficulty: this.difficulty})
			pipe.preload()
			this.pipes.push(pipe)
		}
	}
	/**
	 * Originally inside the setupPipePositions(pipe, index) method, however, this could be used
	 * in other places. This method will get the position of the last element in the array if the
	 * index isn't 0. It will use the previous pipe's position plus the horizontal offset as a new x position.
	 * If it is the first index, being 0, then it will just set the position to whatever
	 * the class starting position variable holds. Then it gets a new y position and returns both the x and y
	 * variables inside an object.
	 * @param index - the index of the current pipe to reposition
	 * @returns {{x: *, y: number}} - new x and y positions to use for a pipe
	 */
	getNewPipePosition(pipe, index) {
		// generating new pipe positions x and y
		if (this.currentHorizontalOffset > this.finalHorizontalOffset)
			this.currentHorizontalOffset -= this.horizontalDecrementAmount
		if (this.currentHorizontalOffset < this.finalHorizontalOffset)
			this.currentHorizontalOffset = this.finalHorizontalOffset
		const newX = index===0?this.startingX:this.pipes[index-1].getPosition().x + this.currentHorizontalOffset
		const newY = this.getRandomY()
		return {x: newX, y: newY}
	}
	
	/**
	 * This method dynamically generates positions for each pipe while
	 * taking in account for the horizontal offset given from the constructor
	 * @param pipe - the pipe that is being positioned
	 * @param index - the index of that pipe from an array of pipes
	 */
	setupPipePositions(pipe, index) {
		// resetting the variable gotCheckpoint
		pipe.gotCheckpoint = false
		this.handleMovingPipeChance(pipe)
		const position = this.getNewPipePosition(pipe, index)
		
		// setting those positions while taking into account the starting x position
		pipe.setPosition(position.x, position.y)
	}
	
	/**
	 * This method is called once and is used to create each PipeGroup gameObject
	 * and sets up each pipe's position
	 */
	create() {
		this.createPipes()
		// this.pipes.map((pipe, index) => {
		// 	// calling thee create method from the PipeGroup object
		// 	pipe.create()
		//
		// 	// setting the positions for the pipe
		// 	this.setupPipePositions(pipe, index)
		// })
	}
	
	createPipes() {
		for (let i = 0; i < this.pipes.length; i++) {
			const pipe = this.pipes[i]
			pipe.create()
			this.setupPipePositions(pipe, i)
		}
	}
	
	/**
	 * 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) {
		if (this.currentState === States.RUNNING) {
			/**
			 * This is the previous implementation of the difficulty curve and it
			 * updated the horizontal offset every number of seconds. the problem was
			 * that the user wouldn't see the new offset until after the 12 PipeGroup objects had first
			 * been recycled which created a weird problem where the offset was getting smaller after each
			 * section of 12 pipes which isn't the way I wanted it designed
			 */
			// if (time - this.changeOffsetDelayStart >= this.changeOffsetDelay && this.currentHorizontalOffset > this.finalHorizontalOffset) {
			// 	this.currentHorizontalOffset -= 1
			// 	if (this.currentHorizontalOffset < this.finalHorizontalOffset) {
			// 		this.currentHorizontalOffset = this.finalHorizontalOffset
			// 	}
			// 	this.changeOffsetDelayStart = time
			// }
			if (time - this.checkRecycleDelayStart > this.checkRecycleDelay) {
				this.checkRecycleDelayStart = time
				this.handlePipePool()
			}
		}
	}
	
	/**
	 * This method checks each of the pipe's position inside the pipes array,
	 * and if the pipe is outside of the canvas, it will reset the pipe back to the
	 * other end of all the other pipes.
	 */
	handlePipePool() {
		// mapping through each PipeGroup object
		for (let i = 0; i < this.pipes.length; i++) {
			const pipe = this.pipes[i]
			// current pipe's position
			const pos = pipe.getPosition()

			// current pipe's width
			const width = pipe.getWidth()

			// if the pipe is beyond the canvas on the left side,
			// the pipe will be repositioned at the end of all the other pipes
			if (pos.x + width / 2 <= 0) {
				this.handleResetPipe(pipe, i)
			}
		}
		// this.pipes.map((pipe, index) => {
		// 	// current pipe's position
		// 	const pos = pipe.getPosition()
		//
		// 	// current pipe's width
		// 	const width = pipe.getWidth()
		//
		// 	// if the pipe is beyond the canvas on the left side,
		// 	// the pipe will be repositioned at the end of all the other pipes
		// 	if (pipe.topPipe.x <= 0) {
		// 	console.log(pipe.topPipe.x)
		// 		this.handleResetPipe(pipe, index)
		// 	}
		// })
	}
	
	/**
	 * This method will take in a PipeGroup object and since this pipe is beyond the canvas,
	 * then we know that it is the first PipeGroup object inside the array. Therefore it will
	 * remove the first element from the PipeGroup array, and appends it to the end. It also
	 * positions the pipe to a new position relative to the previous last element and the
	 * horizontal offset
	 */
	handleResetPipe() {
		// removes the first element from the PipeGroup array
		const pipe = this.pipes.shift()
		// sets the new position for the pipe taking into account the horizontal axis
		this.setupPipePositions(pipe, this.pipes.length)
		// appending the pipe to the end of the PipeGroup array
		this.pipes.push(pipe)
	}
	
	handleMovingPipeChance(pipe) {
		// if the pipe is not being tweened, it will create one which adds more difficulty to the game
		if (Phaser.Math.RND.weightedPick(this.movingPipeChanceArray)==2) {
			pipe.startVerticalMovement()
		}
	}
	
	/**
	 * This function generates a random number on the y-axis by offsetting the middle of the y-axis for
	 * min and max numbers that the random generator uses
	 * @returns {number} - new y position randomly generated to position a pipe on the vertical axis
	 */
	getRandomY() {
		// center of canvas on the y-axis
		const mid = this.scene.game.canvas.height / 2
		// random number generated from min and max. The total area is 351px that a pipe can be positioned on the y-axis
		return Phaser.Math.RND.integerInRange(mid - 200, mid + 150)
	}
	
	/**
	 * This method will change and update the distance between each pipe with the
	 * given newOffset value. This can be used to update the offset mid-game which could
	 * add more complexity to the game
	 * - very cpu intensive and should not be used inside an update method
	 * @param newOffset - the space between each pipe
	 * @deprecated
	 */
	setHorizontalOffset(newOffset) {
		this.currentHorizontalOffset = newOffset
		this.startingX = this.pipes[0].topPipe.x - this.pipes[0].getWidth() / 2
		this.pipes.map((pipe, index) => {
			const pos = this.getNewPipePosition(pipe, index)
			pipe.setPosition(pos.x, pipe.position.y)
		})
	}
	
	/**
	 * This method is used for resetting the pipe's positions and anything else that may have
	 * been altered during the gameplay
	 */
	resetG() {
		// resetting the currentState
		super.reset()
		// resetting the offset
		this.currentHorizontalOffset = this.startingHorizontalOffset
		// resetting startingX
		this.startingX = this.initialX
		// mapping through the PipeGroup array
		this.pipes.map((pipe, index) => {
			pipe.reset()
			// resetting pipes to starting positions
			this.setupPipePositions(pipe, index)
		})
	}
	
	/**
	 * Setter for the velocityX variable
	 * @param velocity - how fast the pipes are moving
	 */
	setVelocityX(velocity) {
		this.velocityX = velocity
		this.pipes.map(pipe => {
			if (pipe.pipeGroup)
				pipe.setVelocityX(velocity)
		})
	}
	
	/**
	 * Starts moving the pipes and updates the currentState to RUNNING
	 * allowing for pipe pooling and one of the main components to start the gameplay
	 */
	startG() {
		super.start()
		this.pipes.map(pipe => {
			pipe.setVelocityX(this.velocityX)
		})
	}
	
	/**
	 * Stops the pipes at their current positions and disables their horizontal velocity.
	 * Also changes the currentState to IDLE
	 */
	stopG() {
		if (this.currentState === States.RUNNING) {
			super.stop()
			this.pipes.map(pipe => {
				pipe.setVelocityX(0)
			})
		}
	}
}

export default PipeManager