Source: Sprite.js

import * as BAGEL from "./index.js";

/**
 * A Sprite represents the entities in a game.
 * <br/>
 * Sprites have a {@link Vector} position, a {@link Rectangle} boundary, and a {@link Texture}.
 */
class Sprite
{
	/**
	 * Initialize all fields to default values.
	 * @constructor
	 */
	constructor()
	{
		this.position  = new BAGEL.Vector();
		this.rectangle = new BAGEL.Rectangle();

		// keep position of boundary rectangle
		//  synchronized with sprite position
		this.rectangle.position = this.position;

		this.texture = null;
		this.visible = true;

		// the Group that contains this sprite
		this.parentGroup = null;

		// additional graphics-related data

		// initial angle = 0 indicates sprite is initially facing to the right
		this.angle  = 0;
		this.opacity = 1.0;
		this.mirrored = false;
		this.flipped = false;

		// store physics data
		this.physics = null;

		// store rectangles to define boundary/wrapping/destroy areas.
		// if a rectangle exists, corresponding function called during update.
		this.boundRectangle   = null;
		this.wrapRectangle    = null;
		this.destroyRectangle = null;

		// store animation data
		this.animation = null;

		// list of Actions: functions applied to object over time
		this.actionList = [];	
	}
	
	/**
     * Set the x and y coordinates of the center of this sprite.
     * @param {number} x - the new x coordinate of the center of this sprite
     * @param {number} y - the new y coordinate of the center of this sprite
     */
    setPosition(x, y)
	{
		this.position.setValues(x, y);
	}

	/**
     * Change this sprite's position by the given amounts.
     * @param {number} dx - value to add to the position x coordinate
     * @param {number} dy - value to add to the position y coordinate
     */
	moveBy(dx, dy)
	{
		this.position.addValues(dx, dy);
	}	
	
	/**
     * Set the texture used when drawing this sprite.
     *  Also updates the size of the sprite to the size of the texture.
     * @param {Texture} texture - the texture to use when drawing this sprite
     */
	setTexture(texture)
	{
		this.texture = texture;
		this.rectangle.width  = texture.region.width;
		this.rectangle.height = texture.region.height;
	}
	
	/**
     * Set the size (rectangle width and height) of the sprite;
     *  used when drawing sprite and checking for overlap with other sprites.
     * @param {number} width - the new width of this sprite
     * @param {number} height - the new height of this sprite
     */
	setSize(width, height)
	{
		this.rectangle.width  = width;
		this.rectangle.height = height;
	}
	
	/**
     * Set whether this sprite should be visible or not;
     *  determines whether sprite will be drawn on canvas.
     * @param {boolean} visible - determines if this sprite is visible or not
     */
    setVisible(visible)
	{
		this.visible = visible;
	}

	/**
     * Get the angle (in degrees) between this sprite and the positive x-axis.
     * <br/>
     * (Angles increase in clockwise direction, since positive y-axis is down.)
     * @return {number} angle between this sprite and positive x-axis
     */
    getAngle(angleDegrees)
	{
		this.angle = angleDegrees;
	}

	/**
     * Set the angle (in degrees) between this sprite and the positive x-axis.
     * <br/>
     * (Angles increase in clockwise direction, since positive y-axis is down.)
     * @param {number} angleDegrees - the new direction angle of this sprite
     */
    setAngle(angleDegrees)
	{
		this.angle = angleDegrees;
	}

	/**
     * Rotate this sprite by the given amount.
     * @param {number} angleDegrees - the angle (in degrees) to rotate this sprite by
     */
    rotateBy(angleDegrees)
	{
		this.angle += angleDegrees;
	}

	/**
     * Move this sprite by a given distance in a given direction.
     * @param {number} distance - distance this sprite will move
     * @param {number} angle - direction along which this sprite will move
     */
    moveAtAngle(distance, angleDegrees)
	{
		this.position.x += distance * Math.cos(angleDegrees * Math.PI/180);
		this.position.y += distance * Math.sin(angleDegrees * Math.PI/180);
	}
	
	/**
     * Move this sprite by a given distance along its current direction angle.
     * @param {number} distance - distance this sprite will move
     */
    moveForward(distance)
	{
		this.moveAtAngle(distance, this.angle);
	}

	/**
     * Change the opacity when drawing, 
     *   enabling objects underneath to be partially visible
     *   by blending their colors with the colors of this object.
     * <br>
     * 0 = fully transparent (appears invisible);  1 = fully opaque (appears solid) 
     * @param {number} opacity - opacity of this object
     */
    setOpacity(opacity)
	{
		this.opacity = opacity;
	}

