2020-08-07 12:21:15 -04:00
|
|
|
import Vec2 from "../DataTypes/Vec2";
|
2020-08-10 13:19:59 -04:00
|
|
|
import Receiver from "../Events/Receiver";
|
2020-09-19 20:18:39 -04:00
|
|
|
import Emitter from "../Events/Emitter";
|
2020-09-06 18:07:09 -04:00
|
|
|
import Scene from "../Scene/Scene";
|
2020-09-05 00:04:14 -04:00
|
|
|
import Layer from "../Scene/Layer";
|
2021-01-26 10:08:38 -05:00
|
|
|
import AI from "../DataTypes/Interfaces/AI";
|
|
|
|
import Physical from "../DataTypes/Interfaces/Physical";
|
|
|
|
import Positioned from "../DataTypes/Interfaces/Positioned";
|
|
|
|
import { isRegion } from "../DataTypes/Interfaces/Region";
|
|
|
|
import Unique from "../DataTypes/Interfaces/Unique";
|
|
|
|
import Updateable from "../DataTypes/Interfaces/Updateable";
|
|
|
|
import DebugRenderable from "../DataTypes/Interfaces/DebugRenderable";
|
|
|
|
import Actor from "../DataTypes/Interfaces/Actor";
|
2020-10-25 17:46:43 -04:00
|
|
|
import Shape from "../DataTypes/Shapes/Shape";
|
|
|
|
import Map from "../DataTypes/Map";
|
|
|
|
import AABB from "../DataTypes/Shapes/AABB";
|
2020-11-04 14:03:52 -05:00
|
|
|
import NavigationPath from "../Pathfinding/NavigationPath";
|
2020-11-29 19:49:04 -05:00
|
|
|
import TweenManager from "../Rendering/Animations/TweenManager";
|
2020-12-21 12:32:32 -05:00
|
|
|
import Debug from "../Debug/Debug";
|
|
|
|
import Color from "../Utils/Color";
|
2021-02-19 15:35:57 -05:00
|
|
|
import Circle from "../DataTypes/Shapes/Circle";
|
2020-08-07 12:21:15 -04:00
|
|
|
|
2020-09-13 20:57:28 -04:00
|
|
|
/**
|
2021-01-05 11:31:17 -05:00
|
|
|
* The representation of an object in the game world.
|
|
|
|
* To construct GameNodes, see the @reference[Scene] documentation.
|
2020-09-13 20:57:28 -04:00
|
|
|
*/
|
2020-12-21 12:32:32 -05:00
|
|
|
export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, DebugRenderable {
|
2020-10-25 17:46:43 -04:00
|
|
|
/*---------- POSITIONED ----------*/
|
2020-09-28 18:57:02 -04:00
|
|
|
private _position: Vec2;
|
2020-10-25 17:46:43 -04:00
|
|
|
|
|
|
|
/*---------- UNIQUE ----------*/
|
|
|
|
private _id: number;
|
|
|
|
|
|
|
|
/*---------- PHYSICAL ----------*/
|
2021-02-05 13:56:56 -05:00
|
|
|
hasPhysics: boolean = false;
|
|
|
|
moving: boolean = false;
|
|
|
|
onGround: boolean = false;
|
|
|
|
onWall: boolean = false;
|
|
|
|
onCeiling: boolean = false;
|
|
|
|
active: boolean = false;
|
2020-10-25 17:46:43 -04:00
|
|
|
collisionShape: Shape;
|
2020-12-22 13:18:10 -05:00
|
|
|
colliderOffset: Vec2;
|
2020-10-25 17:46:43 -04:00
|
|
|
isStatic: boolean;
|
|
|
|
isCollidable: boolean;
|
|
|
|
isTrigger: boolean;
|
|
|
|
group: string;
|
|
|
|
triggers: Map<string>;
|
|
|
|
_velocity: Vec2;
|
|
|
|
sweptRect: AABB;
|
2020-11-16 11:02:45 -05:00
|
|
|
collidedWithTilemap: boolean;
|
|
|
|
physicsLayer: number;
|
2020-10-28 14:55:32 -04:00
|
|
|
isPlayer: boolean;
|
2020-12-22 13:18:10 -05:00
|
|
|
isColliding: boolean = false;
|
2020-10-25 17:46:43 -04:00
|
|
|
|
2020-11-04 14:03:52 -05:00
|
|
|
/*---------- ACTOR ----------*/
|
|
|
|
_ai: AI;
|
|
|
|
aiActive: boolean;
|
|
|
|
actorId: number;
|
|
|
|
path: NavigationPath;
|
|
|
|
pathfinding: boolean = false;
|
|
|
|
|
2020-11-29 19:49:04 -05:00
|
|
|
/*---------- GENERAL ----------*/
|
2021-01-05 11:31:17 -05:00
|
|
|
/** An event receiver. */
|
2020-09-19 20:18:39 -04:00
|
|
|
protected receiver: Receiver;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** An event emitter. */
|
2020-09-19 20:18:39 -04:00
|
|
|
protected emitter: Emitter;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** A reference to the scene this GameNode is a part of. */
|
2020-09-06 18:07:09 -04:00
|
|
|
protected scene: Scene;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** The visual layer this GameNode resides in. */
|
2020-09-06 18:07:09 -04:00
|
|
|
protected layer: Layer;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** A utility that allows the use of tweens on this GameNode */
|
2020-11-29 19:49:04 -05:00
|
|
|
tweens: TweenManager;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** A tweenable property for rotation. Does not affect the bounding box of this GameNode - Only rendering. */
|
2020-11-29 19:49:04 -05:00
|
|
|
rotation: number;
|
2021-01-05 11:31:17 -05:00
|
|
|
/** The opacity value of this GameNode */
|
2020-12-01 14:04:24 -05:00
|
|
|
alpha: number;
|
2020-08-07 12:21:15 -04:00
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// Constructor docs are ignored, as the user should NOT create new GameNodes with a raw constructor
|
2020-08-07 12:21:15 -04:00
|
|
|
constructor(){
|
2020-09-28 18:57:02 -04:00
|
|
|
this._position = new Vec2(0, 0);
|
2020-11-18 10:57:55 -05:00
|
|
|
this._position.setOnChange(() => this.positionChanged());
|
2020-09-19 20:18:39 -04:00
|
|
|
this.receiver = new Receiver();
|
|
|
|
this.emitter = new Emitter();
|
2020-11-29 19:49:04 -05:00
|
|
|
this.tweens = new TweenManager(this);
|
|
|
|
this.rotation = 0;
|
2020-12-01 14:04:24 -05:00
|
|
|
this.alpha = 1;
|
2020-08-07 12:21:15 -04:00
|
|
|
}
|
2020-08-17 20:09:41 -04:00
|
|
|
|
2020-10-25 17:46:43 -04:00
|
|
|
/*---------- POSITIONED ----------*/
|
2020-09-28 18:57:02 -04:00
|
|
|
get position(): Vec2 {
|
|
|
|
return this._position;
|
|
|
|
}
|
|
|
|
|
|
|
|
set position(pos: Vec2) {
|
|
|
|
this._position = pos;
|
2020-11-18 10:57:55 -05:00
|
|
|
this._position.setOnChange(() => this.positionChanged());
|
2020-09-28 18:57:02 -04:00
|
|
|
this.positionChanged();
|
|
|
|
}
|
|
|
|
|
2020-12-21 12:32:32 -05:00
|
|
|
get relativePosition(): Vec2 {
|
2021-02-05 13:56:56 -05:00
|
|
|
return this.inRelativeCoordinates(this.position);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts a point to coordinates relative to the zoom and origin of this node
|
|
|
|
* @param point The point to conver
|
|
|
|
* @returns A new Vec2 representing the point in relative coordinates
|
|
|
|
*/
|
|
|
|
inRelativeCoordinates(point: Vec2): Vec2 {
|
2020-12-21 12:32:32 -05:00
|
|
|
let origin = this.scene.getViewTranslation(this);
|
|
|
|
let zoom = this.scene.getViewScale();
|
|
|
|
|
2021-02-05 13:56:56 -05:00
|
|
|
return point.clone().sub(origin).scale(zoom);
|
2020-12-21 12:32:32 -05:00
|
|
|
}
|
|
|
|
|
2020-10-25 17:46:43 -04:00
|
|
|
/*---------- UNIQUE ----------*/
|
|
|
|
get id(): number {
|
|
|
|
return this._id;
|
|
|
|
}
|
|
|
|
|
|
|
|
set id(id: number) {
|
|
|
|
// id can only be set once
|
|
|
|
if(this._id === undefined){
|
|
|
|
this._id = id;
|
|
|
|
} else {
|
|
|
|
throw "Attempted to assign id to object that already has id."
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*---------- PHYSICAL ----------*/
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-10-25 17:46:43 -04:00
|
|
|
/**
|
|
|
|
* @param velocity The velocity with which to move the object.
|
|
|
|
*/
|
2021-01-05 11:31:17 -05:00
|
|
|
move(velocity: Vec2): void {
|
2020-10-25 17:46:43 -04:00
|
|
|
this.moving = true;
|
|
|
|
this._velocity = velocity;
|
|
|
|
};
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-10-25 17:46:43 -04:00
|
|
|
/**
|
|
|
|
* @param velocity The velocity with which the object will move.
|
|
|
|
*/
|
2021-01-05 11:31:17 -05:00
|
|
|
finishMove(): void {
|
2020-10-25 17:46:43 -04:00
|
|
|
this.moving = false;
|
|
|
|
this.position.add(this._velocity);
|
2020-11-04 14:03:52 -05:00
|
|
|
if(this.pathfinding){
|
|
|
|
this.path.handlePathProgress(this);
|
|
|
|
}
|
2020-08-07 12:21:15 -04:00
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-10-25 17:46:43 -04:00
|
|
|
/**
|
|
|
|
* @param collisionShape The collider for this object. If this has a region (implements Region),
|
|
|
|
* it will be used when no collision shape is specified (or if collision shape is null).
|
|
|
|
* @param isCollidable Whether this is collidable or not. True by default.
|
|
|
|
* @param isStatic Whether this is static or not. False by default
|
|
|
|
*/
|
2021-01-05 11:31:17 -05:00
|
|
|
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void {
|
|
|
|
// Initialize the physics variables
|
2020-10-25 17:46:43 -04:00
|
|
|
this.hasPhysics = true;
|
|
|
|
this.moving = false;
|
|
|
|
this.onGround = false;
|
|
|
|
this.onWall = false;
|
2020-10-28 14:55:32 -04:00
|
|
|
this.onCeiling = false;
|
2020-10-25 17:46:43 -04:00
|
|
|
this.active = true;
|
|
|
|
this.isCollidable = isCollidable;
|
|
|
|
this.isStatic = isStatic;
|
|
|
|
this.isTrigger = false;
|
|
|
|
this.group = "";
|
|
|
|
this.triggers = new Map();
|
|
|
|
this._velocity = Vec2.ZERO;
|
|
|
|
this.sweptRect = new AABB();
|
2020-11-16 11:02:45 -05:00
|
|
|
this.collidedWithTilemap = false;
|
|
|
|
this.physicsLayer = -1;
|
2020-10-25 17:46:43 -04:00
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// Set the collision shape if provided, or simply use the the region if there is one.
|
2020-10-25 17:46:43 -04:00
|
|
|
if(collisionShape){
|
|
|
|
this.collisionShape = collisionShape;
|
2021-02-15 19:44:47 -05:00
|
|
|
this.collisionShape.center = this.position;
|
2020-10-25 17:46:43 -04:00
|
|
|
} else if (isRegion(this)) {
|
|
|
|
// If the gamenode has a region and no other is specified, use that
|
|
|
|
this.collisionShape = (<any>this).boundary.clone();
|
2020-08-10 19:13:42 -04:00
|
|
|
} else {
|
2020-10-25 17:46:43 -04:00
|
|
|
throw "No collision shape specified for physics object."
|
2020-08-10 19:13:42 -04:00
|
|
|
}
|
2020-10-25 17:46:43 -04:00
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// If we were provided with a collider offset, set it. Otherwise there is no offset, so use the zero vector
|
2020-12-22 13:18:10 -05:00
|
|
|
if(colliderOffset){
|
|
|
|
this.colliderOffset = colliderOffset;
|
|
|
|
} else {
|
|
|
|
this.colliderOffset = Vec2.ZERO;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// Initialize the swept rect
|
2020-10-25 17:46:43 -04:00
|
|
|
this.sweptRect = this.collisionShape.getBoundingRect();
|
2021-01-05 11:31:17 -05:00
|
|
|
|
|
|
|
// Register the object with physics
|
2020-10-25 17:46:43 -04:00
|
|
|
this.scene.getPhysicsManager().registerObject(this);
|
|
|
|
}
|
|
|
|
|
2021-02-19 15:35:57 -05:00
|
|
|
/**
|
|
|
|
* Sets the collider for this GameNode
|
|
|
|
* @param collider The new collider to use
|
|
|
|
*/
|
|
|
|
setCollisionShape(collider: Shape): void {
|
|
|
|
this.collisionShape = collider;
|
|
|
|
this.collisionShape.center.copy(this.position);
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-10-25 17:46:43 -04:00
|
|
|
/**
|
|
|
|
* @param group The name of the group that will activate the trigger
|
|
|
|
* @param eventType The type of this event to send when this trigger is activated
|
|
|
|
*/
|
2021-01-05 11:31:17 -05:00
|
|
|
addTrigger(group: string, eventType: string): void {
|
2020-10-25 17:46:43 -04:00
|
|
|
this.isTrigger = true;
|
|
|
|
this.triggers.add(group, eventType);
|
|
|
|
};
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
|
|
|
/**
|
|
|
|
* @param layer The physics layer this node should belong to
|
|
|
|
*/
|
|
|
|
setPhysicsLayer(layer: string): void {
|
2020-11-16 11:02:45 -05:00
|
|
|
this.scene.getPhysicsManager().setLayer(this, layer);
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemened
|
2020-12-22 13:18:10 -05:00
|
|
|
getLastVelocity(): Vec2 {
|
|
|
|
return this._velocity;
|
|
|
|
}
|
|
|
|
|
2020-11-04 14:03:52 -05:00
|
|
|
/*---------- ACTOR ----------*/
|
|
|
|
get ai(): AI {
|
|
|
|
return this._ai;
|
|
|
|
}
|
|
|
|
|
|
|
|
set ai(ai: AI) {
|
|
|
|
if(!this._ai){
|
|
|
|
// If we haven't been previously had an ai, register us with the ai manager
|
|
|
|
this.scene.getAIManager().registerActor(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._ai = ai;
|
|
|
|
this.aiActive = true;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-11-04 14:03:52 -05:00
|
|
|
addAI<T extends AI>(ai: string | (new () => T), options?: Record<string, any>): void {
|
|
|
|
if(!this._ai){
|
|
|
|
this.scene.getAIManager().registerActor(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(typeof ai === "string"){
|
|
|
|
this._ai = this.scene.getAIManager().generateAI(ai);
|
|
|
|
} else {
|
|
|
|
this._ai = new ai();
|
|
|
|
}
|
|
|
|
|
|
|
|
this._ai.initializeAI(this, options);
|
|
|
|
|
|
|
|
this.aiActive = true;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2021-02-19 15:35:57 -05:00
|
|
|
setAIActive(active: boolean, options: Record<string, any>): void {
|
2020-11-04 14:03:52 -05:00
|
|
|
this.aiActive = active;
|
2021-02-19 15:35:57 -05:00
|
|
|
this.ai.activate(options);
|
2020-11-04 14:03:52 -05:00
|
|
|
}
|
|
|
|
|
2020-11-29 19:49:04 -05:00
|
|
|
/*---------- TWEENABLE PROPERTIES ----------*/
|
|
|
|
set positionX(value: number) {
|
|
|
|
this.position.x = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
set positionY(value: number) {
|
|
|
|
this.position.y = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract set scaleX(value: number);
|
|
|
|
|
|
|
|
abstract set scaleY(value: number);
|
|
|
|
|
2020-10-25 17:46:43 -04:00
|
|
|
/*---------- GAME NODE ----------*/
|
|
|
|
/**
|
|
|
|
* Sets the scene for this object.
|
|
|
|
* @param scene The scene this object belongs to.
|
|
|
|
*/
|
|
|
|
setScene(scene: Scene): void {
|
|
|
|
this.scene = scene;
|
2020-08-10 19:13:42 -04:00
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
/**
|
|
|
|
* Gets the scene this object is in.
|
|
|
|
* @returns The scene this object belongs to
|
|
|
|
*/
|
2020-10-25 17:46:43 -04:00
|
|
|
getScene(): Scene {
|
|
|
|
return this.scene;
|
2020-09-28 18:57:02 -04:00
|
|
|
}
|
|
|
|
|
2020-10-25 17:46:43 -04:00
|
|
|
/**
|
|
|
|
* Sets the layer of this object.
|
|
|
|
* @param layer The layer this object will be on.
|
|
|
|
*/
|
|
|
|
setLayer(layer: Layer): void {
|
|
|
|
this.layer = layer;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
/**
|
|
|
|
* Returns the layer this object is on.
|
|
|
|
* @returns This layer this object is on.
|
|
|
|
*/
|
2020-10-25 17:46:43 -04:00
|
|
|
getLayer(): Layer {
|
|
|
|
return this.layer;
|
2020-09-28 18:57:02 -04:00
|
|
|
}
|
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
/** Called if the position vector is modified or replaced */
|
2020-11-18 10:57:55 -05:00
|
|
|
protected positionChanged(): void {
|
2021-02-19 15:35:57 -05:00
|
|
|
if(this.collisionShape){
|
|
|
|
if(this.colliderOffset){
|
|
|
|
this.collisionShape.center = this.position.clone().add(this.colliderOffset);
|
|
|
|
} else {
|
|
|
|
this.collisionShape.center = this.position.clone();
|
|
|
|
}
|
|
|
|
|
2020-10-25 17:46:43 -04:00
|
|
|
}
|
|
|
|
};
|
2020-09-28 18:57:02 -04:00
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
/**
|
|
|
|
* Updates this GameNode
|
|
|
|
* @param deltaT The timestep of the update.
|
|
|
|
*/
|
2020-11-29 19:49:04 -05:00
|
|
|
update(deltaT: number): void {
|
2021-02-05 13:56:56 -05:00
|
|
|
// Defer event handling to AI.
|
|
|
|
while(this.receiver.hasNextEvent()){
|
|
|
|
this._ai.handleEvent(this.receiver.getNextEvent());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update our tweens
|
2020-11-29 19:49:04 -05:00
|
|
|
this.tweens.update(deltaT);
|
|
|
|
}
|
2020-12-21 12:32:32 -05:00
|
|
|
|
2021-01-05 11:31:17 -05:00
|
|
|
// @implemented
|
2020-12-21 12:32:32 -05:00
|
|
|
debugRender(): void {
|
2021-02-05 13:56:56 -05:00
|
|
|
// Draw the position of this GameNode
|
|
|
|
Debug.drawPoint(this.relativePosition, Color.BLUE);
|
2020-12-21 12:32:32 -05:00
|
|
|
|
|
|
|
// If velocity is not zero, draw a vector for it
|
|
|
|
if(this._velocity && !this._velocity.isZero()){
|
2021-02-05 13:56:56 -05:00
|
|
|
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.BLUE);
|
2020-12-22 13:18:10 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// If this has a collider, draw it
|
2021-02-19 15:35:57 -05:00
|
|
|
if(this.collisionShape){
|
2021-02-05 13:56:56 -05:00
|
|
|
let color = this.isColliding ? Color.RED : Color.GREEN;
|
|
|
|
|
|
|
|
if(this.isTrigger){
|
2021-02-19 15:35:57 -05:00
|
|
|
color = Color.MAGENTA;
|
2021-02-05 13:56:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
color.a = 0.2;
|
2021-02-19 15:35:57 -05:00
|
|
|
|
|
|
|
if(this.collisionShape instanceof AABB){
|
|
|
|
Debug.drawBox(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.halfSize.scaled(this.scene.getViewScale()), true, color);
|
|
|
|
} else if(this.collisionShape instanceof Circle){
|
|
|
|
Debug.drawCircle(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.hw*this.scene.getViewScale(), true, color);
|
|
|
|
}
|
2020-12-21 12:32:32 -05:00
|
|
|
}
|
|
|
|
}
|
2020-11-29 19:49:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum TweenableProperties{
|
|
|
|
posX = "positionX",
|
|
|
|
posY = "positionY",
|
|
|
|
scaleX = "scaleX",
|
|
|
|
scaleY = "scaleY",
|
2020-12-01 14:04:24 -05:00
|
|
|
rotation = "rotation",
|
|
|
|
alpha = "alpha"
|
2020-08-07 12:21:15 -04:00
|
|
|
}
|