@hiddentao/clockwork-engine
Version:
A TypeScript/PIXI.js game engine for deterministic, replayable games with built-in rendering
212 lines (211 loc) • 6.63 kB
JavaScript
import { EventEmitter } from "./EventEmitter";
import { Vector2D } from "./geometry/Vector2D";
export var GameObjectEventType;
(function (GameObjectEventType) {
GameObjectEventType["POSITION_CHANGED"] = "positionChanged";
GameObjectEventType["HEALTH_CHANGED"] = "healthChanged";
GameObjectEventType["MAX_HEALTH_CHANGED"] = "maxHealthChanged";
GameObjectEventType["DESTROYED"] = "destroyed";
GameObjectEventType["SIZE_CHANGED"] = "sizeChanged";
GameObjectEventType["VELOCITY_CHANGED"] = "velocityChanged";
GameObjectEventType["ROTATION_CHANGED"] = "rotationChanged";
})(GameObjectEventType || (GameObjectEventType = {}));
export class GameObject extends EventEmitter {
constructor(id, position, size, health = 0, engine) {
super();
this.isRepaintNeeded = true;
this.id = id;
this.position = position;
this.size = size;
this.velocity = new Vector2D(0, 0);
this.rotation = 0;
this.health = health;
this.maxHealth = health;
this.destroyed = false;
this.engine = engine;
// Auto-register with engine if provided
if (engine) {
engine.registerGameObject(this);
}
if (GameObject.debug) {
// debug: creation log
}
}
update(deltaTicks, _totalTicks) {
if (this.destroyed) {
return;
}
// Move object based on velocity
const movement = this.velocity.scale(deltaTicks);
const oldPosition = this.position;
this.position = this.position.add(movement);
if (oldPosition.x !== this.position.x ||
oldPosition.y !== this.position.y) {
this.isRepaintNeeded = true;
}
}
getPosition() {
return this.position;
}
setPosition(position) {
const oldPosition = this.position;
this.position = position;
if (oldPosition.x !== position.x || oldPosition.y !== position.y) {
this.isRepaintNeeded = true;
}
;
this.emit(GameObjectEventType.POSITION_CHANGED, this, oldPosition, position);
}
getSize() {
return this.size;
}
setSize(size) {
if (this.size.x !== size.x || this.size.y !== size.y) {
this.size = size;
this.isRepaintNeeded = true;
}
}
getVelocity() {
return this.velocity;
}
setVelocity(velocity) {
if (this.velocity.x !== velocity.x || this.velocity.y !== velocity.y) {
this.velocity = velocity;
this.isRepaintNeeded = true;
}
}
getRotation() {
return this.rotation;
}
setRotation(rotation) {
if (this.rotation !== rotation) {
this.rotation = rotation;
this.isRepaintNeeded = true;
}
}
getHealth() {
return this.health;
}
getMaxHealth() {
return this.maxHealth;
}
setMaxHealth(maxHealth) {
const oldMaxHealth = this.maxHealth;
this.maxHealth = maxHealth;
if (this.maxHealth !== oldMaxHealth) {
this.isRepaintNeeded = true;
this.emit(GameObjectEventType.MAX_HEALTH_CHANGED, this, oldMaxHealth, maxHealth);
}
}
setHealth(health) {
const oldHealth = this.health;
this.health = Math.max(0, Math.min(this.maxHealth, health));
if (this.health !== oldHealth) {
this.isRepaintNeeded = true;
this.emit(GameObjectEventType.HEALTH_CHANGED, this, this.health, this.maxHealth);
}
if (this.health === 0) {
this.destroyed = true;
this.emit(GameObjectEventType.DESTROYED, this);
}
}
takeDamage(amount) {
const oldHealth = this.health;
this.health = Math.max(0, this.health - amount);
if (this.health !== oldHealth) {
this.isRepaintNeeded = true;
this.emit(GameObjectEventType.HEALTH_CHANGED, this, this.health, this.maxHealth);
}
if (this.health === 0) {
this.destroyed = true;
this.emit(GameObjectEventType.DESTROYED, this);
}
}
heal(amount) {
const oldHealth = this.health;
this.health = Math.min(this.maxHealth, this.health + amount);
if (this.health !== oldHealth) {
this.isRepaintNeeded = true;
this.emit(GameObjectEventType.HEALTH_CHANGED, this, this.health, this.maxHealth);
}
}
isDestroyed() {
return this.destroyed;
}
destroy() {
this.destroyed = true;
this.isRepaintNeeded = true;
if (GameObject.debug) {
// debug: destroy log
}
;
this.emit(GameObjectEventType.DESTROYED, this);
}
/**
* Get the repaint flag for this game object
* @returns Whether the object needs to be repainted
*/
get needsRepaint() {
return this.isRepaintNeeded;
}
/**
* Set the repaint flag for this game object
* @param value Whether the object needs to be repainted
*/
set needsRepaint(value) {
this.isRepaintNeeded = value;
}
/**
* Get the unique identifier for this game object
* @returns The unique ID of this game object
*/
getId() {
return this.id;
}
/**
* Get the engine this GameObject is registered with
* @returns The game engine instance, or undefined if not registered
*/
getEngine() {
return this.engine;
}
/**
* Register this GameObject with an engine
* @param engine The engine to register with
*/
registerWithEngine(engine) {
this.engine = engine;
engine.registerGameObject(this);
}
/**
* Get the collision source identifier for this game object
* @returns The collision source ID of this game object
*/
getCollisionSourceId() {
return this.getId();
}
serialize() {
return {
position: {
x: this.position.x,
y: this.position.y,
},
size: {
x: this.size.x,
y: this.size.y,
},
velocity: {
x: this.velocity.x,
y: this.velocity.y,
},
rotation: this.rotation,
health: this.health,
maxHealth: this.maxHealth,
isDestroyed: this.destroyed,
};
}
static deserialize(_data) {
throw new Error("GameObject.deserialize must be implemented by subclasses");
}
}
GameObject.debug = false;