	/**
     * Determine if this sprite overlaps another sprite (includes overlapping edges).
	 * @param {Sprite} other - sprite to check for overlap with
	 * @return {boolean} true if this sprite overlaps other sprite, false otherwise
     */
    overlaps(other)
    {
    	return this.rectangle.overlaps( other.rectangle );
    }

    /**
     * Initialize {@link Physics} data for this sprite and link to position.
     * <br/>
     * Physics object will be automatically updated and used to control position. 
     * @param {number} accValue - default magnitude of acceleration when using {@link Physics#accelerateAtAngle|accelerateAtAngle}
	 * @param {number} maxSpeed - maximum speed: if speed is ever above this amount, it will be reduced to this amount
	 * @param {number} decValue - when not accelerating, object will decelerate (decrease speed) by this amount
	 */
    setPhysics(accValue, maxSpeed, decValue)
    {
    	this.physics = new BAGEL.Physics(accValue, maxSpeed, decValue);
    	this.physics.positionVector = this.position;
    }

    /**
     * Set world dimensions (width and height) to be used to bound sprite position within the world.
     * <br/>
     * Calling this function will cause {@link Sprite#boundWithinRectangle|boundWithinRectangle} 
     *  to be called automatically by the {@link Sprite#update|update} function.
     * @param {number} width - the width of the screen or world
     * @param {number} height - the height of the screen or world
     */
    setBoundRectangle(width, height)
    {
    	this.boundRectangle = new BAGEL.Rectangle(width/2,height/2, width,height);
    }

    /**
     * Set world dimensions (width and height) to be used to wrap sprite around world when moving beyond screen edges.
     * <br/>
     * Calling this function will cause {@link Sprite#wrapAroundRectangle|wrapAroundRectangle} 
     *  to be called automatically by the {@link Sprite#update|update} function.     
     * @param {number} width - the width of the screen or world
     * @param {number} height - the height of the screen or world
     */
    setWrapRectangle(width, height)
    {
    	this.wrapRectangle = new BAGEL.Rectangle(width/2,height/2, width,height);
    }

    /**
     * Set world dimensions (width and height) to be used to destroy sprite if it moves beyond world edges.
     * <br/>
     * Calling this function will cause {@link Sprite#destroyOutsideRectangle|destroyOutsideRectangle} 
     *  to be called automatically by the {@link Sprite#update|update} function.     
     * @param {number} width - the width of the screen or world
     * @param {number} height - the height of the screen or world
     */
    setDestroyRectangle(width, height)
    {
    	this.destroyRectangle = new BAGEL.Rectangle(width/2,height/2, width,height);
    }

    /**
     * Adjusts the position of this sprite
     *  so that it remains completely contained within screen or world dimensions.
     * <br/>
     * Called automatically by {@link Sprite#update|update} if {@link Sprite#setBoundRectangle|setBoundRectangle} was previously called.
     * @param {number} worldWidth - the width of the screen or world
     * @param {number} worldHeight - the height of the screen or world
     */
    boundWithinRectangle(worldWidth, worldHeight)
    {
    	if (this.position.x - this.rectangle.width/2 < 0)
    		this.position.x = this.rectangle.width/2;

    	if (this.position.x + this.rectangle.width/2 > worldWidth)
    		this.position.x = worldWidth - this.rectangle.width/2;

    	if (this.position.y - this.rectangle.height/2 < 0)
    		this.position.y = this.rectangle.height/2;

    	if (this.position.y + this.rectangle.height/2 > worldHeight)
    		this.position.y = worldHeight - this.rectangle.height/2;    		 
    }

    /**
     * If this sprite moves completely beyond an edge of the screen or world,
     *   adjust its position to the opposite side.
     * <br/>
     * Called automatically by {@link Sprite#update|update} if {@link Sprite#setWrapRectangle|setWrapRectangle} was previously called.
     * @param {number} worldWidth - the width of the screen or world
     * @param {number} worldHeight - the height of the screen or world
     */
    wrapAroundRectangle(worldWidth, worldHeight)
    {
    	if (this.position.x + this.rectangle.width/2 < 0)
    		this.position.x = worldWidth + this.rectangle.width/2;

    	if (this.position.x - this.rectangle.width/2 > worldWidth)
    		this.position.x = -this.rectangle.width/2;

    	if (this.position.y + this.rectangle.height/2 < 0)
    		this.position.y = worldHeight + this.rectangle.height/2;    		 

    	if (this.position.y - this.rectangle.height/2 > worldHeight)
    		this.position.y = -this.rectangle.height/2;
    }

