const HasEntitiesMixin = require('./HasEntitiesMixin')
/**
* Entity is the base class for subgraphics.
* @mixes HasEntitiesMixin
*/
class Entity {
/**
* Create a new Entity with the specified options
* @param {Object} options The options for the Entity, composed of the properties.
* @property {Asset} asset The {@link Asset} to use for the Entity tiles
* @property {Object} parent The parent of this Entity
* @property {integer} x The x-coordinate in pixels relative to its parent (optional, default 0)
* @property {integer} y The y-coordinate in pixels relative to its parent (optional, default 0)
* @property {integer} z The z-coordinate relative to its parent (optional, default 0)
* @property {float} scale The current scale (zoom) where 1 = 100% (optional, default 1)
* @property {float} rotate The current rotation where 0ยบ (optional, default 0)
* @property {boolean} visible Set if this Entity is visible (optional, default true)
*/
constructor (options = {}) {
// Entities can have child entities
HasEntitiesMixin(this)
// Asset is the graphical asset used for drawing this entity.
// Usually, an asset will be divided up into 'tiles' of a specified width and height. A entity will display one
// of these tiles at any given time, and can be animated - so each tile is a frame.
this.asset = options.asset
// The width and height of the tiles in the asset
this.tileWidth = options.tileWidth
this.tileHeight = options.tileHeight
// The current tile coords as an array [x,y]
this.tile = typeof (options.tile) === 'undefined' ? null : options.tile
// The current X/Y from the origin of the container (the TiledScene, or the parent Entity)
this.x = options.x || 0
this.y = options.y || 0
// The Z index, in the stack of the container (the TiledScene, or the parent Entity)
this.z = options.z || 0
// The entity hotspot offset X/Y - this is the offset to the point used to compute the Entity's 'feet'
// when in a TiledScene in PERSPECTIVE_ANGLE mode - this influences the draw order.
this.hotspotX = options.hotspotX || 0
this.hotspotY = options.hotspotY || 0
// The Parent container (the TiledScene, or the parent Entity)
this.parent = typeof (options.parent) === 'undefined' ? null : options.parent
// The visible flag. Invisible Entities and their children are not drawn
this.visible = typeof (options.visible) === 'undefined' ? true : !!options.visible
// The scale and rotation settings for this entity. This context affects all child entities also.
this.scale = typeof (options.scale) === 'undefined' ? 1 : options.scale
this.rotate = typeof (options.rotate) === 'undefined' ? 0 : options.rotate
// Private props
this._animations = {}
this._currentanimation = null
this.redraw()
}
/**
* Mark the entity to be redrawn
*/
redraw () {
// Mark us for redraw
this._doredraw = true
// Child Entities
this.redrawEntities()
}
/**
* Draw the Entity into the given context.
* @param {CanvasRenderingContext2D} context The context in which to draw
*/
draw (context) {
// If we're not marked for a redraw or we're invisible, return
if (!this._doredraw || !this.visible) return
// Save the context params
context.save()
// Reset the origin to our coordinates (so child entities are relative to us)
context.translate(this.x, this.y)
context.scale(this.scale, this.scale)
context.rotate(this.rotate)
// Main Entity, omly draw if tile is not null and both tile coords are not null
if (this.tile != null && this.tile[0] != null && this.tile[1] != null) {
context.drawImage(
this.asset.element,
this.tile[0] * this.tileWidth,
this.tile[1] * this.tileHeight,
this.tileWidth,
this.tileHeight,
0,
0,
this.tileWidth,
this.tileHeight
)
}
// Draw all child entities in their own order
this.drawEntities(context)
// Restore the context params for the next thing being drawn
context.restore()
// Reset the redraw flag
this._doredraw = false
}
/**
* Add an animation with the specified name and definition (def)
* @param {String} name The name of the animation
* @param {Frame[]} def An array of Frames, Frame format is an array: [ tx, ty, dt, dx, dy ]:
*/
addAnimation (name, def) {
this._animations[name] = def
}
/**
* Start an animation
* @param {Object} animation An animation definition:
* @param {String} animation.name The name of the animation to start, must have been previously defined by addAnimation
* @param {Integer} animation.delay Default delay for each frame (unless overridden by the dt in the frame)
* @param {Integer} animation.frame The index of the starting frame (optional, defaults to 0)
* @param {Boolean} animation.loop Loop the animation (i) indefinitely if 'true' (ii) this number of times. (optional)
* @param {Integer} animation.dx x Delta at every frame (move x pixels), unless overridden by the dx in the frame (optional)
* @param {Integer} animation.dy y Delta at every frame (move y pixels), unless overridden by the dy in the frame (optional)
* @param {Integer} animation.minX Stop the animation when the x is equal or less than this value (optional)
* @param {Integer} animation.minY Stop the animation when the y is equal or less than this value (optional)
* @param {Integer} animation.maxX Stop the animation when the x is equal or greater than this value (optional)
* @param {Integer} animation.maxY Stop the animation when the y is equal or greater than this value (optional)
* @param {Tile} animation.stopTile Set this tile [x,y] when the animation stops (optional)
* @param {Function} animation.onStop A function to call when the animation stops, fn(entity) where entity is this entity (optional)
*/
animateStart (animation) {
if (animation) {
if (this._currentanimation) this.animateStop(Entity.STOPSTATUS_REPLACED)
this._currentanimation = animation
this._currentanimation.frame = 0
}
var nextFrame = () => {
// Return immediately if there is no current animation
if (!this._currentanimation) return
// Get the Frame and default Delay
var delay = this._currentanimation.delay
var anim = this._animations[this._currentanimation.name]
var frame = anim[this._currentanimation.frame]
// Set the tile
this.tile = frame.slice(0, 2)
// Override the default delay if specified in the frame
if (frame.length > 2 && frame[2] !== null) delay = frame[2]
// Translation deltaX/Y default from animation itself
var dx = this._currentanimation.dx || 0
var dy = this._currentanimation.dy || 0
// Override that deltaX/Y if specified in the frame
if (frame.length > 3 && frame[3] !== null) dx = frame[3]
if (frame.length > 4 && frame[4] !== null) dy = frame[4]
// If deltaX/Y is specified, move us
if (dx) this.x += dx
if (dy) this.y += dy
// End Conditions
if (
// We have moved to or past a specifed boundary (maxX, maxY)
(this._currentanimation.maxY && this.y >= this._currentanimation.maxY) ||
(this._currentanimation.minY && this.y <= this._currentanimation.minY) ||
(this._currentanimation.maxX && this.x >= this._currentanimation.maxX) ||
(this._currentanimation.minX && this.x <= this._currentanimation.minX)
) {
return this.animateStop(Entity.STOPSTATUS_COMPLETED)
}
// Increment the frame
this._currentanimation.frame++
// End / Loop conditions
if (this._currentanimation.frame >= anim.length) {
if (this._currentanimation.loop) {
// Loop is specifed, restart from first frame
this._currentanimation.frame = 0
// Loop can be true, or a counter. If it's a counter, decrement it.
if (this._currentanimation.loop !== true) this._currentanimation.loop--
} else {
// Loop not specifed, end the animation
return this.animateStop(Entity.STOPSTATUS_COMPLETED)
}
}
// Mark to redraw
this.redraw()
// Schedule the next frame
this._currentanimation._timeout = setTimeout(nextFrame, delay)
}
// Start the animation
nextFrame()
}
/**
* Stop the current animation with an optional Stop Status
* @param {Integer} stopStatus The stop status
*/
animateStop (stopStatus = Entity.STOPSTATUS_STOPPED) {
if (!this._currentanimation) return
if (this._currentanimation._timeout) clearTimeout(this._currentanimation._timeout)
if (this._currentanimation.stopTile) this.tile = this._currentanimation.stopTile
if (this._currentanimation.onStop) {
var func = this._currentanimation.onStop; var self = this; var f = () => { func(null, self, stopStatus) }
setImmediate(f)
}
this._currentanimation = null
this.redraw()
}
}
/**
* The animation stopped because it was finished (non-looped) or a boundary condition was met (maxX, maxY)
* @constant
*/
Entity.STOPSTATUS_COMPLETED = 1
/**
* The animation was stopped with animateStop
* @constant
*/
Entity.STOPSTATUS_STOPPED = 2
/**
* The animation was stopped because animateStart was called with a new animation, replacing it
* @constant
*/
Entity.STOPSTATUS_REPLACED = 3
module.exports = Entity