Source: project.js

/** 
	@namespace Orbital-Explorer
	@since 25/10/19 
	@version 1.0
	@author Brennan Wilkes 
*/

/*
	JSDOCS generation command
	jsdoc source/project.js -d documentation/
*/

// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------- GLOBAL VARIABLE DEFINITIONS ---------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

/**
	Canvas object
	@type {object}
	@global
	@constant
*/
const canvas = document.getElementById("world");

/**
	2d context of the canvas
	@type {object}
	@global 
	@constant
*/
const ctx = canvas.getContext("2d");

/** 
	ID of mainloop interval 	
	@type {number}
	@global
*/
let interval_id;

/**
	Reference to the current {@link Stage}. For multiple "levels" within a single load of the program, this variable will change to a new {@link Stage} object. This was supposed to be implimented kinda how I would in C using {@link Stage} Object pointers. i.e. Stage* current_stage -> draw();
	@global
	@type {object}
	@summary Reference to the currently loaded {@link Stage}
*/
let current_stage;

//Screen size constants

/** 
	Maximum x coordinate of the canvas 
	@type {number}
	@global 
	@constant
*/
const MAX_X = canvas.width;

/** 
	Maximum y coordinate of the canvas 
	@type {number}
	@global 
	@constant
*/
const MAX_Y = canvas.height;

/** 
	Minimum x coordinate of the canvas 
	@type {number}
	@global 
	@constant
*/
const MIN_X = 0;

/** 
	Minimum y coordinate of the canvas 
	@type {number}
	@global 
	@constant
*/
const MIN_Y = 0;

/** 
	Time tracking variable for framerate display and optimization
	@type {number}
	@global 
	@todo fully impliment
*/
let last_tick_time = new Date().getTime();

/** 
	Time tracking variable for framerate display and optimization
	@type {number}
	@global 
	@todo fully impliment
*/
let total_tick_time = 0;


/** 
	Linear physics calculation depth is the amount of iterations used to predict the future position of the player. Greater numbers will mean the player position will be predicted and displayed further into the "future", but will add increased strain to the cpu
	@summary Depth of physics calculations
	@type {number}
	@global 
*/
let rails_depth = 5000;

/**
	Map of the current state of keys. i.e. whether they are pressed or not. This is to solve the problem of annoying key press latency.
	@type {number}
	@global
*/
let keyPressMap;

/**
	Paused game flag. 1:paused -1:unpaused
	@type {number}
	@global
*/
let pause = -1;

/**
	Auto otimization flag. 1:ON -1:OFF
	@type {number}
	@global
*/
let auto_optimize = -1;

/**
	Total ticks that have been called. Will cause undefined behaviour if the game is left running for longer than about 200 days. (i.e MAX_SAFE_INT is reached and {@link tickCount} overflows.)
	@type {number}
	@global
*/
let tickCount = 0;

/**
	Map to keep track of dynamic keybinds. Links actual keys with "codes" to determine said key's behaviour. i.e. ArrowLeft will by default map to rotate_left which will cause left rotation behaviour.
	@summary Map to key track of dynamic keybinds
	@global
	@type {Array}
*/
let controls;

/**
	Variable to detect key bind changes. Will change to non null and store the information needed for the next key bind change.
	@global
	@type {object}
*/
let updateNextControl;

/**
	ID of current screen. 0:gameplay 1:Win 2:Lose 3:Start 4:Load 5:Highscores
	@global
	@type {number}
*/
let current_screen = 3;

/**
	Mode of camera - 1:2d -1:3d
	@global
	@type {number}
*/
let camera_mode = 1;

/**
	Array for storing highschool entry initials
	@global
	@type {array}
*/
let high_score_initials = new Array(3);

/**
	Index of selected option in menu
	@global
	@type {number}
*/
let menu_selection = 0;

/**
	Index of selected stage or -1 if random
	@global
	@type {number}
*/
let current_stage_ID;

/**
	Menu options
	@global
	@type {array}
*/
const START_MENU_OPTIONS = ["RANDOM STAGE","LOAD STAGE","VIEW HIGHSCORES"];

/**
	Map of keypress behaviour functions
	@type {array}
	@global 
	@constant
*/
const MENU_KEYPRESS_MAP = [	key_press_game,key_press_win,
							key_press_lose,
							key_press_start_menu,
							key_press_load_menu,
							key_press_highscore_menu,
							function(){}					//Error prevention for users who decide to mash the keyboard on the loading screen
						  ];

/**
	Map of tick behaviour functions
	@type {array}
	@global 
	@constant
*/
const TICK_MAP = [			tick_gameplay,
							tick_win_menu,
							tick_lose_menu,
							function(){menu(START_MENU_OPTIONS,"ORBITAL EXPLORER");},
							function(){menu(STAGES_DISPLAY_OPTIONS,"LOAD STAGE");},
							function(){menu(STAGES_DISPLAY_OPTIONS,"HIGH SCORES");},
							loading_screen_display
			 	 ];

/**
	Stages selection menu options
	@global
	@type {array}
*/
const STAGES_DISPLAY_OPTIONS = new Array(SAVED_STAGES.length+1);

/**
	Colour of start menu text
	@global
	@type {string}
*/
const MENU_COLOUR = ran_colour();

/**
	Gravitational constant
	@constant
	@global
	@type {Number}
*/
const G = 6.674;

/**
 {@link player} acceleration constant
 @constant
 @global
 @type {Number}
*/
const ROCKET_SPEED = 0.001;

/**
	{@link player} camera shake constant
	@constant
	@global
	@type {Number}
*/
const CAMERA_SHAKE = 5;

/**
	Sound manager object. soundManger["soundname"] will return an HTML Audio object
	@type {array}
	@constant
	@global
*/
const soundManager = new Array();

// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------- CLASS HIERARCHY ---------------------------------------------------------------------------------------------
// -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


	


/** 
	class to repersent a game stage/level
	@class
*/
class Stage{
	
	/**
	 Creates an instance of Stage.
	 @constructor
	 @param {number} md Type of constructor to use 0:Random generated {@link Stage}
	 @param {number} stage_id ID of stage to load
	*/
	constructor(md, stage_id){
		
		//randomly generate a new stage
		if(md===0){
			this.generate()
			current_stage_ID = -1;		//indicates a random stage
		}
		
		//load a stage
		else if(md === 1){
			this.load(stage_id);
			current_stage_ID = stage_id;
		}
		
		/** 
			Amount of points available 
			@type {number}
		*/
		this.score = 1000;
		
		/** 
			Completed flag 
			@type {boolean}
		*/
		this.complete = false;
		
		/**
			Array of {@link StarObject} to render in 3d mode
			@type {array}
		*/
		this.skybox = this.generate_skybox(200,true);
		
		/**
			Array of {@link StarObject} to render in 2d mode
			@type {array}
		*/
		this.skymat = this.generate_skybox(50,false);
	}
	
