UNPKG

@game-vir/entity

Version:

Entity system for game development.

313 lines (312 loc) 14.4 kB
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 {};