Skip to content

Commit

Permalink
Initial commit, added base classes
Browse files Browse the repository at this point in the history
  • Loading branch information
james-pre committed Aug 23, 2024
1 parent d374273 commit 30332d6
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 1 deletion.
31 changes: 30 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,11 @@
"typedoc-plugin-remove-references": "^0.0.6",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0"
},
"dependencies": {
"@babylonjs/core": "^7.22.1",
"eventemitter3": "^5.0.1",
"logzen": "^0.3.8",
"utilium": "^0.5.6"
}
}
27 changes: 27 additions & 0 deletions src/core/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Level } from './level.js';
import { logger } from './utils.js';

export interface Component<TJSON = unknown> {
readonly id?: string;

update?(): void;

toJSON(): TJSON;

fromJSON(data: TJSON): void;
}

export type ComponentData<T extends Component> = T extends Component<infer TJSON> ? TJSON : never;

export interface ComponentStatic<T extends Component = Component> {
name: string;

FromJSON(data: ComponentData<T>, level?: Level): T;
}

export const components = new Map<string, ComponentStatic>();

export function component<Class extends ComponentStatic>(target: Class) {
logger.debug('Registered component: ' + target.name);
components.set(target.name, target);
}
186 changes: 186 additions & 0 deletions src/core/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { IVector3Like } from '@babylonjs/core/Maths/math.like.js';
import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
import { EventEmitter } from 'eventemitter3';
import { assignWithDefaults, pick, randomHex, resolveConstructors } from 'utilium';
import { component, type Component } from './component.js';
import type { Level } from './level.js';
import { findPath } from './path.js';

export interface EntityJSON {
id: string;
name: string;
owner?: string;
parent?: string;
entityType: string;
position: [number, number, number];
rotation: [number, number, number];
velocity: [number, number, number];
}

const copy = ['id', 'name', 'entityType'] as const satisfies ReadonlyArray<keyof Entity>;

@component
export class Entity
extends EventEmitter<{
update: [];
created: [];
}>
implements Component<EntityJSON>
{
public get [Symbol.toStringTag](): string {
return this.constructor.name;
}

public name: string = '';

public get entityType(): string {
return this.constructor.name;
}

public get entityTypes(): string[] {
return resolveConstructors(this);
}

public isType<T extends Entity>(...types: string[]): this is T {
return types.some(type => this.entityTypes.includes(type));
}

public parent?: Entity;

protected _owner?: Entity;
public get owner(): Entity | undefined {
return this._owner;
}

public set owner(value: Entity | undefined) {
this._owner = value;
}

/**
* Used by path finding to check for collisions
* @internal
*/
public _pathRadius: number = 1;

public position: Vector3 = Vector3.Zero();
public rotation: Vector3 = Vector3.Zero();
public velocity: Vector3 = Vector3.Zero();

public get absolutePosition(): Vector3 {
return this.parent instanceof Entity ? this.parent.absolutePosition.add(this.position) : this.position;
}

public get absoluteRotation(): Vector3 {
return this.parent instanceof Entity ? this.parent.absoluteRotation.add(this.rotation) : this.rotation;
}

public get absoluteVelocity(): Vector3 {
return this.parent instanceof Entity ? this.parent.absoluteVelocity.add(this.rotation) : this.rotation;
}

public constructor(
public id: string = randomHex(32),
public readonly level: Level
) {
super();
this.id ||= randomHex(32);
level.entities.add(this);

setTimeout(() => this.emit('created'));
}

public update() {
if (Math.abs(this.rotation.y) > Math.PI) {
this.rotation.y += Math.sign(this.rotation.y) * 2 * Math.PI;
}

this.position.addInPlace(this.velocity);
this.emit('update');
}

public remove() {
this.level.entities.delete(this);
this.level.emit('entity_removed', this.toJSON());
}

/**
* @param target The position the entity should move to
* @param isRelative Wheter the target is a change to the current position (i.e. a "delta" vector) or absolute
*/
public async moveTo(target: IVector3Like, isRelative = false) {
if (!(target instanceof Vector3)) throw new TypeError('target must be a Vector3');
const path = findPath(this.absolutePosition, target.add(isRelative ? this.absolutePosition : Vector3.Zero()));
if (!path.length) {
return;
}
this.level.emit(
'entity_path_start',
this.id,
path.map(({ x, y, z }) => ({ x, y, z }))
);
this.position = path.at(-1)!.subtract(this.parent?.absolutePosition || Vector3.Zero());
const rotation = Vector3.PitchYawRollToMoveBetweenPoints(path.at(-2)!, path.at(-1)!);
rotation.x -= Math.PI / 2;
this.rotation = rotation;
}

public toJSON(): EntityJSON {
return {
...pick(this, copy),
owner: this.owner?.id,
parent: this.parent?.id,
position: this.position.asArray(),
rotation: this.rotation.asArray(),
velocity: this.velocity.asArray(),
};
}

public fromJSON(data: Partial<EntityJSON>): void {
assignWithDefaults(this as Entity, {
...pick(data, copy),
position: data.position && Vector3.FromArray(data.position),
rotation: data.rotation && Vector3.FromArray(data.rotation),
velocity: data.velocity && Vector3.FromArray(data.velocity),
parent: data.parent ? this.level.getEntityByID(data.parent) : undefined,
owner: data.owner ? this.level.getEntityByID(data.owner) : undefined,
});
}

public static FromJSON(data: Partial<EntityJSON>, level: Level): Entity {
const entity = new this(data.id, level);
entity.fromJSON(data);
return entity;
}
}

export function filterEntities(entities: Iterable<Entity>, selector: string): Set<Entity> {
if (typeof selector != 'string') {
throw new TypeError('selector must be of type string');
}

if (selector == '*') {
return new Set(entities);
}

const selected = new Set<Entity>();
for (const entity of entities) {
switch (selector[0]) {
case '@':
if (entity.name == selector.slice(1)) selected.add(entity);
break;
case '#':
if (entity.id == selector.slice(1)) selected.add(entity);
break;
case '.':
for (const type of entity.entityTypes) {
if (type.toLowerCase().includes(selector.slice(1).toLowerCase())) {
selected.add(entity);
}
}
break;
default:
throw 'Invalid selector';
}
}
return selected;
}
Loading

0 comments on commit 30332d6

Please sign in to comment.