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 };