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 AABB from "../DataTypes/Shapes/AABB" ;
2020-11-04 14:03:52 -05:00
import NavigationPath from "../Pathfinding/NavigationPath" ;
2021-03-18 17:28:05 -04:00
import TweenController from "../Rendering/Animations/TweenController" ;
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" ;
2022-02-15 13:57:47 -05:00
import GoapAI from "../DataTypes/Interfaces/GoapAI" ;
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 ;
2021-03-19 15:44:11 -04:00
frozen : boolean = false ;
2021-02-05 13:56:56 -05:00
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 ;
2021-03-18 17:28:05 -04:00
triggerMask : number ;
triggerEnters : Array < string > ;
triggerExits : Array < string > ;
2020-10-25 17:46:43 -04:00
_velocity : Vec2 ;
sweptRect : AABB ;
2020-11-16 11:02:45 -05:00
collidedWithTilemap : boolean ;
2021-03-18 17:28:05 -04:00
group : 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 ----------*/
2022-02-15 13:57:47 -05:00
_ai : AI | GoapAI ;
2020-11-04 14:03:52 -05:00
aiActive : boolean ;
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 */
2021-03-18 17:28:05 -04:00
tweens : TweenController ;
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 */
2021-03-18 17:28:05 -04:00
abstract set alpha ( a : number ) ;
abstract get 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 ( ) ;
2021-03-18 17:28:05 -04:00
this . tweens = new TweenController ( this ) ;
2020-11-29 19:49:04 -05:00
this . rotation = 0 ;
2021-03-18 17:28:05 -04:00
}
destroy ( ) {
this . tweens . destroy ( ) ;
this . receiver . destroy ( ) ;
if ( this . hasPhysics ) {
this . removePhysics ( ) ;
}
if ( this . _ai ) {
this . _ai . destroy ( ) ;
delete this . _ai ;
this . scene . getAIManager ( ) . removeActor ( this ) ;
}
this . scene . remove ( this ) ;
this . layer . removeNode ( this ) ;
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 {
2021-03-19 15:44:11 -04:00
if ( this . frozen ) return ;
2020-10-25 17:46:43 -04:00
this . moving = true ;
this . _velocity = velocity ;
} ;
2021-03-04 19:10:41 -05:00
moveOnPath ( speed : number , path : NavigationPath ) : void {
2021-03-19 15:44:11 -04:00
if ( this . frozen ) return ;
2021-03-04 19:10:41 -05:00
this . path = path ;
let dir = path . getMoveDirection ( this ) ;
this . moving = true ;
this . pathfinding = true ;
this . _velocity = dir . scale ( speed ) ;
}
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 ) ;
2021-03-04 19:10:41 -05:00
this . path = null ;
this . pathfinding = false ;
2020-11-04 14:03:52 -05:00
}
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 ;
2021-03-18 17:28:05 -04:00
this . triggerMask = 0 ;
this . triggerEnters = new Array ( 32 ) ;
this . triggerExits = new Array ( 32 ) ;
2020-10-25 17:46:43 -04:00
this . _velocity = Vec2 . ZERO ;
this . sweptRect = new AABB ( ) ;
2020-11-16 11:02:45 -05:00
this . collidedWithTilemap = false ;
2021-03-18 17:28:05 -04:00
this . group = - 1 ; // The default group, collides with everything
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-03-18 17:28:05 -04:00
/** Removes this object from the physics system */
removePhysics ( ) : void {
2021-03-19 15:44:11 -04:00
// Remove this from the physics manager
this . scene . getPhysicsManager ( ) . deregisterObject ( this ) ;
2021-03-18 17:28:05 -04:00
// Nullify all physics fields
this . hasPhysics = false ;
this . moving = false ;
this . onGround = false ;
this . onWall = false ;
this . onCeiling = false ;
this . active = false ;
this . isCollidable = false ;
this . isStatic = false ;
this . isTrigger = false ;
this . triggerMask = 0 ;
this . triggerEnters = null ;
this . triggerExits = null ;
this . _velocity = Vec2 . ZERO ;
this . sweptRect = null ;
this . collidedWithTilemap = false ;
this . group = - 1 ;
this . collisionShape = null ;
this . colliderOffset = Vec2 . ZERO ;
this . sweptRect = null ;
2021-03-19 15:44:11 -04:00
}
2021-03-18 17:28:05 -04:00
2021-03-19 15:44:11 -04:00
/** Disables physics movement for this node */
freeze ( ) : void {
this . frozen = true ;
}
/** Reenables physics movement for this node */
unfreeze ( ) : void {
this . frozen = false ;
2021-03-18 17:28:05 -04:00
}
/** Prevents this object from participating in all collisions and triggers. It can still move. */
disablePhysics ( ) : void {
this . active = false ;
}
/** Enables this object to participate in collisions and triggers. This is only necessary if disablePhysics was called */
enablePhysics ( ) : void {
this . active = true ;
}
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
/ * *
2021-03-18 17:28:05 -04:00
* Sets this object to be a trigger for a specific group
* @param group The name of the group that activates the trigger
* @param onEnter The name of the event to send when this trigger is activated
* @param onExit The name of the event to send when this trigger stops being activated
* /
setTrigger ( group : string , onEnter : string , onExit : string ) : void {
// Make this object a trigger
2020-10-25 17:46:43 -04:00
this . isTrigger = true ;
2021-03-18 17:28:05 -04:00
// Get the number of the physics layer
let layerNumber = this . scene . getPhysicsManager ( ) . getGroupNumber ( group ) ;
if ( layerNumber === 0 ) {
console . warn ( ` Trigger for GameNode ${ this . id } not set - group " ${ group } " was not recognized by the physics manager. ` ) ;
return ;
}
// Add this to the trigger mask
this . triggerMask |= layerNumber ;
// Layer numbers are bits, so get which bit it is
let index = Math . log2 ( layerNumber ) ;
// Set the event names
this . triggerEnters [ index ] = onEnter ;
this . triggerExits [ index ] = onExit ;
2020-10-25 17:46:43 -04:00
} ;
2021-01-05 11:31:17 -05:00
// @implemented
/ * *
2021-03-18 17:28:05 -04:00
* @param group The physics group this node should belong to
2021-01-05 11:31:17 -05:00
* /
2021-03-18 17:28:05 -04:00
setGroup ( group : string ) : void {
this . scene . getPhysicsManager ( ) . setGroup ( this , group ) ;
2020-11-16 11:02:45 -05:00
}
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 ----------*/
2022-02-15 13:57:47 -05:00
get ai ( ) : AI | GoapAI {
2020-11-04 14:03:52 -05:00
return this . _ai ;
}
2022-02-15 13:57:47 -05:00
set ai ( ai : AI | GoapAI ) {
2020-11-04 14:03:52 -05:00
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
2022-02-15 13:57:47 -05:00
addAI < T extends AI | GoapAI > ( ai : string | ( new ( ) = > T ) , options? : Record < string , any > , type ? : number ) : void {
2020-11-04 14:03:52 -05:00
if ( ! this . _ai ) {
this . scene . getAIManager ( ) . registerActor ( this ) ;
}
if ( typeof ai === "string" ) {
this . _ai = this . scene . getAIManager ( ) . generateAI ( ai ) ;
} else {
this . _ai = new ai ( ) ;
}
2022-02-15 13:57:47 -05:00
// Question, how much do we want different type of AI to be handled the same, i.e. should GoapAI and AI similar methods and signatures for the sake of unity
2020-11-04 14:03:52 -05:00
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-03-04 19:10:41 -05:00
if ( this . aiActive ) {
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 ( ) ) ;
}
2020-11-29 19:49:04 -05:00
}
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
}