	/**
		Generates a new skybox
		@param num {number} Number of stars to create
		@param project {boolean} If stars should be projected to the boundries
		@returns {array} skybox array
	*/
	generate_skybox(num,project){
		let sky = new Array(num);
		for(let j=0;j<sky.length;j++){
			sky[j] = new StarObject();
			
			//projection is for 3d mode stars
			if(project){
				sky[j].project();
			}
		}
		return sky;
	}
	
	/**
		Loads a stage from file
		@param stage_id ID of stage to load
	*/
	load(stage_id){
	
		//load initial player position and velocity
		this.player_x = SAVED_STAGES[stage_id][0][1];
		this.player_y = SAVED_STAGES[stage_id][0][2];
		this.player_xs = SAVED_STAGES[stage_id][0][3];
		this.player_ys = SAVED_STAGES[stage_id][0][4];
		
		//create planet objects
		this.planets = new Array(SAVED_STAGES[stage_id].length-1);
		for(let i=1;i<SAVED_STAGES[stage_id].length;i++){
			this.planets[i-1] = new Planet(this.planets,SAVED_STAGES[stage_id][i][0],SAVED_STAGES[stage_id][i][1],SAVED_STAGES[stage_id][i][2],SAVED_STAGES[stage_id][i][3]);	
		}	
		
		//assign target planet
		this.planets[0].target = true;
	}
	
	/** Resets stage to defaults*/
	reload(){
		pause = 1;
		reset_keypress();
		
		this.score = 1000;
		this.complete = false;
		this.planets["player"].landed = false;
		this.planets["player"].x = this.player_x;
		this.planets["player"].y = this.player_y;
		this.planets["player"].x_speed = this.player_xs;
		this.planets["player"].y_speed = this.player_ys;
		
		//Redraw physics predictions
		this.planets["player"].calc_rails(rails_depth);
	}
	
	/** 
		Generates a new, random, valid and stable stage
	 	@todo add procedurals
	*/
	generate(){
		
		//Number of planets to be created
		let rand = ran_b(2,10);
		
		/** 
			Main array of {@link GameObject} storage. Size is number of {@link Planet} plus an addition slot for the {@link player}	
			@type {Array}
			@todo Add additional slots for procedurals
		*/
		this.planets = new Array(0);//rand+1);
		
		//Populate this.playets with planet objects
		let temp;	
		for(let i=0;i<rand;i++){
			
			//create randomized planet object
			temp = new Planet(this.planets,ran_b(MIN_X+50,MAX_X-50), ran_b(MIN_Y+50,MAX_Y-50), ran_colour(), ran_b(10,20));			
			
			//validate planet placement
			if(calc_collision(temp,this.planets)){
				i--;
			}
			else{
				this.planets.push(temp);
			}
		}
		
		//Set one of the planets as the stage target
		this.planets[0].target = true;
		
		//Positional shift values and their respective velocity vectors
		const pos = [[0,-1],[-1,0],[0,1],[1,0]];
		const spd = [[-1,0],[0,-1],[1,0],[0,1]];
		
		//valid starting orientation flags
		let valid;
		let found = false;
		
		//Temporary storage for physics calculations
		let grav;

		//Dummy object to elegently store coordinate and speed values
		let temp_dummy = new DummyObject(0,0);
		
		/*
			Main validation loop. Will loop over every planet, and place the initial
			player location at each relative coordinate in respect to said planet,
			and set the relative velocity accordingly. Then it will calculate ahead
			[rails_depth] into the future, and if the player has not collided with 
			any objects, and has remained roughly within the screen bounds, the object
			distribution and initial player position will be marked as valid, and will be
			used. Complexity is O(4*p*d), ex. depth = 10000, planets = 10, validation
			will take up to O(400000). This means generating a valid stage may take a 
			few seconds, depending on hardware.
		*/
		for(let i=1; i < rand && (!found) ;i++){
			for(let j=0;j<4;j++){
				valid = true;
				
				//Set initial position
				temp_dummy.x = this.planets[i].x+(pos[j][0]*this.planets[i].mass*3);
				temp_dummy.y = this.planets[i].y+(pos[j][1]*this.planets[i].mass*3);
				
				//Set initial speed
				temp_dummy.xs = spd[j][0]*this.planets[i].mass/5;
				temp_dummy.ys = spd[j][1]*this.planets[i].mass/5;
				
				//Calculate physics translations
				for(let k=0;k<rails_depth/2;k++){
				
					//calculate current tick of gravity
					grav = calc_grav(temp_dummy,this.planets);
					
					//Apply translations
					temp_dummy.xs += grav[0];
					temp_dummy.ys += grav[1];
					temp_dummy.x += temp_dummy.xs;
					temp_dummy.y += temp_dummy.ys;
					
					//Validate current tick
					if(calc_collision(temp_dummy,this.planets,10) || !inBounds(temp_dummy.x,temp_dummy.y,MAX_X*-0.1,MAX_X*1.1,MAX_Y*-0.1,MAX_Y*1.1)){
						valid = false;
						break;
					}	
				}
				
				//Valid stage found
				if(valid && inBounds(temp_dummy.x,temp_dummy.y,MIN_X,MAX_X,MIN_Y,MAX_Y)){
					found = true;
					
					//Set inital player location
					this.player_x = this.planets[i].x+(pos[j][0]*this.planets[i].mass*3);
					this.player_y = this.planets[i].y+(pos[j][1]*this.planets[i].mass*3);
					
					//Set itinial player velcocity vector
					this.player_xs = spd[j][0]*this.planets[i].mass/5;
					this.player_ys = spd[j][1]*this.planets[i].mass/5;
					
					break;
				}
			}
		}
		
		//Valid stage was not found, generate a new one
		if(!found){
			this.generate();
		}
	}
}

/**
  @class
  Top level object class. Just contains an x and y value. This is used for passing x y values into functions without the ability to overload.
*/
class DummyObject{
	/**
		@constructor 
		@param {number} x x coordinate
		@param {number} y y coordinate		
	*/
	constructor(x,y){
		/** 
			x coordinate 
			@type {Number}
		*/
		this.x = x;
		
		/** 
			y coordinate 
			@type {Number}
		*/
		this.y = y;
	}
}

/**
	@class Skybox star class. Is a {@link DummyObject} with a z coordinate and an intensity or "brightness" scaler
	@extends DummyObject
*/
class StarObject extends DummyObject{
	/**
		@constructor 
	*/
	constructor(){
		super(ran_b(0,MAX_X),ran_b(0,MAX_X));
		
		/**
			Y coordinate to be drawn on screen. Z coordinate in 3d space
			@type {number}
		*/	
		this.z = ran_b(0,MAX_Y);
		
		/**
			Intensity to be drawn at.
			@type {number}
		*/
		this.intensity = Math.random();
	}
	
