Source: object.js

/** A game object creates an object with a sprite at a certain position. Once the object is added to a SceneGraph it will be drawn on the canvas.
 * @class GameObject
 * @param {Number} name the name of the object
 * @param {Number} sprite the sprite for the object (null if undrawable)
 * @param {Number} x the x position of the object on the screen
 * @param {Number} y the y position of the object on the screen
 * @param {Number} [xOffset=0] the x offset of the top left corner of the sprite relative to the object position
 * @param {Number} [yOffset=0] the y offset of the top left corner of the sprite relative to the object position
 * @property {String} name the name of the object
 * @property {Sprite} sprite the object's sprite
 * @property {Vector} pos the current position of the object
 * @property {Number} nextX the x position of the object after an update
 * @property {Number} nextY the y position of the object after an update
 * @property {Number} lastX the last x position of the object (before the last update)
 * @property {Number} lastY the last y position of the object (before the last update)
 * @property {Boolean} isClicked whether the object is currently being clicked
 * @property {Boolean} isDraggable=false whether or not the object can be dragged via the mouse
 * @property {Boolean} isClickable=true whether or not the object can be clicked
 * @property {Boolean} isCollidable=true whether or not the object collides with anything
 * @property {Boolean} isLooping=false whether the object should loop around the edges of the screen
 * @property {Boolean} doUpdate=true whether or not the object should be updated
 * @property {Vector} direction the velocity of the object
 * @property {Number} angle the angle of the object, used for what angle to draw it at
 */