    /**
     * Destroy this sprite if it moves completely beyond the edges of the screen or world.
     * <br/>
     * Called automatically by {@link Sprite#update|update} if {@link Sprite#setDestroyRectangle|setDestroyRectangle} was previously called.
     * @param {number} worldWidth - the width of the screen or world
     * @param {number} worldHeight - the height of the screen or world
     */
    destroyOutsideRectangle(worldWidth, worldHeight)
    {
    	if ( (this.position.x + this.rectangle.width/2 < 0) || 
    		
    		 (this.position.x - this.rectangle.width/2 > worldWidth) ||
    		 (this.position.y + this.rectangle.height/2 < 0) ||
    		 (this.position.y - this.rectangle.height/2 > worldHeight) )
    		this.destroy();
    }

	/**
     * Set the {@link Animation} used when drawing this sprite.
     * <br/>
     * Also updates the size of the sprite to the size of an animation frame.
     * <br/>
     * Animation object will be automatically updated and used when drawing sprite. 
     * @param {Animation} animation - the animation to use when drawing this sprite
     */
	setAnimation(animation)
	{
		this.animation = animation;
		this.texture = animation.texture;
		this.rectangle.width  = animation.texture.region.width;
		this.rectangle.height = animation.texture.region.height;
	}

	/**
	 * Add an {@link Action} to this sprite: a special function that
	 *  will be automatically applied to the sprite over time until it is complete.
	 * <br>
	 * Most common actions can be created with the static methods in the
	 * {@link ActionFactory} class.
	 * <br>
	 * All actions added to this sprite are performed in parallel, unless
	 *  enclosed by a {@link ActionFactory#sequence|Sequence} action.
	 * @param {Action} action - an action to be applied to this object
	 */
	addAction(action)
	{
		this.actionList.push(action);
	}


    /**
     * Perform any internal actions that should be repeated every frame.
     * @param {number} deltaTime - time elapsed since previous frame
     */
    update(deltaTime)
    {
    	// use physics to update position (based on velocity and acceleration)
        //   if it has been initialized for this sprite
    	if (this.physics != null)
            this.physics.update(deltaTime);

        if (this.boundRectangle != null)
        	this.boundWithinRectangle(this.boundRectangle.width, this.boundRectangle.height);

        if (this.wrapRectangle != null)
        	this.wrapAroundRectangle(this.wrapRectangle.width, this.wrapRectangle.height);

        if (this.destroyRectangle != null)
        	this.destroyOutsideRectangle(this.destroyRectangle.width, this.destroyRectangle.height);

    	if (this.animation != null)
            this.animation.update(deltaTime);

        // Update all actions (in parallel, by default).
        // Using a copy of the list to avoid skipping the next action in the list
        //  when the previous action is removed.
		let actionListCopy = this.actionList.slice();
		for (let action of actionListCopy)
		{
			let finished = action.apply(this, deltaTime);
			if (finished)
			{
				let index = this.actionList.indexOf(action);
				if (index > -1)
					this.actionList.splice(index, 1);
			}
		}
    }

	/**
	 * Draw the sprite on a canvas, centered at the sprite's position, in an area corresponding to the sprite's size.
	 * Also take into account sprite's angle, whether the image should be flipped or mirrored, and the opacity of the image.
	 * If visible is set to false, sprite will not be drawn.
     * @param context - the graphics context object associated to the game canvas
	 */
	draw(context)
	{
		if ( !this.visible )
			return;
		
		let A = this.angle * Math.PI/180;
		let scaleX = 1;
		let scaleY = 1;
		if (this.mirrored)
			scaleX *= -1;
		if (this.flipped)
			scaleY *= -1;
		let cosA = Math.cos(A);
		let sinA = Math.sin(A);

		context.setTransform(scaleX*cosA, scaleX*sinA, -scaleY*sinA, scaleY*cosA, 
			this.position.x, this.position.y);

		context.globalAlpha = this.opacity;

		// image, 4 source parameters, 4 destination parameters
        context.drawImage(this.texture.image, 
            this.texture.region.position.x, this.texture.region.position.y, 
            this.texture.region.width, this.texture.region.height,
            -this.rectangle.width/2, -this.rectangle.height/2, 
             this.rectangle.width, this.rectangle.height);
	}

	/**
	 * Remove this sprite from the group that contains it.
	 */
	destroy()
	{
		this.parentGroup.removeSprite(this);
	}
	
}


export { Sprite };