	/**
		Projects closest x or y coordinate to the boundry. This is used to create the effect of a "box" of stars, where all {@link StarObject} have one coordinate on the min or max plane. In this implementation, the x/y coordinate closest to the plane is set to the plane itself. However as the stars are actually rendered as a cylinder, this does create a bug where stars are more likely to be clustered around the "corners" of the skybox, however I don't deem this an issue worth working on, as clusters of stars actually create a better looking texture than a perfectly random distribution.
		@summary Projects closest x or y coordinate to the boundry
	*/
	project(){
		if(Math.min(this.x,MAX_X-this.x) < Math.min(this.y,MAX_X-this.y)){
			if(this.x < MAX_X/2){
				this.x = MIN_X;
			}
			else{
				this.x = MAX_X;
			}
		}
		else{
			if(this.y < MAX_X/2){
				this.y = MIN_X;
			}
			else{
				this.y = MAX_X;
			}
		}
		
		//shifts the z coordinate to be closer to the middle of the screen. This is an optimization because in 3d camera mode, the top and bottom of the screen are covered by the IVA texture.
		this.z = ran_b(Math.floor(MAX_Y*0.2),Math.floor(MAX_Y*0.8));
	}
	
	/**
		Draws the star at x
		@param x {number} x coordinate for rendering
	*/
	draw(x){
		draw_shape("arc",x,this.z,"rgba(255,255,255, "+this.intensity+")",2,0,0,0,Math.PI*2);
	}
}

/**
	@class Parent class of all interactable objects such as {@link Planet} and {@link Rocket}.
	@extends DummyObject
*/
class GameObject extends DummyObject{
	/**
	 	@constructor
		@param {object} storage Array of {@link GameObject} to store itself in
		@param {number} x x coordinate
		@param {number} y y coordinate
		@param {string} type Descriptor of object. i.e. "planet" or "rocket"
	*/
	 constructor(storage,x,y,type){
		super(x,y);
		
		/**
			Descriptor of object i.e. "planet" or "rocket"
			@type {string}
		*/
		this.type = type;
	}
}

/**
	@class Class for controlable {@link GameObject}
	@extends GameObject
*/
class Rocket extends GameObject{
	/**
	 	@constructor
		@param {array} storage Array of {@link GameObject} to store itself in
		@param {number} x x coordinate
		@param {number} y y coordinate
		@param {number} xs x component of velocity vector
		@param {number} yx y component of velocity vector
	*/
	constructor(storage,x,y,xs,ys){
		super(storage,x,y,"rocket");
		
		/**
			Direction to point in; Used both for physics and rendering
			@type {number}
		*/
		this.dir = 0;
		
		/** 
			x component of velocity vector
			@type {number}
		*/
		this.x_speed = xs;
		
		/**
			y component of velocity vector
			@type {number}
		*/
		this.y_speed = ys;
		
		/**
			Array of all coordinate points of physics predictions FORMAT: [x,y,x_speed,y_speed]
			@type {array}
		*/
		this.rails = new Array(rails_depth);
		
		/** 
			Workaround for passing coordinates into non overloaded functions.
			This is used for calculating physics and should eventually
			become deprecated. Fingers crossed
		*/
		this.dummy = new DummyObject(0,0);
		
		/**
			Landed flag
			@type {boolean}
		*/
		this.landed = false;

		/**
			Path of the rocket texture image file. This file has been sitting around my computer for YEARS, no idea where I got it from.
			@type {string}
		*/
		this.texture_path = "../assets/ship.png";

		/**
			Canvas image object to be drawn
			@type {object}
		*/
		this.texture = new Image();
		this.texture.src = this.texture_path;

		/**
			Path of the rocket IVA image file. Taken from www.dreamstime.com/
			@type {string}
		*/
		this.iva_path = "../assets/ship_iva.png";

		/**
			Canvas image object to be drawn
			@type {object}
		*/
		this.iva = new Image();
		this.iva.src = this.iva_path;

		/**
			Size of the rocket hitbox, and scale factor of texture
			@type {number}
		*/
		this.size = 25;

		/**
			Storage for rocket exhaust trail
			@type {array}
		*/
		this.trails = new Array(this.size);

		/**
			Colour gradient used for rocket trails
			@type {object}
		*/
		this.trails_gradient = ctx.createLinearGradient(0,0,0,this.size,this.size,this.size);
		this.trails_gradient.addColorStop(0, "orange");
		this.trails_gradient.addColorStop(0.5, "red");
	}
	
	/**	Draws the full rocket sprite to the canvas	*/
	draw(){
	
		//calculate and render fiery trails by drawing orange/red circles with descending radii.
		for(let i=0;i<this.trails.length-1&&this.trails[i]!=undefined;i++){
			//render circle
			draw_shape("arc",this.x+(Math.sin(Math.PI-this.dir)*i*3),this.y+(Math.cos(Math.PI-this.dir)*i*3),this.trails_gradient,(this.trails.length-i)/3,this.dir,0,0,Math.PI*2);
			
			//update trails array
			if(this.trails[i]===0){
				this.trails[i] = undefined;
			}
			else{
				this.trails[i+1] = this.trails[i]-1;
			}
		}
		
		//update trails array
		if(this.trails[0]!=undefined){
			this.trails[0]--;
		}
		
		//draw rocket texture
		draw_shape("texture",this.x,this.y,this.texture,this.size,this.dir,1);
	}
	
	/**
		Accelerates the {@link Rocket} by the {@link ROCKET_SPEED} in the current {@link Rocket#dir} and decrements {@link Stage#score}
	*/
	thrust(){
	
		//if in 3rd person camera mode, start a new rocket trail
		if(camera_mode === 1){
			this.trails[0] = this.trails.length;
		}
		
		//decrement score
		current_stage.score--;
		
		//apply acceleration
		this.accelerate(vector(ROCKET_SPEED,this.dir));
	}
	
	/**
		Accelerates the {@link Rocket} by the passed velocity vector
		@param velocity {array} velocity vector to be applied
	*/
	accelerate(velocity){
		this.x_speed = this.x_speed + velocity[0];
		this.y_speed = this.y_speed + velocity[1];
	}
	
	/**
		Rotates the {@link Rocket} in the passed direction
		@param direction {number} rotational direction
	*/
	rotate(direction){
		this.dir+=Math.PI/120*direction;
	}
	
