ShatteredSword/src/Wolfie2D/Nodes/GameNode.ts
ZGrandison a0bace91a0
Origin/new engine features (#2)
* isolate features
2022-02-15 13:57:47 -05:00

480 lines
13 KiB
TypeScript

import Vec2 from "../DataTypes/Vec2";
import Receiver from "../Events/Receiver";
import Emitter from "../Events/Emitter";
import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer";
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";
import Shape from "../DataTypes/Shapes/Shape";
import AABB from "../DataTypes/Shapes/AABB";
import NavigationPath from "../Pathfinding/NavigationPath";
import TweenController from "../Rendering/Animations/TweenController";
import Debug from "../Debug/Debug";
import Color from "../Utils/Color";
import Circle from "../DataTypes/Shapes/Circle";
import GoapAI from "../DataTypes/Interfaces/GoapAI";
/**
* The representation of an object in the game world.
* To construct GameNodes, see the @reference[Scene] documentation.
*/
export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, DebugRenderable {
/*---------- POSITIONED ----------*/
private _position: Vec2;
/*---------- UNIQUE ----------*/
private _id: number;
/*---------- PHYSICAL ----------*/
hasPhysics: boolean = false;
moving: boolean = false;
frozen: boolean = false;
onGround: boolean = false;
onWall: boolean = false;
onCeiling: boolean = false;
active: boolean = false;
collisionShape: Shape;
colliderOffset: Vec2;
isStatic: boolean;
isCollidable: boolean;
isTrigger: boolean;
triggerMask: number;
triggerEnters: Array<string>;
triggerExits: Array<string>;
_velocity: Vec2;
sweptRect: AABB;
collidedWithTilemap: boolean;
group: number;
isPlayer: boolean;
isColliding: boolean = false;
/*---------- ACTOR ----------*/
_ai: AI | GoapAI;
aiActive: boolean;
path: NavigationPath;
pathfinding: boolean = false;
/*---------- GENERAL ----------*/
/** An event receiver. */
protected receiver: Receiver;
/** An event emitter. */
protected emitter: Emitter;
/** A reference to the scene this GameNode is a part of. */
protected scene: Scene;
/** The visual layer this GameNode resides in. */
protected layer: Layer;
/** A utility that allows the use of tweens on this GameNode */
tweens: TweenController;
/** A tweenable property for rotation. Does not affect the bounding box of this GameNode - Only rendering. */
rotation: number;
/** The opacity value of this GameNode */
abstract set alpha(a: number);
abstract get alpha(): number;
// Constructor docs are ignored, as the user should NOT create new GameNodes with a raw constructor
constructor(){
this._position = new Vec2(0, 0);
this._position.setOnChange(() => this.positionChanged());
this.receiver = new Receiver();
this.emitter = new Emitter();
this.tweens = new TweenController(this);
this.rotation = 0;
}
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);
}
/*---------- POSITIONED ----------*/
get position(): Vec2 {
return this._position;
}
set position(pos: Vec2) {
this._position = pos;
this._position.setOnChange(() => this.positionChanged());
this.positionChanged();
}
get relativePosition(): Vec2 {
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 {
let origin = this.scene.getViewTranslation(this);
let zoom = this.scene.getViewScale();
return point.clone().sub(origin).scale(zoom);
}
/*---------- 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 ----------*/
// @implemented
/**
* @param velocity The velocity with which to move the object.
*/
move(velocity: Vec2): void {
if(this.frozen) return;
this.moving = true;
this._velocity = velocity;
};
moveOnPath(speed: number, path: NavigationPath): void {
if(this.frozen) return;
this.path = path;
let dir = path.getMoveDirection(this);
this.moving = true;
this.pathfinding = true;
this._velocity = dir.scale(speed);
}
// @implemented
/**
* @param velocity The velocity with which the object will move.
*/
finishMove(): void {
this.moving = false;
this.position.add(this._velocity);
if(this.pathfinding){
this.path.handlePathProgress(this);
this.path = null;
this.pathfinding = false;
}
}
// @implemented
/**
* @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
*/
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void {
// Initialize the physics variables
this.hasPhysics = true;
this.moving = false;
this.onGround = false;
this.onWall = false;
this.onCeiling = false;
this.active = true;
this.isCollidable = isCollidable;
this.isStatic = isStatic;
this.isTrigger = false;
this.triggerMask = 0;
this.triggerEnters = new Array(32);
this.triggerExits = new Array(32);
this._velocity = Vec2.ZERO;
this.sweptRect = new AABB();
this.collidedWithTilemap = false;
this.group = -1; // The default group, collides with everything
// Set the collision shape if provided, or simply use the the region if there is one.
if(collisionShape){
this.collisionShape = collisionShape;
this.collisionShape.center = this.position;
} else if (isRegion(this)) {
// If the gamenode has a region and no other is specified, use that
this.collisionShape = (<any>this).boundary.clone();
} else {
throw "No collision shape specified for physics object."
}
// If we were provided with a collider offset, set it. Otherwise there is no offset, so use the zero vector
if(colliderOffset){
this.colliderOffset = colliderOffset;
} else {
this.colliderOffset = Vec2.ZERO;
}
// Initialize the swept rect
this.sweptRect = this.collisionShape.getBoundingRect();
// Register the object with physics
this.scene.getPhysicsManager().registerObject(this);
}
/** Removes this object from the physics system */
removePhysics(): void {
// Remove this from the physics manager
this.scene.getPhysicsManager().deregisterObject(this);
// 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;
}
/** Disables physics movement for this node */
freeze(): void {
this.frozen = true;
}
/** Reenables physics movement for this node */
unfreeze(): void {
this.frozen = false;
}
/** 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;
}
/**
* 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);
}
// @implemented
/**
* 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
this.isTrigger = true;
// 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;
};
// @implemented
/**
* @param group The physics group this node should belong to
*/
setGroup(group: string): void {
this.scene.getPhysicsManager().setGroup(this, group);
}
// @implemened
getLastVelocity(): Vec2 {
return this._velocity;
}
/*---------- ACTOR ----------*/
get ai(): AI | GoapAI {
return this._ai;
}
set ai(ai: AI | GoapAI) {
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;
}
// @implemented
addAI<T extends AI | GoapAI>(ai: string | (new () => T), options?: Record<string, any>, type?: number): 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();
}
// 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
this._ai.initializeAI(this, options);
this.aiActive = true;
}
// @implemented
setAIActive(active: boolean, options: Record<string, any>): void {
this.aiActive = active;
if(this.aiActive){
this.ai.activate(options);
}
}
/*---------- 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);
/*---------- GAME NODE ----------*/
/**
* Sets the scene for this object.
* @param scene The scene this object belongs to.
*/
setScene(scene: Scene): void {
this.scene = scene;
}
/**
* Gets the scene this object is in.
* @returns The scene this object belongs to
*/
getScene(): Scene {
return this.scene;
}
/**
* Sets the layer of this object.
* @param layer The layer this object will be on.
*/
setLayer(layer: Layer): void {
this.layer = layer;
}
/**
* Returns the layer this object is on.
* @returns This layer this object is on.
*/
getLayer(): Layer {
return this.layer;
}
/** Called if the position vector is modified or replaced */
protected positionChanged(): void {
if(this.collisionShape){
if(this.colliderOffset){
this.collisionShape.center = this.position.clone().add(this.colliderOffset);
} else {
this.collisionShape.center = this.position.clone();
}
}
};
/**
* Updates this GameNode
* @param deltaT The timestep of the update.
*/
update(deltaT: number): void {
// Defer event handling to AI.
while(this.receiver.hasNextEvent()){
this._ai.handleEvent(this.receiver.getNextEvent());
}
}
// @implemented
debugRender(): void {
// Draw the position of this GameNode
Debug.drawPoint(this.relativePosition, Color.BLUE);
// If velocity is not zero, draw a vector for it
if(this._velocity && !this._velocity.isZero()){
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.BLUE);
}
// If this has a collider, draw it
if(this.collisionShape){
let color = this.isColliding ? Color.RED : Color.GREEN;
if(this.isTrigger){
color = Color.MAGENTA;
}
color.a = 0.2;
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);
}
}
}
}
export enum TweenableProperties{
posX = "positionX",
posY = "positionY",
scaleX = "scaleX",
scaleY = "scaleY",
rotation = "rotation",
alpha = "alpha"
}