core2d
Version:
Multiplatform 2D interaction engine
701 lines (609 loc) • 15.1 kB
JavaScript
"use strict";
import { Animation } from "./Animation.mjs";
import { Direction } from "./Direction.mjs";
import { Frame } from "./Frame.mjs";
import { Rect } from "./Rect.mjs";
import { Static } from "./Static.mjs";
/**
* Represents a sprite, which is a basic game object that can be rendered on the screen.
* @extends Rect
*/
export class Sprite extends Rect {
/**
* Creates a new Sprite.
*/
constructor() {
super();
/**
* The horizontal acceleration of the sprite.
* @type {number}
*/
this.accelerationX = 0;
/**
* The vertical acceleration of the sprite.
* @type {number}
*/
this.accelerationY = 0;
/**
* The alpha transparency of the sprite.
* @type {number}
*/
this.alpha = 1;
/**
* The boundary of the sprite. If the sprite goes outside of this boundary, the `offBoundary` method is called.
* @type {Rect}
*/
this.boundary = null;
/**
* The color of the sprite.
* @type {string}
*/
this.color = null;
/**
* Whether the sprite is essential. If an essential sprite expires, the scene expires as well.
* @type {boolean}
*/
this.essential = false;
/**
* The expiration time of the sprite, in ticks.
* @type {number}
*/
this.expiration = 0;
/**
* Whether the sprite has expired.
* @type {boolean}
*/
this.expired = false;
/**
* The layer index of the sprite. Sprites with a higher layer index are rendered on top of sprites with a lower layer index.
* @type {number}
*/
this.layerIndex = 0;
/**
* The maximum horizontal speed of the sprite.
* @type {number}
*/
this.maxSpeedX = 0;
/**
* The maximum vertical speed of the sprite.
* @type {number}
*/
this.maxSpeedY = 0;
/**
* The scene that the sprite belongs to.
* @type {import("./Scene.mjs").Scene}
*/
this.scene = null;
/**
* Whether the sprite is solid. Solid sprites can collide with other solid sprites.
* @type {boolean}
*/
this.solid = false;
/**
* The horizontal speed of the sprite.
* @type {number}
*/
this.speedX = 0;
/**
* The vertical speed of the sprite.
* @type {number}
*/
this.speedY = 0;
/**
* Whether the sprite is visible.
* @type {boolean}
*/
this.visible = true;
/**
* The animation of the sprite.
* @type {Animation}
* @private
*/
this._animation = null;
/**
* The last horizontal speed of the sprite.
* @type {number}
* @private
*/
this._lastSpeedX = 0;
/**
* The last vertical speed of the sprite.
* @type {number}
* @private
*/
this._lastSpeedY = 0;
/**
* The last horizontal position of the sprite.
* @type {number}
* @private
*/
this._lastX = this.x;
/**
* The last vertical position of the sprite.
* @type {number}
* @private
*/
this._lastY = this.y;
/**
* The tags of the sprite.
* @type {Object<string, boolean>}
* @private
*/
this._tags = {};
/**
* The number of ticks that have passed since the sprite was created.
* @type {number}
* @private
*/
this._tick = 0;
}
/**
* The angle of the sprite in degrees.
* @type {number}
*/
get angle() {
return Static.toDegrees(Math.atan2(this.speedY, this.speedX));
}
/**
* The direction of the sprite.
* @type {Direction}
*/
get direction() {
const DIRECTION = new Direction();
if (this.x < this._lastX) {
DIRECTION.setLeft();
} else if (this.x > this._lastX) {
DIRECTION.setRight();
}
if (this.y < this._lastY) {
DIRECTION.setTop();
} else if (this.y > this._lastY) {
DIRECTION.setBottom();
}
return DIRECTION;
}
/**
* The image of the sprite.
* @type {HTMLImageElement|HTMLCanvasElement}
*/
get image() {
return this._animation && this._animation.image;
}
/**
* The number of ticks that have passed since the sprite was created.
* @type {number}
*/
get tick() {
return this._tick;
}
/**
* Sets the horizontal acceleration of the sprite.
* @param {number} accelerationX The horizontal acceleration.
* @returns {Sprite} This sprite.
*/
setAccelerationX(accelerationX = 0) {
this.accelerationX = accelerationX;
return this;
}
/**
* Sets the vertical acceleration of the sprite.
* @param {number} accelerationY The vertical acceleration.
* @returns {Sprite} This sprite.
*/
setAccelerationY(accelerationY = 0) {
this.accelerationY = accelerationY;
return this;
}
/**
* Sets the alpha transparency of the sprite.
* @param {number} alpha The alpha transparency.
* @returns {Sprite} This sprite.
*/
setAlpha(alpha = 1) {
this.alpha = alpha;
return this;
}
/**
* Sets the boundary of the sprite.
* @param {Rect} rect The boundary.
* @returns {Sprite} This sprite.
*/
setBoundary(rect = null) {
this.boundary = rect || this.scene;
return this;
}
/**
* Sets the color of the sprite.
* @param {string} color The color.
* @returns {Sprite} This sprite.
*/
setColor(color) {
this.color = color;
return this;
}
/**
* Sets whether the sprite is essential.
* @param {boolean} isEssential Whether the sprite is essential.
* @returns {Sprite} This sprite.
*/
setEssential(isEssential = true) {
this.essential = isEssential;
return this;
}
/**
* Sets the expiration time of the sprite.
* @param {number} expiration The expiration time in ticks.
* @returns {Sprite} This sprite.
*/
setExpiration(expiration = 0) {
this.expiration = expiration;
return this;
}
/**
* Sets whether the sprite has expired.
* @param {boolean} isExpired Whether the sprite has expired.
* @returns {Sprite} This sprite.
*/
setExpired(isExpired = true) {
this.expired = isExpired;
return this;
}
/**
* Sets the layer index of the sprite.
* @param {number} layerIndex The layer index.
* @returns {Sprite} This sprite.
*/
setLayerIndex(layerIndex = 0) {
this.layerIndex = layerIndex;
return this;
}
/**
* Sets the maximum horizontal speed of the sprite.
* @param {number} maxSpeedX The maximum horizontal speed.
* @returns {Sprite} This sprite.
*/
setMaxSpeedX(maxSpeedX = 0) {
this.maxSpeedX = maxSpeedX;
return this;
}
/**
* Sets the maximum vertical speed of the sprite.
* @param {number} maxSpeedY The maximum vertical speed.
* @returns {Sprite} This sprite.
*/
setMaxSpeedY(maxSpeedY = 0) {
this.maxSpeedY = maxSpeedY;
return this;
}
/**
* Sets whether the sprite is solid.
* @param {boolean} isSolid Whether the sprite is solid.
* @returns {Sprite} This sprite.
*/
setSolid(isSolid = true) {
this.solid = isSolid;
return this;
}
/**
* Sets the horizontal speed of the sprite.
* @param {number} speedX The horizontal speed.
* @returns {Sprite} This sprite.
*/
setSpeedX(speedX = 0) {
this.speedX = speedX;
return this;
}
/**
* Sets the vertical speed of the sprite.
* @param {number} speedY The vertical speed.
* @returns {Sprite} This sprite.
*/
setSpeedY(speedY = 0) {
this.speedY = speedY;
return this;
}
/**
* Sets whether the sprite is visible.
* @param {boolean} isVisible Whether the sprite is visible.
* @returns {Sprite} This sprite.
*/
setVisible(isVisible = true) {
this.visible = isVisible;
return this;
}
/**
* Sets the animation of the sprite.
* @param {Animation} animation The animation.
* @returns {Sprite} This sprite.
*/
setAnimation(animation) {
if (animation == this._animation) {
return this;
}
this._animation = animation;
this._animation.setFrameIndex(0);
this.height = this._animation.height;
this.width = this._animation.width;
return this;
}
/**
* Sets the image of the sprite.
* @param {HTMLImageElement|HTMLCanvasElement} image The image.
* @returns {Sprite} This sprite.
*/
setImage(image) {
this.setAnimation(new Animation([new Frame(image)]));
return this;
}
/**
* The image of the sprite.
* @type {HTMLImageElement|HTMLCanvasElement}
*/
set image(image) {
this.setImage(image);
}
/**
* Sets the speed of the sprite to an angle.
* @param {number} speed The speed.
* @param {number} degrees The angle in degrees.
* @returns {Sprite} This sprite.
*/
setSpeedToAngle(speed, degrees) {
const RADIANS = Static.toRadians(degrees);
this.setSpeedX(speed * Math.cos(RADIANS));
this.setSpeedY(speed * Math.sin(RADIANS));
return this;
}
/**
* Sets the speed of the sprite to a point.
* @param {number} speed The speed.
* @param {Point} point The point.
* @returns {Sprite} This sprite.
*/
setSpeedToPoint(speed, point) {
const SQUARE_DISTANCE =
Math.abs(this.centerX - point.x) + Math.abs(this.centerY - point.y);
this.setSpeedX(((point.x - this.centerX) * speed) / SQUARE_DISTANCE);
this.setSpeedY(((point.y - this.centerY) * speed) / SQUARE_DISTANCE);
return this;
}
/**
* Adds a tag to the sprite.
* @param {string} tag The tag.
* @returns {Sprite} This sprite.
*/
addTag(tag) {
this._tags[tag] = true;
return this;
}
/**
* Bounces the sprite from a direction.
* @param {Direction} direction The direction.
* @returns {Sprite} This sprite.
*/
bounceFrom(direction) {
if (
(this.speedX < 0 && direction.left) ||
(this.speedX > 0 && direction.right)
) {
this.bounceX();
}
if (
(this.speedY < 0 && direction.top) ||
(this.speedY > 0 && direction.bottom)
) {
this.bounceY();
}
return this;
}
/**
* Bounces the sprite horizontally.
* @returns {Sprite} This sprite.
*/
bounceX() {
this.setSpeedX(this.speedX * -1);
this.x += this.speedX;
return this;
}
/**
* Bounces the sprite vertically.
* @returns {Sprite} This sprite.
*/
bounceY() {
this.setSpeedY(this.speedY * -1);
this.y += this.speedY;
return this;
}
/**
* Expires the sprite.
*/
expire() {
this.expired = true;
}
/**
* Gets the collision direction with another sprite.
* @param {Sprite} sprite The other sprite.
* @returns {Direction} The collision direction.
*/
getCollision(sprite) {
const DIRECTION = new Direction();
const TA = this.top;
const RA = this.right;
const BA = this.bottom;
const LA = this.left;
const XA = this.centerX;
const YA = this.centerY;
const TB = sprite.top;
const RB = sprite.right;
const BB = sprite.bottom;
const LB = sprite.left;
if (XA <= LB && RA < RB) {
DIRECTION.setRight();
} else if (XA >= RB && LA > LB) {
DIRECTION.setLeft();
}
if (YA <= TB && BA < BB) {
DIRECTION.setBottom();
} else if (YA >= BB && TA > TB) {
DIRECTION.setTop();
}
return DIRECTION;
}
/**
* Checks if the sprite has a collision with a rectangle.
* @param {Rect} rect The rectangle.
* @returns {boolean} Whether the sprite has a collision with the rectangle.
*/
hasCollision(rect) {
return !(
this.left > rect.right ||
this.right < rect.left ||
this.top > rect.bottom ||
this.bottom < rect.top
);
}
/**
* Checks if the sprite has a tag.
* @param {string} tag The tag.
* @returns {boolean} Whether the sprite has the tag.
*/
hasTag(tag) {
return this._tags[tag];
}
/**
* Initializes the sprite.
*/
init() {
// no default behavior
}
/**
* Called when the sprite goes off its boundary.
*/
offBoundary() {
this.setExpired();
}
/**
* Called when the animation of the sprite loops.
*/
onAnimationLoop() {
// no default behavior
}
/**
* Called when the sprite collides with another sprite.
* @param {Sprite} sprite The other sprite.
* @returns {Sprite} The other sprite.
*/
onCollision(sprite) {
// no default behavior
return sprite;
}
/**
* Pauses the sprite.
* @returns {Sprite} This sprite.
*/
pause() {
this._lastSpeedX = this.speedX;
this._lastSpeedY = this.speedY;
this.speedX = 0;
this.speedY = 0;
return this;
}
/**
* Renders the sprite.
* @param {CanvasRenderingContext2D} context The rendering context.
* @returns {boolean} Whether the sprite was rendered.
*/
render(context) {
if (!this.visible) {
return false;
}
const X = Math.floor(this.x + this.scene.x);
const Y = Math.floor(this.y + this.scene.y);
if (this.alpha < 1) {
context.globalAlpha = this.alpha;
}
if (this.color) {
context.fillStyle = this.color;
context.fillRect(X, Y, this.width, this.height);
}
if (this._animation) {
context.drawImage(this._animation.image, X, Y, this.width, this.height);
}
if (this.alpha < 1) {
context.globalAlpha = 1;
}
return true;
}
/**
* Resumes the sprite.
* @returns {Sprite} This sprite.
*/
resume() {
this.speedX = this._lastSpeedX;
this.speedY = this._lastSpeedY;
return this;
}
/**
* Stops the sprite.
*/
stop() {
this._lastSpeedX = 0;
this._lastSpeedY = 0;
this.speedX = 0;
this.speedY = 0;
}
/**
* Synchronizes the sprite.
* @returns {boolean} Whether the sprite has expired.
*/
sync() {
this._lastX = this.x;
this._lastY = this.y;
this.update();
if (++this._tick == this.expiration) {
this.setExpired();
}
if (this.expired) {
return true;
}
if (this._animation && this._animation.sync()) {
this.onAnimationLoop();
}
this.x += this.speedX;
this.y += this.speedY;
this.speedX += this.accelerationX;
this.speedY += this.accelerationY;
if (this.maxSpeedX && Math.abs(this.speedX) > this.maxSpeedX) {
const SIGNAL = this.speedX / Math.abs(this.speedX);
this.speedX = this.maxSpeedX * SIGNAL;
}
if (this.maxSpeedY && Math.abs(this.speedY) > this.maxSpeedY) {
const SIGNAL = this.speedY / Math.abs(this.speedY);
this.speedY = this.maxSpeedY * SIGNAL;
}
if (this.boundary && !this.hasCollision(this.boundary)) {
this.offBoundary();
}
return false;
}
/**
* Updates the sprite.
*/
update() {
// no default behavior
}
/**
* Zooms the sprite.
* @param {number} ratio The zoom ratio.
* @returns {Sprite} This sprite.
*/
zoom(ratio) {
const widthChange = this.width * ratio;
this.width += widthChange;
this.x -= widthChange / 2;
const heightChange = this.height * ratio;
this.height += heightChange;
this.y -= heightChange / 2;
return this;
}
}