	/**
		Applies physics and updates positional attributes
	*/
	tick(){
		if(!this.landed){
			this.accelerate(calc_grav(this,current_stage.planets));
			this.calc_rails(0);
			
			this.x = this.x + this.x_speed;
			this.y = this.y + this.y_speed;
		}
	}
	/**
		Calculates and generates the {@link Rocket#rails} attribute
		@param iter {number} Was formerly a recursive function. Now specifies mode
		@todo good lord clean this function up. It. Is. A. Mess.
	*/
	calc_rails(iter){
		let prev;
		let index;
		let grav;
		
		//singular new rail calc i.e. new tick
		if(iter==0){
			left_shift(this.rails);
			prev = this.rails[this.rails.length-1];
			index = this.rails.length-1;
			
			//This method of passing data has to be changed
			this.dummy.x=prev[0];
			this.dummy.y=prev[1];
			
			//calculate physics
			grav = calc_grav(this.dummy,current_stage.planets)
			
			//apply physics
			this.rails[index] = [prev[0]+prev[2]+grav[0],prev[1]+prev[3]+grav[1],prev[2]+grav[0],prev[3]+grav[1]]; // [x,y,x_speed,y_speed]
		}
		else{
		
			//This was formerly recursive but I was hitting the stack limit :(
			while(true){
				index = rails_depth-iter;
				
				//Either physics depth has been reached or a collision has happened, so the array is padded with null terminators (false values right now)
				if(iter<0){
					index = index + (iter*2);
					
					//this should be changed with a null terminator @todo
					this.rails[index] = false;
					iter++;
					if(iter<0){
						continue;
					}
					break;
				}
				else if(iter===rails_depth){	//first of recursive
					prev = [this.x,this.y,this.x_speed,this.y_speed];
				}
				else{							//normal recursive
					prev = this.rails[rails_depth-iter-1];
				}
				this.dummy.x=prev[0];
				this.dummy.y=prev[1];
				
				//calculate physics
				grav = calc_grav(this.dummy,current_stage.planets)
				
				//apply physics
				this.rails[index] = [prev[0]+prev[2]+grav[0],prev[1]+prev[3]+grav[1],prev[2]+grav[0],prev[3]+grav[1]]; // [x,y,x_speed,y_speed]
		
				iter--;
				if(iter===0){
					break;
				}
				
				//switch to collision mode ie pad out array with null terminators
				if(calc_collision(this.dummy,current_stage.planets)){
					iter = iter * -1;
				}	
			}
		}
	}
}

/**
	@class Class of all planet objects
	@extends GameObject
	@todo move to separate radius/mass attribute system
*/
class Planet extends GameObject{
	
	/**
	 	@constructor
		@param {array} storage Array of {@link GameObject} to store itself in
		@param {number} x x coordinate
		@param {number} y y coordinate
		@param {string} colour Colour of planet
		@param {number} mass Mass of planet
	*/
	constructor(storage,x,y,colour,mass){
		super(storage,x,y,"planet");
		
		/**
			Mass AND radius of planet
			@type {number}
			@todo move to separate radius/mass attribute system
		*/
		this.mass = mass;
		
		/**
			Colour of planet
			@type {string}
		*/
		this.colour = colour;
		
		/**
			Transparent edge gradient of the colour
			@type {string}
		*/
		this.colour_gradient;
		
		/**
			Stage target landing flag
			@type {boolean}
		*/
		this.target = false;
	}
	
	/**
		Draws the planet to the canvas in map overview mode
	*/
	draw(){
		//render circle
		draw_shape("arc",this.x,this.y,this.colour,this.mass,0,0,0,Math.PI*2);
		
		//render golden outline
		if(this.target){
			draw_shape("arc",this.x,this.y,"yellow",this.mass*1.2,0,1,0,Math.PI*2);
		}
	}
	
	/**
		Draws the planet to the canvas in first person raytracing mode
	*/
	draw3d(x){
		//this formula is incorrect, but it works well enough for this project.
		let size = 500*this.mass/(get_distance(this,current_stage.planets["player"]));
		
		//create a gradient so that planets have smooth edges
		this.colour_gradient = ctx.createRadialGradient(0,0,0,0,0,size);
		this.colour_gradient.addColorStop(0, this.colour);
		this.colour_gradient.addColorStop(0.95, this.colour);
		this.colour_gradient.addColorStop(1, add_alpha(this.colour,0));
		
		//render planet
		draw_shape("arc",x,MAX_Y/2,this.colour_gradient,size,0,0,0,Math.PI*2);
		
		//render golden outline
		if(this.target){
			draw_shape("arc",x,MAX_Y/2,"yellow",size*1.1,0,1,0,Math.PI*2);
		}
	}
}

// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------- FUNCTIONS ---------------------------------------------------------------------------------------------
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

/**
 	Draws a shape to the canvas
	@param {string} shape Shape description
	@param {number} x x coordinate to draw at
	@param {number} y y coordinate to draw at
	@param {string} colour Shape colour
	@param {number} size size to scale shape to in radians
	@param {number} rotation rotation to draw shape at
	@param {number} md Outline flag 1:YES 0:NO
	@param {number} arc_start if a circle, radian to start drawing at
	@param {number} arc_length if a circle, radian to end drawing at
*/
function draw_shape(shape,x,y,colour,size,rotation,md,arc_start,arc_length){
	ctx.save();
	ctx.translate(x,y);
	ctx.rotate(rotation);
	ctx.beginPath();
	ctx.fillStyle = colour;
	ctx.strokeStyle = colour;
	
	if(shape=="tri"){
		draw_triangle(size);
	}
	else if(shape=="arc"){
		draw_arc(size,arc_start,arc_length)
	}
	else if(shape=="texture"){
		ctx.drawImage(colour,size/-2,size/-2,size,size);
	}
	if(md===0){
		ctx.fill();
	}
	else{
		ctx.stroke();
	}
	ctx.restore();
}

/**
 	Draws an arc to the canvas
	@param {number} size size to scale shape to in radians
	@param {number} arc_start Radian to start drawing at
	@param {number} arc_length Radian to end drawing at
*/
function draw_arc(size,arc_start,arc_length){
	ctx.arc(0,0, size, arc_start, arc_length);
}

/** 
	gets all of the projects sound names 
	Sounds were aquired from soundbible.com and soundcloud.com/squirrel-murphy/maeistrom-sounds
	@returns array of sound names
*/
function get_sounds(){
	return ["tick","thrust","win","crash","lose"];
}

/**
	Runs on page load. Initializes global variables. Sets intial physics calculation depth to the settings field. Initializes default keybinds. Adds event listeners.
	@todo Move to settings file system
	@todo Move to server side global highscores
*/
function setUp(){
	
	//initialize sounds
	let sounds=get_sounds();
	for(let i=0;i<sounds.length;i++){
		soundManager[sounds[i]] = new Audio("../assets/"+sounds[i]+".mp3");
	}
	soundManager["thrust"].loop = true;
	
	//pull highscore data from the browser cache
	if(typeof(Storage) !== "undefined" || typeof(Storage) !== undefined){
		
		//first time project load
		if(localStorage.getItem("init") === null) {
			localStorage.setItem("init","true");
			for(let i=0;i<HIGH_SCORES.length;i++){
				localStorage.setItem(i,String(HIGH_SCORES[i]));
			}
		}
		
		//get cached highscores
		else{
			let temp;
			for(let i=0;i<HIGH_SCORES.length;i++){
				HIGH_SCORES[i] = new Array(0);
				temp = localStorage.getItem(i).split(",");
				for(let j=0;j<temp.length;j+=2){
					HIGH_SCORES[i].push([temp[j],parseInt(temp[j+1])]);
				}
			}
		}
	}
	
	//sort highscores
	for(let i=0;i<HIGH_SCORES.length;i++){
		HIGH_SCORES[i].sort(high_score_sort);
	}
	
	//initialize initials
	for(let i=0;i<high_score_initials.length;i++){
		high_score_initials[i] = '_';
	}
	
	//generate stage display array
	for(let i=0;i<SAVED_STAGES.length;i++){
		STAGES_DISPLAY_OPTIONS[i] = "["+i+"] "+SAVED_STAGES[i][0][0];
	}
	STAGES_DISPLAY_OPTIONS[SAVED_STAGES.length] = "BACK";;
	
	//Set default value to settings text field
	set_val("rails_depth",rails_depth);
	
	//Initialize keybinds
	keyPressMap = new Array(0);
	updateKey(false);
	controls=new Array(0);
	controls["Thrust"] = "Space";
	controls["Rotate_Left"] = "ArrowLeft";
	controls["Rotate_Right"] = "ArrowRight";
	controls["Freeze_Time"] = "KeyF";
	controls["Reload"] = "KeyR";
	controls["Camera"] = "KeyC";
	controls["Menu"] = "Escape";
	updateNextControl = null;
	
	
	//Setup event listeners
	addEventListener("keydown",key_down);
	addEventListener("keyup",key_up);
	addEventListener("click",mouse_click);
	
	//set font
	ctx.font = "52px VT323";
	
	//I am using setTimeout() instead of setInterval() so that I can dynamically change the tickrate at runtime
	interval_id = setTimeout(tick,10);	
}