function GameObject(name, sprite, x, y, xOffset=0, yOffset=0) {
	var self = this;
	
	/** Creates the game object
	 * @memberof GameObject
	 * @function GameObject#constructor
	 * @param {Number} name the name of the object
	 * @param {Number} sprite the sprite for the object (null if undrawable)
	 * @param {Number} x the x position of the object on the screen
	 * @param {Number} y the y position of the object on the screen
	 * @param {Number} [xOffset=0] the x offset of the top left corner of the sprite relative to the object position
	 * @param {Number} [yOffset=0] the y offset of the top left corner of the sprite relative to the object position
	 */
	self.constructor = function(name, sprite, x, y, xOffset, yOffset) {
		self.name = name;
		self.sprite = sprite;
		self.pos = new Vector(x, y);
		self.nextX = x;
		self.nextY = y;
		self.lastX = x;
		self.lastY = y;
		self.xOffset = xOffset;
		self.yOffset = yOffset;
		self.isClicked = false;
		self.isDraggable = false;
		self.isClickable = true;
		self.isCollidable = true;
		self.isLooping = false;
		self.doUpdate = true;
		self.setSquareHitbox([0, 1], [0, 1]);
		self.direction = new Vector(0,0);
		self.velocity = new Vector(0,0);
		self.angle = 0;
	}
	
	Object.defineProperties(self, {
		/**
		 * @memberof GameObject
		 * @instance
		 * @property {Number} x x position of object (gets current position, but sets nextX)
		 */
		'x': { 
			get: function() {
				return self.pos.x;
			},
			set: function(val) {
				self.nextX = val;
			}
		},
		/**
		 * @memberof GameObject
		 * @instance
		 * @property {Number} y y position of object (gets current position, but sets nextY)
		 */
		'y': { 
			get: function() {
				return self.pos.y;
			},
			set: function(val) {
				self.nextY = val;
			}
		},
		
		/**
		 * @memberof GameObject
		 * @instance
		 * @property {Number} width width of image
		 * @readonly
		 */
		'width': { get: function() {if(self.sprite == null){return null} else{return self.sprite.width}}},
		/**
		 * @memberof GameObject
		 * @instance
		 * @property {Number} height height of image
		 * @readonly
		 */
		'height': { get: function() {if(self.sprite == null){ return null} else{return self.sprite.height}}},
		/**
		 * @memberof GameObject
		 * @instance
		 * @property {String} src source of the image
		 * @readonly
		 */
		'src': { get: function() {if(self.sprite == null){ return null} else{return self.sprite.src}}}
	});	
	
	/** Use this function to set a Square Hitbox for the object
	 * @memberof GameObject
	 * @function GameObject#setSquareHitbox
	 * @param {Number[]} [xRange=[0,1]] - The x range of which you want the hitbox to be. The array should only have a length of 2, with each number being in the range [0, 1]
	 * @param {Number[]} [yRange=[0,1]] - The y range of which you want the hitbox to be. The array should only have a length of 2, with each number being in the range [0, 1]
	 */
	self.setSquareHitbox = function(xRange=[0,1], yRange=[0,1]) {
		self.hitbox = {type: 'square', xRange: xRange, yRange: yRange, center: [(xRange[1]+xRange[0])/2, (yRange[1]+yRange[0])/2], halfWidth: (xRange[1]-xRange[0])/2, halfHeight: (yRange[1]-yRange[0])/2};
	}
	
	/** Use this function to set a Circular Hitbox for the object
	 * @memberof GameObject
	 * @function GameObject#setCircleHitbox
	 * @param {Vector} [center=halfway point of sprite] - The center of the circle
	 * @param {Number} [radius=Math.max(this.width, this.height)/2] - The radius of the circle
	 */
	self.setCircleHitbox = function(center=new Vector(self.width/2, self.height/2), radius=Math.max(self.width, self.height)/2) {
		self.hitbox = {type: 'circle', radius: radius, center: center};
	}
	
	/** The mouseDown handler for the object. By default, just sets the isClicked variable to true.
	 * @memberof GameObject
	 * @function GameObject#mouseDown
	 * @param {Game} game - the game object
	 * @param {MouseEvent} event - the actual mouse event
	 */
	self.mouseDown = function(game, event) {
		self.isClicked = true;
	}
	
	/** The mouseUp handler for the object. By default, just sets the isClicked variable to false.
	 * @memberof GameObject
	 * @function GameObject#mouseUp
	 * @param {Game} game - the game object
	 * @param {MouseEvent} event - the actual mouse event
	 * @returns {Boolean} True if the mouse is currently not clicking on the object
	 */
	self.mouseUp = function(game, event) {
		self.isClicked = false;
	}
	
	/** Override this function to put in your custom pre-updates for this object. Called at start of update. 
	 * @memberof GameObject
	 * @function GameObject#customUpdate
	 * @param {Game} game - the game object
	 */
	self.customUpdate = function(game){ }
	
	/** Override this function to put in your custom pre-draw for this object. Called at start of pre-draw. 
	 * @memberof GameObject
	 * @function GameObject#customPreDraw
	 * @param {Game} game - the game object
	 */
	self.customPreDraw = null;
	
	/** The update function of the object. Do not override this function. Handles looping, velocity, and dragging
	 * @memberof GameObject
	 * @function GameObject#update
	 * @param {Game} game - The game object
	 */
	self.update = function(game) {
		if (self.doUpdate) {
			self.customUpdate(game);
			if(self.isLooping && game.outOfBounds(self.nextX, self.nextY)){
				if(self.nextX > game.canvas.width){self.nextX = 0}
				else if(self.nextX < 0){self.nextX = game.canvas.width}
					
				if(self.nextY > game.canvas.height){self.nextY = 0}
				else if(self.nextY < 0){self.nextY = game.canvas.height}
			}
			if (self.direction.x != 0 || self.direction.y != 0) {
				self.nextX += self.direction.x;
				self.nextY += self.direction.y;
			}
			if (self.isDraggable && self.isClicked) {
				self.nextX = game.mouseX;
				self.nextY = game.mouseY;
			}
			self.postUpdate(game);
		}
	}
	
	/** Override this function to put in your custom post updates for this object. Called after update. 
	 * @memberof GameObject
	 * @function GameObject#postUpdate
	 * @param {Game} game - the game object
	 */
	self.postUpdate = function(game) {}
	
	/** 
	 * Call this function to update the object's position (moves position from nextX and nextY to current position)
	 * @memberof GameObject
	 * @function GameObject#updatePosition
	 */
	self.updatePosition = function() {
		self.lastX = self.x;
		self.lastY = self.y;
		self.pos.x = self.nextX;
		self.pos.y = self.nextY;
	}
	
	/** Changes the object angle based on the direction given
	 * @memberof GameObject
	 * @function GameObject#calculateAngleFromDirection
	 * @param {Number} dirX - the x direction
	 * @param {Number} dirY - the y direction
	 */
	self.calculateAngleFromDirection = function(dirX, dirY) {
		self.angle = Math.atan2(dirY, dirX) / (Math.PI/180);
	}
	
	/** This function draws the object. Do not override this function. If you wish to add anything to this function, use customePreDraw.
	 * @memberof GameObject
	 * @function GameObject#draw
	 * @param {RenderingContext} context - the context of the game.
	 * @returns {Boolean} true if sprite is drawn
	 */
	self.draw = function(context) {
		if (self.customPreDraw)
			self.customPreDraw(context);
		if (self.sprite) {
			self.sprite.draw(context, self.x - self.xOffset, self.y - self.yOffset, self.angle);
			return true;
		}
		return false;
	}
	
	/** Returns true if the object collides at a specific point.
	 * @memberof GameObject
	 * @function GameObject#pointCollide
	 * @param {Number} x - x position to check
	 * @param {Number} y - y position to check
	 * @returns {Boolean} true if the object collides with the specific position
	 */
	self.pointCollide = function(x, y) {
		var pos = new Vector(x, y);
		if (self.hitbox.type == 'square') {
			var minX = self.x - self.xOffset + self.hitbox.xRange[0] * self.width;
			var maxX = self.x - self.xOffset + self.hitbox.xRange[1] * self.width;
			var minY = self.y - self.yOffset + self.hitbox.yRange[0] * self.height;
			var maxY = self.y - self.yOffset + self.hitbox.yRange[1] * self.height;
			return x >= minX && x <= maxX && y >= minY && y <= maxY;
		} else if (self.hitbox.type == 'circle') {
			return pos.subtract(self.hitbox.center).magnitude() < self.hitbox.radius;
		} 
		return false;
	}
	
	/** Returns true if the object collides with another object
	 * @memberof GameObject
	 * @function GameObject#checkForObjectCollide
	 * @param {GameObject} other - the object to check collisions for.
	 * @returns {Boolean} true if the objects collide
	 */
	self.checkForObjectCollide = function(other) {
		if (self.hitbox.type == 'square') {
			var minX = self.nextX - self.xOffset + self.hitbox.xRange[0] * self.width;
			var maxX = self.nextX - self.xOffset + self.hitbox.xRange[1] * self.width;
			var minY = self.nextY - self.yOffset + self.hitbox.yRange[0] * self.height;
			var maxY = self.nextY - self.yOffset + self.hitbox.yRange[1] * self.height;
			if (other.hitbox.type == 'square') {
				//TODO account for angle
				var otherMinX = other.x - other.xOffset + other.hitbox.xRange[0] * other.width;
				var otherMaxX = other.x - other.xOffset + other.hitbox.xRange[1] * other.width;
				var otherMinY = other.y - other.yOffset + other.hitbox.yRange[0] * other.height;
				var otherMaxY = other.y - other.yOffset + other.hitbox.yRange[1] * other.height;
				return minX < otherMaxX && maxX > otherMinX && minY < otherMaxY && maxY > otherMinY
			} else if (other.hitbox.type == 'circle') {
				//TODO account for angle
				var centerX = self.nextX - self.xOffset + self.hitbox.center[0] * self.width;
				var centerY = self.nextY - self.yOffset + self.hitbox.center[1] * self.height;
				var diffCentX = Math.abs(other.hitbox.center.x + other.x - other.xOffset - centerX);
				var diffCentY = Math.abs(other.hitbox.center.y + other.y - other.yOffset - centerY);
				if (diffCentX >= self.hitbox.halfWidth + other.hitbox.radius || diffCentY >= self.hitbox.halfHeight + other.hitbox.radius) {
					return false;
				}
				if (diffCentX < self.hitbox.halfWidth || diffCentY < self.hitbox.halfHeight) {
					return true;
				}
				return Math.hypot(diffCentX - self.hitbox.halfWidth, diffCentY - self.hitbox.halfHeight) < other.hitbox.radius;
			}
		} else if (self.hitbox.type == 'circle') {
			if (other.hitbox.type == 'square') {
				return other.checkForObjectCollide(self);
			} else if (other.hitbox.type == 'circle') {
				return other.hitbox.center.add(new Vector(other.x - other.xOffset, other.y-other.yOffset)).subtract(self.hitbox.center.add(new Vector(self.nextX - self.xOffset, self.nextY-self.yOffset))).magnitude() < self.hitbox.radius + other.hitbox.radius
			}
		}
		return false;
	}
	
	/** Changes the the sprite sheet to a specific sheet number
	 * @memberof GameObject
	 * @function GameObject#changeSpriteSheetNumber
	 * @param {Number} number - the accessor that the sprite sheet will use
	 */
	self.changeSpriteSheetNumber = function(number) {
		self.sprite.currentSprite = number;
	}
	
	/** Calculates velocity based on the angle and speed of the object
	 * @memberof GameObject
	 * @function GameObject#calculateVelocity
	 * @param {Number} speed - the speed of the object
	 * @param {Number} angle - the angle of the object
	 * @return {Vector} The new velocity based on the given angle and speed
	 */
	self.calculateVelocity = function(speed, angle){
		return tempVel = new Vector(speed*Math.sin(angle * (Math.PI/180)), -speed*Math.cos(angle * (Math.PI/180)))
	}
	 
	/** Returns a slowed velocity based on the deceleration amount. 
	 * @memberof GameObject
	 * @memberof GameObject#slowVelocity
	 * @param {Number} velocity - the velocity of the object
	 * @param {Number} decelerationAmt - the amount to slow velocity by
	 * @returns {Vector} The new velocity based on the old velocity and the deceleration amount.
	 */
	self.slowVelocity = function(velocity, decelerationAmt){
		var currentVelVector = velocity;
		var velMagnitudeCurrent = currentVelVector.magnitude();
		if(velMagnitudeCurrent != 0){
			var velMagnitudeNext = velMagnitudeCurrent - decelerationAmt;
			if(velMagnitudeNext < 0 )
				velMagnitudeNext = 0;
			var velUnitVector = currentVelVector.normalize();
			var nextVelVector = velUnitVector.multiply(velMagnitudeNext);
			return nextVelVector;
		}
		return velocity;
	}
	/** Adds an angle amount to the current angle of the object
	 * @memberof GameObject
	 * @function GameObject#changeAngle
	 * @param {Number} angle - the amount of degrees to add to the current angle
	 */
	self.changeAngle = function(angle){
		self.angle += angle;
	}
	/** 
	 * Override this function to determine what this object can and can't collide with.
	 * @memberof GameObject
	 * @function GameObject#canCollideWith
	 * @param {GameObject} other - the object that collided
	 * @return {Boolean} if the object can collide with other objects
	 */
	self.canCollideWith = function(other) {
		return false;
	}
	
	/** Overide this function with the event you want to take place when the object collides with another
	 * @memberof GameObject
	 * @function GameObject#collideWith
	 * @param {GameObject} other - the object that collided
	 * @returns {Boolean} False if there is no event to take place when a collision has happened.
	 */ 
	self.collideWith = function(other) {
		return false;
	}
	
	/** Tries to collide with another object. Fails if other object isn't collidable, if hitboxes aren't colliding, or if the objects don't have an applicable collision interaction.
	 * @memberof GameObject
	 * @function GameObject#tryCollide
	 * @param {GameObject} other - the object that collided
	 * @returns {Boolean} returns true if the object successfully collides with another object
	 */
	self.tryCollide = function(other) {
		if (self !== other && other.isCollidable && self.checkForObjectCollide(other) && self.canCollideWith(other)) {
			return self.collideWith(other);
		}
		return false;
	}
	/** Creates the GameObject */
	self.constructor(name, sprite, x, y, xOffset, yOffset);
};