@game-vir/entity
Version:
Entity system for game development.
313 lines (312 loc) • 14.4 kB
TypeScript
import { type AnyObject, type ExtractKeysWithMatchingValues, type PartialWithUndefined, type Values } from '@augment-vir/common';
import { System, type Response as Collision, type Body as Hitbox } from 'detect-collisions';
import { type ShapeDefinition } from 'object-shape-tester';
import { type Application, type ViewContainer } from 'pixi.js';
import { type AbstractConstructor, type Constructor, type IsEqual, type IsNever, type WritableKeysOf } from 'type-fest';
import { ListenTarget } from 'typed-event-target';
import { ConstructorMap } from '../constructor-map.js';
/**
* Parameters for {@link EntityStore.addEntity}. Flattens itself to an empty array if there are no
* entity constructor params.
*
* @category Internal
*/
export type AddEntityParams<EntityConstructor extends Constructor<BaseEntity>> = ConstructorParameters<EntityConstructor>[0] extends infer Args extends Pick<EntityConstructorParams<any, any>, 'params'> ? Args['params'] extends undefined ? [] : [Args['params']] : ['ERROR: invalid entity constructor'];
/**
* Parameters for the constructor of {@link EntityStore}.
*
* @category Internal
*/
export type EntityStoreConstructorParams<Context, RegisteredEntities extends Values<EntityStore['entityKeyConstructorMap']>> = (IsNever<Extract<Context, undefined | null>> extends true ? {
context: Context;
} : {
context?: Context;
}) & {
/**
* An [`Application`](https://pixijs.download/release/docs/app.Application.html) instance from
* the [`pixi.js`](https://www.npmjs.com/package/pixi.js) package.
*/
pixi: Application;
/**
* A `System` instance from the
* [`detect-collisions`](https://www.npmjs.com/package/detect-collisions) package. If this
* property is omitted or `undefined`, the {@link EntityStore} instance will create its own.
*/
hitboxSystem?: System | undefined;
/**
* An array of all entity constructors that will be registered with this {@link EntityStore}
* instance. If an entity is added to the store without it being registered here, adding it will
* fail.
*/
registeredEntities: ReadonlyArray<RegisteredEntities>;
};
/**
* Extract all event types from all given entity constructors (in a union).
*
* @category Internal
*/
export type ExtractEntityEvents<Entity extends Constructor<BaseEntity>> = InstanceType<Entity> extends BaseEntity<any, any, infer Events> ? Events : never;
/**
* The top level storage class of all entities. Add entities with {@link EntityStore.addEntity}.
*
* @category Internal
*/
export declare class EntityStore<Context = undefined, const RegisteredEntities extends Values<EntityStore['entityKeyConstructorMap']> = any> {
/**
* All current child entities.
*
* Instead of modifying this set, use {@link EntityStore.addEntity} or
* {@link EntityStore.removeEntity}. If you must manually modify this set directly, you'll also
* need to modify {@link EntityStore.entityInstanceMap}.
*/
readonly currentEntityInstances: Set<BaseEntity<any, any, never>>;
/** If true, this entity store should no longer be used or operated upon. */
readonly isDestroyed: boolean;
/** An internal mapping of all entity constructors to their current instances. */
readonly entityInstanceMap: ConstructorMap;
/** Original pixi app. */
readonly pixi: Application;
/** Context given to all entities. This can be undefined. */
readonly context: Context;
/** Collision detection system. */
readonly hitboxSystem: System;
/** A map of all entity keys to their registered Entity constructors. */
readonly entityKeyConstructorMap: Record<string, Constructor<BaseEntity> & Pick<typeof BaseEntity, 'deserialize' | 'entityKey'>>;
/** Events emitted from any child entities. */
events: ListenTarget<ExtractEntityEvents<RegisteredEntities> | EntityDestroyEvent>;
constructor(args: Readonly<EntityStoreConstructorParams<Context, RegisteredEntities>>);
/**
* Runs `.update()` on all current entities and runs collision detection for all hitboxes. If
* any entities get marked as destroyed during their update, then they will be removed from the
* set of entities.
*
* @returns All detected hitbox collisions (if any).
*/
updateAllEntities(): void;
/** Get all current instances of the given entity class constructor. */
getEntities<T>(entityClassConstructor: AbstractConstructor<T> | Constructor<T>): Set<T>;
/** Remove an entity from the store. */
removeEntity(entity: BaseEntity): void;
/**
* Create an entity instance by finding the registered constructor with the given `entityKey`
* and then deserializing and passing the given `serializedParams` to that constructor.
*/
deserializeEntity<const EntityKey extends RegisteredEntities['entityKey']>(entityKey: EntityKey, serializedParams: string | undefined): InstanceType<Extract<RegisteredEntities, {
entityKey: EntityKey;
}>>;
/** Create a new instance of the given entity class and add it to this entity store. */
addEntity<const EntityConstructor extends RegisteredEntities>(entityClass: EntityConstructor, ...params: AddEntityParams<EntityConstructor>): InstanceType<EntityConstructor>;
/** Destroys the entity store and all entities contained inside it. */
destroy(): void;
}
/**
* Shape definition for {@link EntityPositionParams}.
*
* @category Util
*/
export declare const entityPositionParamsShape: ShapeDefinition<{
x: number;
y: number;
}, false>;
/**
* Base entity serialization. All entities should at least include these properties.
*
* @category Internal
*/
export type EntityPositionParams = typeof entityPositionParamsShape.runtimeType;
/**
* Parameters for an entity's constructor.
*
* @category Internal
*/
export type EntityConstructorParams<Params extends Record<string, any> | undefined = undefined, Context = undefined> = (IsNever<Extract<Context, undefined | null>> extends true ? {
context: Context;
} : {
context?: Context;
}) & (IsNever<Extract<Params, undefined | null>> extends true ? {
params: Params;
} : {
params?: Params;
}) & {
paramsMap?: ParamsMap<NoInfer<Params>> | undefined;
entityStore: EntityStore<Context>;
pixi: Application;
hitboxSystem: System;
};
/**
* Finds all keys from `Params` that match the value at the given `Key` in `OriginalObject`.
*
* @category Internal
*/
export type MatchingKeys<Key extends PropertyKey, Params extends Record<string, any> | undefined, OriginalObject extends AnyObject> = Params extends Record<string, any> ? ExtractKeysWithMatchingValues<Params, Extract<OriginalObject, Record<Key, any>>[Key]> : never;
/**
* An object that controls which entity parameter projects get mapped to view and hitbox properties.
* Values can be:
*
* - `true`: indicates that the property is mapped directly from params to that view or hitbox object
* - Omitted: the property is not mapped at all
* - A string: specifies the params key that this view or hitbox property is mapped to
*/
export type ParamsMap<Params extends Record<string, any> | undefined = AnyObject> = IsEqual<Params, undefined> extends true ? undefined : PartialWithUndefined<{
view: Partial<{
[Key in WritableKeysOf<ViewContainer> as IsNever<MatchingKeys<Key, Params, ViewContainer>> extends true ? never : Key]: (Key extends keyof Params ? Params[Key] extends ViewContainer[Key] ? true : never : never) | MatchingKeys<Key, Params, ViewContainer>;
}>;
hitbox: Partial<{
[Key in WritableKeysOf<Hitbox> as IsNever<MatchingKeys<Key, Params, Hitbox>> extends true ? never : Key]: (Key extends keyof Params ? Params[Key] extends Extract<Hitbox, Record<Key, any>>[Key] ? true : never : never) | MatchingKeys<Key, Params, Hitbox>;
}>;
}>;
declare const EntityDestroyEvent_base: (new (eventInitDict?: EventInit) => import("typed-event-target").TypedEvent<"entity-destroy-event">) & Pick<{
new (type: string, eventInitDict?: EventInit): Event;
prototype: Event;
readonly NONE: 0;
readonly CAPTURING_PHASE: 1;
readonly AT_TARGET: 2;
readonly BUBBLING_PHASE: 3;
}, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedEvent<"entity-destroy-event">, "type">;
/**
* Event emitted by all entities when they are destroyed.
*
* @category Internal
*/
export declare class EntityDestroyEvent extends EntityDestroyEvent_base {
}
/**
* Default value for the optional {@link ParamsMap}.
*
* @category Internal
*/
export declare const defaultParamsMap: ParamsMap;
/**
* Type for {@link BaseEntity.reverseParamsMap}.
*
* @category Internal
*/
export type ReverseParamsMap = Record<string, Partial<Record<'hitbox' | 'view', string[]>>>;
/**
* Base entity class, types, and functionality.
*
* @category Internal
*/
export declare abstract class BaseEntity<Context = any, Params extends Record<string, any> | undefined = any, Events extends Readonly<Event> = never> {
/**
* This key is used for deserialization of entities to track which class needs to be
* constructed. You cannot have duplicate keys loaded at the same time.
*/
static readonly entityKey: string;
/** Shape definition of this entity's parameters. */
static readonly paramsShape: ShapeDefinition<AnyObject, any> | undefined;
/**
* Defines which properties from {@link BaseEntity.params} will be mapped to hitbox and/or view
* properties.
*/
static readonly paramsMap: ParamsMap | undefined;
/**
* A mapping from params properties to hitbox or view properties, making it easy to map params
* values.
*/
static readonly reverseParamsMap: ReverseParamsMap | undefined;
/** Parses the serialized params generated by {@link BaseEntity.serialize}. */
static deserialize(serialized: string | undefined): AnyObject | undefined;
/**
* Dispatch an event. This will be dispatched through this entity's entity store (so listen for
* the event on the store).
*/
dispatch(event: Events | EntityDestroyEvent): void;
/** If true, this entity should no longer be used or operated upon. */
readonly isDestroyed: boolean;
hitbox: Hitbox<this> | undefined;
/** The entity store to add all entities to. */
readonly entityStore: EntityStore<Context>;
readonly context: Context;
/** Writable entity params. These should be serializable. */
readonly params: Params;
/** Original pixi app. */
readonly pixi: Application;
/** Collision detection system. */
readonly hitboxSystem: System;
constructor(args: Readonly<EntityConstructorParams<NoInfer<Params>, NoInfer<Context>>>);
/**
* Called every game tick. Run all entity updates in here. This should be overridden in all
* entity definition classes.
*/
abstract update(): void;
/** Add a new entity to the entity store. */
addEntity<const EntityConstructor extends Constructor<BaseEntity>>(entityClass: EntityConstructor, ...params: AddEntityParams<EntityConstructor>): InstanceType<EntityConstructor>;
/** Marks the entity for destruction in the next entity store update. */
destroy(): void;
/**
* Immediately destroy the current entity, stop its updates.
*
* This is probably not what you want to use! See {@link BaseEntity.destroy} instead.
*/
immediatelyDestroy(): void;
/**
* This method is call whenever this entity's hitbox (if it has one) collides with another
* hitbox. It will be called for each individual collision. Override this to do something about
* it.
*/
collide(otherEntity: BaseEntity, collision: Readonly<Collision>): void;
/**
* Serialize the entity params for sharing across the network (for multiplayer play). By default
* this simply calls `JSON.stringify` on `this.params`. This method must be overridden if your
* entity has params that are not JSON compatible.
*/
serialize(): string | undefined;
}
/**
* Output of {@link ViewEntity.createView}.
*
* @category Internal
*/
export type ViewCreation = {
/**
* A view for rendering. Create with, for example, [`new
* AnimatedSprite`](https://pixijs.download/release/docs/scene.AnimatedSprite.html) or [`new
* Graphics`](https://pixijs.download/release/docs/scene.Graphics.html), etc. imported from the
* [`pixi.js`](https://www.npmjs.com/package/pixi.js) package.
*/
view?: ViewContainer | undefined;
/**
* A Body instance for hitbox collision detection. Create one with, for example,
* `this.hitboxSystem.createBox()` or import directly from the
* [`detect-collisions`](https://www.npmjs.com/package/detect-collisions) package, like with
* [`new Circle`](https://prozi.github.io/detect-collisions/classes/Circle.html#constructor).
*
* This property optional, if a hitbox is not provided, collision detection will not be
* calculated for this entity.
*/
hitbox?: Hitbox | undefined;
};
/**
* Base view entity class, types, and functionality.
*
* @category Internal
*/
export declare abstract class ViewEntity<Context = any, Params extends Record<string, any> | undefined = any, Events extends Readonly<Event> = never> extends BaseEntity<Context, Params, Events> {
/** The entity's PixiJS view. */
view: ViewContainer;
constructor(args: Readonly<EntityConstructorParams<NoInfer<Params>, NoInfer<Context>>>);
private wrapParamsInProxy;
/**
* Creates the entity's PixiJS view. This will be called on entity construction and added to the
* PixiJS application stage.
*/
abstract createView(): ViewCreation;
/** Detects if the current entity is still within the bounds of the render canvas. */
isInBounds(options?: PartialWithUndefined<{
/**
* If `true`, the entire entity's bounds must be within the canvas's bounds. If `false`,
* any portion of the entity being within the canvas bounds is counted.
*
* @default false
*/
entirely?: boolean;
}>): boolean;
/**
* Immediately destroy the current entity, stop its updates, and remove it from the view.
*
* This is probably not what you want to use! See {@link BaseEntity.destroy} instead.
*/
immediatelyDestroy(): void;
}
export {};