/**
	Calculates the current framerate
	@returns {number} Framerate
*/
function get_framerate(){
	return Math.floor((tickCount%100)/total_tick_time*1000);
}

/**
	Resets the {@link Rocket#rails} array
	@param amt new depth value
*/
function update_rails(amt){
	rails_depth = amt;
	current_stage.planets["player"].rails = new Array(rails_depth);
	current_stage.planets["player"].calc_rails(rails_depth);
	set_val("rails_depth",rails_depth);
}

/**
	Toggles optimizations
	@todo fully implement better optimizations. For now, this isn't really worth turning on
*/
function toggle_auto_rails(){
	auto_optimize = auto_optimize * -1;
	if(auto_optimize===1){
		set_HTML("auto_rails","Slow computer optimizations are: ON");
	}
	else{
		set_HTML("auto_rails","Slow computer optimizations are: OFF");
	}
}

/**	Runs tick behaviour for the gameplay screen */
function tick_gameplay(){
	
	//Key down detection
	if(keyPressMap["Thrust"]){
		current_stage.planets["player"].thrust();
		//recalculate rails
		if(camera_mode===1){
			current_stage.planets["player"].calc_rails(rails_depth);
		}
	}
	if(keyPressMap["Rotate_Left"]){
		current_stage.planets["player"].rotate(-1);
	}
	if(keyPressMap["Rotate_Right"]){
		current_stage.planets["player"].rotate(1);
	}

	if(pause===-1){
		if(!current_stage.planets["player"].landed){
			tickCount++;
			if(tickCount%100===0){
				current_stage.score--;
			}
		}

		//calculates player collisions
		current_stage.planets["player"].landed = (calc_collision(current_stage.planets["player"],current_stage.planets) || current_stage.planets["player"].landed);

		//invokes player subtick
		current_stage.planets["player"].tick();
		
		

		//win/loss detection and behaviour
		if(current_stage.planets["player"].landed&&(!current_stage.complete)){
			current_stage.complete = true;
			let win=false;
			for(let i=0;i<current_stage.planets.length;i++){
				
				//check that the player is landed at the target planet
				if(current_stage.planets[i].target){
					if(get_distance(current_stage.planets[i],current_stage.planets["player"])<=current_stage.planets[i].mass+current_stage.planets["player"].size){
						win=true;
					}
					break;
				}
			}
			reset_keypress();
			
			//keep score at a minimum of 0
			current_stage.score = Math.max(0,current_stage.score);

			if(win){
				soundManager["win"].play();
				current_screen = 1;
			}
			else{
				soundManager["crash"].play();
				setTimeout(function(){soundManager["lose"].play();},1000);
				current_screen = 2;
			}
		}
	}

	//Render the screen
	draw();

	//Update score text
	set_HTML("score",Math.max(current_stage.score,0));
}

/**	Runs tick behaviour for the win menu */
function tick_win_menu(){
	
	//draw a faded version of the final gameplay screen
	draw();
	clear_screen(0.8);
	
	//draw the highscore initial entering screen
	ctx.fillStyle = current_stage.planets[0].colour;
	ctx.textAlign = "center";
	ctx.fillText("SCORE: "+current_stage.score, MAX_X/2, 150);
	let entered = true;
	for(let i=0;i<high_score_initials.length;i++){
		if(high_score_initials[i]==='_'){
			entered = false;
		}
	}
	if(entered){
		ctx.fillText(high_score_initials[0]+high_score_initials[1]+high_score_initials[2]+" WON", MAX_X/2, 75);
		ctx.fillText("PRESS ANY KEY", MAX_X/2, 250);
		ctx.fillText("TO CONTINUE", MAX_X/2, 315);
	}
	else{
		ctx.fillText("YOU WON", MAX_X/2, 75);
		ctx.fillText("ENTER INITIALS: "+high_score_initials[0]+" "+high_score_initials[1]+" "+high_score_initials[2], MAX_X/2, 225);
	}
}

/**	Runs tick behaviour for the lose menu */
function tick_lose_menu(){
	
	//draw a faded version of the final gameplay screen
	draw();
	clear_screen(0.8);
	
	ctx.fillStyle = current_stage.planets[0].colour;
	ctx.textAlign = "center";
	ctx.fillText("YOU LOSE", MAX_X/2, 75);
	ctx.fillText("PRESS ANY KEY", MAX_X/2, 250);
	ctx.fillText("TO TRY AGAIN", MAX_X/2, 315);
}

/**	Draws the word loading to the screen	*/
function loading_screen_display(){
	clear_screen();
	ctx.fillStyle = MENU_COLOUR;
	ctx.textAlign = "center";
	ctx.fillText("LOADING", MAX_X/2, 150);
}

/**
	Main game tick function. Applies key inputs. Updates framerate display. Updates tickcounts. Runs subtick methods. Calculates collisions. Calculates endgame conditions. Draws the screen. Updates information displays
*/
function tick(){
	
	//check for highscore display screen
	if(current_screen >= 50){
		let h_score_arr = new Array(HIGH_SCORES[current_screen-50].length);		
		for(let i=0;i<HIGH_SCORES[current_screen-50].length;i++){
			h_score_arr[i] = HIGH_SCORES[current_screen-50][i][0]+" - "+HIGH_SCORES[current_screen-50][i][1];
		}
		menu_selection = -1;
		menu(h_score_arr,STAGES_DISPLAY_OPTIONS[current_screen-50]+" HIGHSCORES");
	}
	
	//run tick function for every other screen
	else{
		TICK_MAP[current_screen]();
	}
	
	//Framerate calculation and display
	if(tickCount%100===99){
		set_HTML("fps","Framerate: "+get_framerate()+" fps");
	
		//WIP optimization. Adjusts physics calculation depth
		if(auto_optimize === 1){
			if(get_framerate()<50){
				update_rails(Math.max(1,rails_depth-1000));
			}
			else if(get_framerate()>60){
				update_rails(rails_depth+1000);
			}
		}
		total_tick_time = 0;
	}
	
	//WIP Optimizations for running ticks slightly sooner if the program is lagging
	let tick_time = new Date().getTime()-last_tick_time;
	let speed = 10	//this is to be implemented at a later date when I have a better understanding of how js events work under the hood
	total_tick_time += tick_time;
	last_tick_time = new Date().getTime();
	
	//Call next game tick
	interval_id = setTimeout(tick,speed);
}

