import Vec2 from "../vector/vec2.js";
import ComponentManager from "./component/component_manager.js";
import DrawableComponent from "./component/drawable_component.js";
import Bounds from "../vector/bounds.js";
import CollisionComponent from "./component/collision_component.js";
/**
* @module Actor
* @fileoverview Contains Actor class.
*/
/**
* @class
* Base class for objects in space.
*/
class Actor {
/**
* Create a new actor.
* @param {Vec2} pos Initial position in space
* @param {number} rotation Rotation around center
* @param {number} scale Uniform scaling for rendering
* @param {"auto"|null} collision Collision initializer
* @param {Sprite|string|null} sprite Sprite or sprite identifier for rendering
* @param {boolean} persistent Whether to always update regardless of loaded chunks
* @param {Object.<string, function|function[]>} events Events to register
* @param {Object.<string, ActorComponent>} components Components to add
* @param {string} id Global (in space) identifier of actor
* @constructor
*/
constructor({
pos = new Vec2(),
rotation = 0,
scale = 4,
collision = null,
sprite = null,
persistent = false,
events = {},
components = {},
id = ""
} = {}) {
this._intialized = false;
this._parent = null;
this._children = new Set();
this.events = new Map();
for (const event of Object.keys(events)) {
const handler = events[event];
if (handler instanceof Array)
for (const h of handler)
this.on(event, h);
else
this.on(event, handler);
}
this._components = new ComponentManager(this);
for (const compID of Object.keys(components))
this.components.add(compID, components[compID]);
this._pos = pos.copy();
this.scale = Vec2.from(scale);
this.persistent = persistent;
this.bounds = {
l: 0,
t: 0,
r: 0,
b: 0
};
this.bounds = null;
this.overlappingChunks = new Set();
if (sprite !== null) {
this.components.add("drawable", new DrawableComponent({
sprite: sprite
}));
}
if (collision !== null) {
if (collision === "auto") {
this.components.add("collision", new CollisionComponent({
bounds: this.drawableBounds
}));
} else if (collision instanceof CollisionComponent) {
this.components.add("collision", collision);
}
}
this.shadow = true;
this.id = null;
this._globalID = id;
this.space = null;
this.alwaysOnTop = false;
this.rotation = rotation;
this.debugPrefix = "ID: ";
this.markedForDeletion = false;
this._animations = new Set();
}
/**
* Executed when an actor is added to a space.
*/
init() {
for (const comp of this.components.getAll())
comp.init();
this.findOverlappingChunks();
this._intialized = true;
this.doEvent("init", this);
this.forEachChild(child => {
if (child.space === null)
this.space.addActor(child);
});
this.space.addGlobalID(this);
}
/**
* Mark the actor for deletion.
*/
delete() {
if (this.markedForDeletion)
return;
this.doEvent("delete", this);
this.clearOverlappingChunks();
this.components.destructor();
this.markedForDeletion = true;
for (const child of this._children.values())
child.delete();
if (this._parent !== null)
this._parent.removeChild(this);
}
//
// Getters
//
/**
* Get the global identifier.
* @returns {string} Global ID
*/
get globalID() {
return this._globalID;
}
/**
* Get the local position.
* @returns {Vec2} Local (relative) position
*/
get pos() {
return this._pos.copy();
}
/**
* Get the global position.
* @returns {Vec2} Global (absolute) position
*/
get globalPos() {
if (this.hasParent())
return this.pos.transform(this._parent.getTransformMatrix());
return this.pos;
}
/**
* Get the global transformation matrix.
* @param {boolean} trans Whether to include translation
* @param {boolean} rot Whether to include rotation
* @param {boolean} scale Whether to include scaling
* @returns Global transformation matrix
*/
getTransformMatrix(trans = true, rot = true, scale = true) {
let parent = mat3.create();
if (this._parent !== null)
parent = this._parent.getTransformMatrix(trans, rot, scale);
const matrix = mat3.create();
if (scale)
mat3.multiply(matrix, this.scale.toScaleMatrix(), matrix);
if (rot) {
const rotMat = mat3.create();
mat3.fromRotation(rotMat, this.rotation);
mat3.multiply(matrix, rotMat, matrix);
}
if (trans)
mat3.multiply(matrix, this.pos.toTranslationMatrix(), matrix);
mat3.multiply(matrix, parent, matrix);
return matrix;
}
/**
* Get the component manager.
* @returns {ComponentManager} Component manager
*/
get components() {
return this._components;
}
/**
* Get the total bounding box of all drawable components.
* @returns {Bounds} Total drawable bounding box
*/
get drawableBounds() {
let bounds = new Bounds();
for (const comp of this.components.getDrawables())
bounds = bounds.merge(comp.bounds);
return bounds;
}
/**
* Get the total bounding box of all collision components.
* @returns {Bounds} Total collision bounding box
*/
get collisionBounds() {
let bounds = new Bounds();
for (const comp of this.components.getCollisions())
bounds = bounds.merge(comp.bounds);
return bounds;
}
/**
* Check if the actor has been initialized.
* @returns {boolean} Whether actor has been initialized
*/
get initialized() {
return this._intialized;
}
//
// Setters
//
/**
* Set the global (space) identifier.
* @param {string} id New global identifier
*/
set globalID(id) {
this._globalID = id;
if (this.space !== null)
this.space.addGlobalID(this);
}
/**
* Set the local (relative) position.
* @param {Vec2} pos New local position
*/
set pos(pos) {
this._pos = pos.copy();
this.findOverlappingChunks();
}
/**
* Set the global (absolute) position.
* @param {Vec2} pos New global position
*/
set globalPos(pos) {
let matrix = mat3.create();
if (this.hasParent())
matrix = this._parent.getTransformMatrix();
this._pos = pos.inverseTransform(matrix);
}
//
// Update & Draw
//
/**
* Update the actor
* @param {number} elapsed Time since last update
*/
update(elapsed) {
this._components.updateAll(elapsed);
this.doEvent("update", elapsed, this);
for (const animation of this._animations.values()) {
animation.update(elapsed);
if (animation.done)
this._animations.delete(animation);
}
}
/**
* Draw debug information for the actor (if enabled).
* @param {CanvasRenderingContext2D} ctx Canvas context to draw on
* @param {boolean} showID Whether to show chunk actor ID
* @param {boolean} showDot Whether to show the dot in the center
*/
drawDebug(ctx, showID = true, showDot = true) {
const pos = this.globalPos;
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.fillStyle = $$.colors.debug;
if (showID) {
ctx.textAlign = "center";
ctx.font = "bold 16px " + $$.fonts.debug;
const id = this.debugPrefix + this.id;
const y = this.drawableBounds.top * this.scale.y;
ctx.fillText(id, 0, y - 12);
}
this.components.drawDebugAll(ctx);
if (showDot)
ctx.fillRect(-2, -2, 4, 4);
ctx.restore();
}
/**
* Draw the actor
* @param {CanvasRenderingContext2D} ctx The canvas context to draw on
*/
draw(ctx) {
const pos = this.pos;
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(this.rotation);
ctx.scale(this.scale.x, this.scale.y);
this._components.drawAll(ctx);
this.forEachChild(child => child.draw(ctx));
ctx.restore();
}
/**
* Set up the shadow for drawing.
* @param {CanvasRenderingContext2D} ctx Canvas context to draw on
*/
setShadow(ctx) {
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowColor = "rgba(0,0,0,0.32)";
}
/**
* Reset/remove the shadow for drawing.
* @param {CanvasRenderingContext2D} ctx Canvas context to draw on
*/
resetShadow(ctx) {
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
/**
* Check collision with another actor.
* @param {CollisionComponent} other Other component to check with
* @param {Vec2} pos The position to attempt to move to
* @returns {Object} Collision result
*/
checkCollision(other, pos) {
for (const comp of this.components.getCollisions()) {
const result = comp.check(other, pos);
if (result.result)
return result;
}
return {
result: false
};
}
/**
* Find, save, and register in all chunks that collision components are overlapping with.
*/
findOverlappingChunks() {
if (this.space === null)
return;
if (this.components.collisionCount === 0) {
this.clearOverlappingChunks();
return;
}
const old = this.overlappingChunks;
this.overlappingChunks = new Set(this.space.chunks.getInBounds(this.collisionBounds.scale(this.scale).add(this._pos)));
for (const chunk of this.overlappingChunks.values()) {
chunk.addOverlapping(this);
old.delete(chunk);
}
for (const chunk of old.values())
chunk.removeOverlapping(this);
}
/**
* Clear and remove actor from all overlapping chunks.
*/
clearOverlappingChunks() {
for (const chunk of this.overlappingChunks.values())
chunk.removeOverlapping(this);
this.overlappingChunks.clear();
}
/**
* Add a child actor.
* @typedef {T}
* @param {Actor|T} actor Actor to add as child
* @returns {Actor|T} Actor that was added
*/
addChild(actor) {
if (actor === null)
return actor;
this._children.add(actor);
actor.setParent(this);
if (this.space !== null && !actor.initialized)
this.space.addActor(actor);
return actor;
}
/**
* Remove a child actor. This will delete the actor.
* @param {Actor} actor Child to remove
*/
removeChild(actor) {
if (!this._children.has(actor))
return;
this._children.delete(actor);
actor.delete();
}
/**
* Remove and delete all child actors.
*/
clearChildren() {
for (const child of this._children)
child.delete();
this._children.clear();
}
/**
* Run a function for each child actor.
* @param {function} func Function to run for each child
*/
forEachChild(func = child => {}) {
this._children.forEach(func);
}
/**
* Set the parent actor (do not call this manually).
* @param {Actor} actor New parent actor.
*/
setParent(actor) {
this._parent = actor;
}
/**
* Check if the actor has a parent actor.
* @returns {boolean} Whether actor has a parent
*/
hasParent() {
return this._parent !== null;
}
//
// Events
//
/**
* @typedef {"init" | "update" | "delete" | "collide"} ActorEvent
*/
/**
* Add an event function.
* @param {ActorEvent|string} event Event to add function to
* @param {function} func Function to run when called
*/
on(event, func) {
if (!this.events.has(event))
this.events.set(event, []);
this.events.get(event).push(func);
}
/**
* Run all functions attached to a specified event.
* @param {ActorEvent|string} event Event to trigger
* @param {...any} args Arguments to pass into functions
*/
doEvent(event, ...args) {
if (event !== ".event")
this.doEvent(".event", event, ...args);
if (this.events.has(event))
for (const handler of this.events.get(event))
handler(...args);
}
//
// Animations
//
/**
* Add an animation.
* @param {ActorAnimation} animation Animation to add
* @returns {ActorAnimation} Animation that was added
*/
addAnimation(animation) {
animation.actor = this;
this._animations.add(animation);
return animation;
}
/**
* Stop all animations.
*/
stopAnimations() {
this._animations.clear();
}
/**
* Check if there are any active animations.
* @returns {boolean} Whether any animations are active
*/
hasAnimations() {
return this._animations.size > 0;
}
}
export default Actor;