/**
	Clears the screen by redrawing the background image
	@param {number} alpha Alpha value for background
*/
function clear_screen(alpha){
	if(alpha===undefined){
		alpha = 1;
	}
	ctx.fillStyle = "rgba(0, 0, 0, "+alpha+")";
	ctx.fillRect(MIN_X,MIN_Y,MAX_X,MAX_Y);
}

/** Renders the screen */
function draw(){
	clear_screen();
	
	//2d map overview mode
	if(camera_mode===1){
		
		//render stars
		for(let s=0;s<current_stage.skymat.length;s++){
			current_stage.skymat[s].draw(current_stage.skymat[s].x);
		}
		
		//Draw the physics prediciton line
		if(!current_stage.planets["player"].landed){
			draw_path();
		}
	
		//Draw each object
		for(let obj=0;obj<current_stage.planets.length;obj++){
			current_stage.planets[obj].draw();
		}
		
		//draw the player
		current_stage.planets["player"].draw();
	}
	
	//3d first person camera mode
	else{
		let angle;
		let end_point = new DummyObject(0,0);
		
		//sort planets based on distance to player
		current_stage.planets.sort(sort_player_distance);
		
		//render stars
		if(auto_optimize === -1){
			let zero_point = new DummyObject(250,250);
			//render skybox
			for(let s=0;s<current_stage.skybox.length;s++){
				for(let i=-90;i<90;i++){
					end_point.x = Math.cos(Math.PI*i/180 + current_stage.planets["player"].dir+Math.PI/2)*MAX_X*2;
					end_point.y = Math.sin(Math.PI*i/180 + current_stage.planets["player"].dir+Math.PI/2)*MAX_Y*2;
				
					//checks if ray has collided with star using triangle projection
					if(Math.abs(
								(get_distance(end_point,current_stage.skybox[s])+get_distance(zero_point,current_stage.skybox[s]))
								- get_distance(zero_point,end_point)) 
								< 0.1){
						
						//Draw star
						current_stage.skybox[s].draw(MAX_X/2 + (i/45*MAX_X/2));
						break;
					}
				
				}
			}
		}
		
		//Render planets
		for(let p=0;p<current_stage.planets.length;p++){
			for(let i=-90;i<90;i++){
				end_point.x = current_stage.planets["player"].x + Math.cos(Math.PI*i/180 + current_stage.planets["player"].dir+Math.PI/2)*MAX_X;
				end_point.y = current_stage.planets["player"].y + Math.sin(Math.PI*i/180 + current_stage.planets["player"].dir+Math.PI/2)*MAX_Y;
				
				//checks if ray has collided with star using triangle projection
				if(Math.abs(
							(get_distance(end_point,current_stage.planets[p])+get_distance(current_stage.planets["player"],current_stage.planets[p]))
							- get_distance(current_stage.planets["player"],end_point)) 
							< 0.001*get_distance(current_stage.planets["player"],current_stage.planets[p])){
					
					//Draw planet
					current_stage.planets[p].draw3d(MAX_X/2 + (i/45*MAX_X/2));
					break;
				}
				
			}
		}
		
		//apply camera shake
		let shift_x = 0;
		let shift_y = 0;
		if(keyPressMap["Thrust"]){
			shift_x = ran_b(-1*CAMERA_SHAKE,CAMERA_SHAKE);
			shift_y = ran_b(-1*CAMERA_SHAKE,CAMERA_SHAKE);
		}
		
		//render IVA texture
		draw_shape("texture",MAX_X/2 + shift_x,MAX_Y/2 + shift_y,current_stage.planets["player"].iva,MAX_X+(CAMERA_SHAKE*2),0,1);
	}
	
	//pause screen white filter
	if(pause===1){
		ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
		ctx.fillRect(MIN_X,MIN_Y,MAX_X,MAX_Y);
	}
}

/**
	Gets the adjustable keybinds
	@returns {array} adjustable keybinds
*/
function get_possible_keys(){
	return ["Thrust","Rotate_Left","Rotate_Right","Camera","Menu"];
}

/**
	Updates a keybind
	@param {string} val new keybind
	@param {object} event event to pull keypress data from. If undefined, function is in force override mode
*/
function updateKey(val,event){
	let force = false;
	if(event===undefined){
		force = true;
	}
	for(let i=0;i<3;i++){
		if(force || event.code === controls[get_possible_keys()[i]]){
			keyPressMap[get_possible_keys()[i]] = val;
		}
	}
}

/**
	Detects key down events and updates the key tracker
	@param {object} event event to pull keypress data from
*/
function key_down(event){
	if(updateNextControl===null){
		updateKey(true,event);
	}
	if(current_screen === 0 && event.code === controls["Thrust"]){
		soundManager["thrust"].play();
	}
}

/**
	Runs keypress behaviour for gameplay screen
	@param {object} event keypress event object 
 */
function key_press_game(event){
	updateKey(false,event);
	if(event.keyCode === 13){
		update_settings()
	}
	else if(event.code === controls["Freeze_Time"]){
		if(pause===-1){
			current_stage.score -= 50;
		}
		pause = pause * -1;	
	}
	else if(event.code === controls["Reload"]){
		current_stage.reload();
	}
	else if(event.code === controls["Camera"]){
		camera_mode = camera_mode * -1;
		if(camera_mode === 1){
			current_stage.planets["player"].calc_rails(rails_depth);
		}
	}
}

/**
	Runs keypress behaviour for win screen
	@param {object} event keypress event object 
 */
function key_press_win(event){
	let initial = -1;
	for(let i=0;i<high_score_initials.length;i++){
		if(high_score_initials[i]==='_'){
			initial = i;
			break;
		}
	}
	
	//All three initials have been entered
	if(initial===-1){
		if(current_stage_ID > -1){
			
			//saves highscore
			if(HIGH_SCORES[current_stage_ID].length<6 || current_stage.score > HIGH_SCORES[current_stage_ID].length[HIGH_SCORES[current_stage_ID].length-1]){
				HIGH_SCORES[current_stage_ID].push([high_score_initials[0]+high_score_initials[1]+high_score_initials[2],current_stage.score]);
				HIGH_SCORES[current_stage_ID].sort(high_score_sort);
			
				//save highscore to browser cache
				if(typeof(Storage) !== "undefined" || typeof(Storage) !== undefined){
					localStorage.setItem(current_stage_ID,localStorage.getItem(current_stage_ID)+","+high_score_initials[0]+high_score_initials[1]+high_score_initials[2]+","+current_stage.score);
				}
			
			}
		}
		
		//reset game
		current_screen = 0;
		high_score_initials = ['_','_','_'];
		current_stage.reload();
	}
	
	//save a new initial
	else if(event.keyCode>=65 && event.keyCode<=90){
		if(initial!=-1){
			high_score_initials[initial] = event.key.toUpperCase();
			play_tick_sound();
		}
	}
}

/**
	Runs keypress behaviour for lose screen
	@param {object} event keypress event object 
 */
function key_press_lose(event){
	current_screen = 0;
	current_stage.reload();
}

/**
	Runs keypress behaviour for start menu
	@param {object} event keypress event object 
 */
function key_press_start_menu(event){
	if(event.keyCode === 13 || event.keyCode === 32){
		
		//random generation selected
		if(menu_selection === 0){
			current_screen = 6;
			
			//This delay was to allow for the rendering of the "LOADING" screen to happen before the stage begins generating and blocks screen refreshes
			setTimeout(function(){load_level(0);},250);		
		}
		
		//Load level screen selected
		else if(menu_selection === 1){
			current_screen = 4;
			menu_selection = 0;
		}
		
		//highscore screen selected
		else if(menu_selection === 2){
			current_screen = 5;
			menu_selection = 0;
		}
	}
	menu_selection = update_selection(event.keyCode,menu_selection,START_MENU_OPTIONS);
}

/**
	Runs keypress behaviour for load stage menu
	@param {object} event keypress event object 
 */
function key_press_load_menu(event){
	if(event.keyCode === 13 || event.keyCode === 32){
		
		//back
		if(menu_selection === STAGES_DISPLAY_OPTIONS.length-1){
			menu_selection = 0;
			current_screen = 3;
		}
		
		//loads a level
		else{
			load_level(1,menu_selection);
		}
	}
	menu_selection = update_selection(event.keyCode,menu_selection,STAGES_DISPLAY_OPTIONS);
}

/**
	Runs keypress behaviour for highscore viewing menu
	@param {object} event keypress event object 
 */
function key_press_highscore_menu(event){
	if(event.keyCode === 13 || event.keyCode === 32){
		
		//back
		if(menu_selection === HIGH_SCORES.length){
			menu_selection = 0;
			current_screen = 3;
			return;
		}
		
		//selected a highscore to view
		else{
			current_screen = 50+menu_selection;
			menu_selection = 0;
			return;
		}
	}
	menu_selection = update_selection(event.keyCode,menu_selection,STAGES_DISPLAY_OPTIONS);
}

/**
	Loads a new stage into the game
	@param {number} md stage creation mode 0:Random gen, 1: Load level
	@param {number} index if loading a level, index of level to load 
 */
function load_level(md,index){
	
	//initalize stage
	current_stage = new Stage(md,index);
	
	//load player
	current_stage.planets["player"] = new Rocket(current_stage.planets,current_stage.player_x,current_stage.player_y,current_stage.player_xs,current_stage.player_ys);

	//Initial physics calculation
	current_stage.planets["player"].calc_rails(rails_depth);
	
	reset_keypress();
	pause = 1;
	current_screen = 0;
}

/**
	Detects key up events and updates the key tracker
	@param {object} event event to pull keypress data from
*/
function key_up(event){
	
	//Finish keybind cycle. Apply new key to keybind map and unpause
	if(updateNextControl!=null){
		controls[updateNextControl] = event.code;
		set_HTML(updateNextControl,event.code);
		updateNextControl = null;
		pause = -1;
	}
	
	//Run regular keypress behaviour
	else{
		if(event.code === controls["Menu"]){
			if((current_screen === 3 || current_screen === 4 || current_screen === 5) && current_stage!=undefined && current_stage.planets != undefined){
				current_screen = 0;
			}
			else if(current_screen === 0){
				current_screen = 3;
			}
			menu_selection = 0;
			reset_keypress();
		}
		else if(event.code === controls["Thrust"]){
			soundManager["thrust"].pause();
			soundManager["thrust"].currentTime = 0;
		}
		
		//highscore menu - return to main menu
		if(current_screen >= 50){
			current_screen = 3;
			menu_selection = 0;
			play_tick_sound();
		}
		else{
			if(current_screen!=0 && (event.keyCode === 13 || event.keyCode === 32)){
				play_tick_sound();
			}
			MENU_KEYPRESS_MAP[current_screen](event);
		}
	}
}

/**
	Detects mouse click events and updates the rail depth settings
	@param {object} event event to pull mousepress data from
*/
function mouse_click(event){
	//If mouse click was outside of the physics depth field, update it 
	if(event.target!=get_element("rails_depth")){
		update_settings();
	}
	
}

/**
	Generates x/y values from a vector in magnitude/angle form
	@param {number} magnitude Magnitude value to apply to vector
	@param {number} angle angle to multiply magnitude by
	@returns {array} Vector in [x,y] form
*/
function vector(magnitude,angle){
	return [magnitude*Math.sin(angle*-1),magnitude*Math.cos(angle)];
}

/**
	Calculates x/y accleration to apply to an object
	@param {object} object to calculate physics on
	@param {array} physics_objs array of objects to calculate physics from
	@returns {number} acceleration to apply in [x,y] form
*/
function calc_grav(obj,physics_objs){
	let xacc = 0;
	let yacc = 0;
	
	let speed;
	let angle;
	
	for(let i=0;i<physics_objs.length;i++){
		
		//only apply physics from planet objects
		if(physics_objs[i].type!="planet"){
			continue;
		}
		speed = newtons_gravity(physics_objs[i].mass,get_distance(physics_objs[i],obj));
		angle = get_angle(physics_objs[i],obj)
		xacc = xacc + vector(speed,angle)[0];
		yacc = yacc + vector(speed,angle)[1];
	}
	
	return [xacc,yacc];
}

/**
	Calculates the distance between two objects
	@param {object} a Object one
	@param {object} b Object two
	@returns {number} distance between a and b
*/
function get_distance(a,b){
	return Math.sqrt(Math.pow(Math.abs(a.x-b.x),2)+Math.pow(Math.abs(a.y-b.y),2));
}

/**
	Calculates the distance between an object and the player. Used for sorting
	@param {object} a Object one
	@returns {number} distance between a and {@link player}
*/
function sort_player_distance(a,b){
	return get_distance(b,current_stage.planets["player"])-get_distance(a,current_stage.planets["player"]);
}

/**
	Calculates the angle between two objects
	@param {object} a Object one
	@param {object} b Object two
	@returns {number} angle between a and b
*/
function get_angle(a,b){
	return Math.PI/-2+Math.atan2(a.y-b.y,a.x-b.x);
}

/**
	Calculates the acceleration due to gravity applied to an object
	@param {number} m Mass of object two
	@param {number} d Distance between the two objects
	@returns {number} Acceleration due to gravity 
*/
function newtons_gravity(m,d){
	return G*m/Math.pow(d,2)
}

/**
	Renders the physics prediction line
	@todo Maybe store this function somewhere better
*/
function draw_path(){	
	ctx.beginPath();
	ctx.moveTo(current_stage.planets["player"].x,current_stage.planets["player"].y);
	ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
	
	//shift to account for faded end to the line
	let shift = 0;
	if(current_stage.planets["player"].rails.length>100){
		shift = 100;
	}
	
	//Main path
	for(let i=0;i<current_stage.planets["player"].rails.length-shift;i++){
		ctx.lineTo(current_stage.planets["player"].rails[i][0],current_stage.planets["player"].rails[i][1]);
	}
	ctx.stroke();
	
	
	//Transparent fade at the end of the path
	ctx.beginPath();
	for(let j=0;j<shift;j++){
		ctx.lineTo(current_stage.planets["player"].rails[current_stage.planets["player"].rails.length-shift+j][0],current_stage.planets["player"].rails[current_stage.planets["player"].rails.length-shift+j][1]);
		ctx.strokeStyle = "rgba(80, 80, 80, "+(0.5*(100-j)/100)+")";
		ctx.stroke();
	}
}

/**
	Left shifts elements in an array
	@param {array} arr array to shift
*/
function left_shift(arr){
	for(let i=0;i<arr.length-1;i++){
		arr[i]=arr[i+1];
	}
}

/**
	Calculates collisions between two objects
	@param {object} object to calculate collisions on
	@param {array} physics_objs array of objects to calculate collisions from
	@returns {boolean} collision happened
*/
function calc_collision(obj,physics_objs,force_shift){
	let shift = 0;
	if(force_shift!=undefined){
		shift = force_shift;
	}
	else if(obj.type==="planet"){
		shift = obj.mass;
	}
	else if(obj.type==="rocket"){
		shift = obj.size/2;
	}
	for(let i=0;i<physics_objs.length;i++){
		if(physics_objs[i].type!="planet"||physics_objs[i]===obj){
			continue;
		}
		if(get_distance(obj,physics_objs[i]) <= physics_objs[i].mass+shift){
			return true;
		}
	}
	return false;
}

/**
	Calculates if x,y is the bounding box formed by xmin,ymin xmax,ymax
	@param {number} xmin x coordinate of the top left corner of the bounding box
	@param {number} xmax x coordinate of the bottom right corner of the bounding box
	@param {number} ymin y coordinate of the top left corner of the bounding box
	@param {number} ymax y coordinate of the bottom right corner of the bounding box
	@returns {boolean} x,y is within xmin,ymin xmax,ymax
*/
function inBounds(x,y,xmin,xmax,ymin,ymax){
	 return (x>xmin && x<xmax && y>ymin && y<ymax);
}

/**
	Generates a random integer between min_b and max_b inclusively
	@param {number} min_b Minimum bound
	@param {number} max_b Maximum bound
	@returns {number} Random integer min_b <= num <= max_b
*/
function ran_b(min_b,max_b){
	return Math.floor( min_b + Math.random()*(max_b+1) );
}

/**
	Generates a random rgb colour
	@returns {string} Random colour
*/
function ran_colour(){
	return "rgb("+ran_b(50,255)+", "+ran_b(50,255)+", "+ran_b(50,255)+")";
}

/**
	gets an element from document
	@param {string} id id of element
	@returns {object} element of document
*/
function get_element(id){
	return document.getElementById(id);
}

/**
	gets a value from document
	@param {string} id id of element
	@returns {object} value of document
*/
function get_val(id){
	return get_element(id).value;
}

/**
	gets an HTML fragment from document
	@param {string} id id of HTML
	@returns {object} HTML of document
*/
function get_HTML(id){
	return get_element(id).innerHTML;
}

/**
	sets an HTML fragment in a document
	@param {string} id id of element
	@param {string} val value of HTML to set to element
*/
function set_HTML(id,val){
	get_element(id).innerHTML = val;
}

/**
	sets a value in a document
	@param {string} id id of element
	@param {string} val value to set to element
*/
function set_val(id,val){
	get_element(id).value = val;
}

/** Gets new physics depth input, validates it, and applies it */
function update_settings(){
	let new_rails = get_val("rails_depth");
	if(!isNaN(new_rails) && parseInt(new_rails)>0){
		if(current_stage!=undefined){
			update_rails(parseInt(new_rails));
		}
	}
	else{
		set_val("rails_depth",rails_depth);
	}
	
}

/** Pauses the game, and sets {@link updateNextControl} to the keybind being updated */
function updateControls(key){
	pause = pause = 1;
	updateNextControl = key;
}

/** Resets all kepresses to off */
function reset_keypress(){
	for(let j=0;j<get_possible_keys().length;j++){
		keyPressMap[get_possible_keys()[j]] = false;
	}
	soundManager["thrust"].pause();
	soundManager["thrust"].currentTime = 0;
}

/**
	Renders a menu
	@param inputs {array} Menu options
	@param display {string} Menu Title
*/
function menu(inputs,display){
	clear_screen();
	ctx.fillStyle = MENU_COLOUR;
	ctx.textAlign = "center";
	let shift_text = 0;
	
	//draw a title
	if(display!=undefined){
		ctx.fillText(display, MAX_X/2, 75);
		shift_text = 50;
	}
	
	//draw each item in a list
	let text_w;
	for(let i=0;i<inputs.length;i++){
		ctx.fillStyle = MENU_COLOUR;
		
		//draw a box around selected item, and invert the colour
		if(menu_selection===i){
			text_w = ctx.measureText(inputs[i]).width
			ctx.fillRect(MAX_X/2-text_w/1.9,90+shift_text+(65*i)-1.286*20,text_w*1.05,1.286*35);
			ctx.fillStyle = "black";
		}
		ctx.fillText(inputs[i],MAX_X/2,100+shift_text+(65*i));
	}
}

/**
	Moves a menu selector up or down with wrapping
	@param key {number} code of key press
	@param selector {number} current selection
	@param options {array} menu options
	@returns new selction
*/
function update_selection(key,selector,options){
	if(key === 40){
		selector = (selector+1) % options.length;
		play_tick_sound();
	}
	else if(key === 38){
		selector = ((selector-1) + options.length) % options.length;
		play_tick_sound();
	}
	return selector;
}

/** Prints the relevent contents of a stage to the console for debug purposes */
function debug_stage(){
	console.log("[,"+current_stage.player_x+","+current_stage.player_y+","+current_stage.player_xs+","+current_stage.player_ys+"]");
	for(let i=0;i<current_stage.planets.length;i++){
		console.log("["+current_stage.planets[i].x+","+current_stage.planets[i].y+",\""+current_stage.planets[i].colour+"\","+current_stage.planets[i].mass+"]");
	}
}

/**
	Inserts an alpha value into a canvas rgb colour
	@param colour {string} colour before alpha
	@param alpha {number} alpha value
	@returns New colour with alpha value
*/
function add_alpha(colour,alpha){
	return "rgba("+colour.substring(4,colour.length-1)+", "+alpha+")";
}

/*
	Sorts highscores by their actual score value
	@param a {array} highscore 1
	@param b {array} highscore 2
	@returns comparison of two highscores
*/
function high_score_sort(a,b){
	return b[1]-a[1];
}

/*	Plays a tick sound.	*/
function play_tick_sound(){
	//This reset was so that ticks would always be played, even if the previous fade out had not yet been completed
	soundManager["tick"].pause();
	soundManager["tick"].currentTime = 0;
	soundManager["tick"].play();
}