@antha/entity-2d 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
1
  import { type AnyObject, type Constructor, type IsAny } from '@augment-vir/common';
2
2
  import { type Shape } from 'object-shape-tester';
3
- import { BaseEntity2d, type BaseEntityAssetDefinitions, type Entity2dConstructorParams, EntityStore2d, type EntityStore2dConstructorParams, type MappedEntityAssets, type ParamsMap, type ReverseParamsMap, ViewEntity2d } from './entity.js';
3
+ import { BaseEntity2d, type BaseEntityAssetDefinitions, type Entity2dConstructor, type Entity2dConstructorParams, type EntityCollisionDefinition, EntityStore2d, type EntityStore2dConstructorParams, type MappedEntityAssets, type ParamsMap, type ReverseParamsMap, ViewEntity2d } from './entity.js';
4
4
  /**
5
5
  * Params for both {@link EntitySuite2d.defineEntity} and {@link EntitySuite2d.defineLogicEntity}.
6
6
  *
7
7
  * @category Internal
8
8
  */
9
9
  export type DefineEntity2dArgs<ParamsShape extends Shape<AnyObject> | undefined, EntityAssets extends BaseEntityAssetDefinitions | undefined> = {
10
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
11
+ collidesWith?: EntityCollisionDefinition | undefined;
10
12
  /**
11
13
  * This key is used for deserialization of entities to track which class needs to be
12
14
  * constructed. Do not use duplicate key strings across multiple entity classes.
@@ -96,6 +98,10 @@ export type DefineEntity2dArgs<ParamsShape extends Shape<AnyObject> | undefined,
96
98
  * @category Internal
97
99
  */
98
100
  export type StaticEntity2dParts<State extends AnyObject = any, ParamsShape extends Shape<AnyObject> | undefined = any, EntityAssets extends BaseEntityAssetDefinitions | undefined = any> = {
101
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
102
+ collidesWith: EntityCollisionDefinition | undefined;
103
+ /** Cached entity classes this entity observes collisions with. */
104
+ collidesWithSet: ReadonlySet<Entity2dConstructor>;
99
105
  /**
100
106
  * This key is used for deserialization of entities to track which class needs to be
101
107
  * constructed. You cannot have duplicate keys loaded at the same time.
@@ -13,7 +13,7 @@ export function defineEntitySuite2d() {
13
13
  return (params) => {
14
14
  if (params.assets) {
15
15
  getObjectTypedEntries(params.assets).forEach(([key, rawAsset,]) => {
16
- rawAsset.name = [
16
+ rawAsset.assetName = [
17
17
  params.key,
18
18
  key,
19
19
  ].join(':');
@@ -22,7 +22,7 @@ export function defineEntitySuite2d() {
22
22
  return defineEntity(entityParent, params);
23
23
  };
24
24
  }
25
- function defineEntity(entityParent, { key, paramsShape, paramsMap, assets }) {
25
+ function defineEntity(entityParent, { collidesWith, key, paramsShape, paramsMap, assets }) {
26
26
  if (entityKeys.has(key)) {
27
27
  throw new Error(`Entity key '${key}' has already been attached to an entity class.`);
28
28
  }
@@ -30,9 +30,11 @@ export function defineEntitySuite2d() {
30
30
  const classWrapper = {
31
31
  // @ts-expect-error: abstract methods are intentionally not implemented here
32
32
  [key]: class extends entityParent {
33
+ static collidesWith = collidesWith;
34
+ static collidesWithSet = new Set(collidesWith?.collidesWithOtherEntities);
33
35
  static entityKey = key;
34
36
  static paramsShape = paramsShape;
35
- static assets = assets;
37
+ static assets = assets || {};
36
38
  static paramsMap = paramsMap;
37
39
  static reverseParamsMap = reverseParamsMap(paramsMap);
38
40
  },
package/dist/entity.d.ts CHANGED
@@ -13,8 +13,8 @@ export { System as HitboxSystem, type Response as Collision } from 'detect-colli
13
13
  * @category Internal
14
14
  */
15
15
  export type BaseEntityAssetDefinitions = Record<string, Omit<Asset,
16
- /** Name will be the value's key in this object. */
17
- 'name'>>;
16
+ /** Asset names are derived from the entity and asset keys. */
17
+ 'assetName'>>;
18
18
  /**
19
19
  * A record of `Asset` entries.
20
20
  *
@@ -28,7 +28,7 @@ export type BaseEntityAssets = Record<string, Asset>;
28
28
  */
29
29
  export type MappedEntityAssets<Definitions extends BaseEntityAssetDefinitions | undefined> = Definitions extends undefined ? EmptyObject : {
30
30
  [Key in keyof Definitions]: Definitions[Key] & {
31
- name: string;
31
+ assetName: string;
32
32
  };
33
33
  };
34
34
  /**
@@ -72,7 +72,31 @@ export type EntityStore2dConstructorParams<State extends AnyObject = any> = {
72
72
  *
73
73
  * @category Internal
74
74
  */
75
- export type Entity2dConstructor = Constructor<BaseEntity2d> & StaticEntity2dParts;
75
+ export type Entity2dConstructor = Constructor<BaseEntity2d> & StaticEntity2dParts<any, any, BaseEntityAssetDefinitions>;
76
+ /**
77
+ * Defines which entity classes this entity observes collisions with.
78
+ *
79
+ * @category Internal
80
+ */
81
+ export type EntityCollisionDefinition = PartialWithUndefined<{
82
+ /** Whether this entity observes collisions with instances of its own class. */
83
+ collidesWithSelf: boolean;
84
+ /** Other entity classes this entity observes collisions with. */
85
+ collidesWithOtherEntities: ReadonlyArray<Entity2dConstructor>;
86
+ }>;
87
+ /** Loads assets declared by the given entity classes. */
88
+ export declare function loadEntityAssets({ assetLoader, entities, otherAssets, }: Readonly<{
89
+ assetLoader: AssetLoader;
90
+ entities: ReadonlyArray<Entity2dConstructor>;
91
+ otherAssets?: ReadonlyArray<Readonly<Asset>> | undefined;
92
+ }>, options?: Readonly<AssetBulkLoaderLoadOptions> | undefined): Promise<readonly unknown[]>;
93
+ /** A collision system that skips pairs no entity observes before running SAT collision checks. */
94
+ export declare class EntityHitboxSystem extends HitboxSystem {
95
+ protected checkedHitboxPairs: WeakMap<Hitbox, WeakSet<Hitbox>> | undefined;
96
+ checkAll(...[callback, response,]: Parameters<HitboxSystem['checkAll']>): any;
97
+ checkOne(...[hitbox, callback, response,]: Parameters<HitboxSystem['checkOne']>): boolean;
98
+ checkCollision(...hitboxes: Parameters<HitboxSystem['checkCollision']>): boolean;
99
+ }
76
100
  /**
77
101
  * The top level storage class of all entities. Add entities with {@link EntityStore2d.addEntity}.
78
102
  *
@@ -102,14 +126,6 @@ export declare class EntityStore2d<State extends AnyObject = any> {
102
126
  readonly state: State;
103
127
  readonly assetLoader: AssetLoader;
104
128
  constructor(args: Readonly<EntityStore2dConstructorParams>);
105
- /**
106
- * Load all the assets for all the given entities. Without this, assets will be loaded on demand
107
- * only.
108
- */
109
- loadEntityAssets({ entities, otherAssets, }: Readonly<{
110
- entities: ReadonlyArray<Entity2dConstructor>;
111
- otherAssets?: ReadonlyArray<Readonly<Asset>> | undefined;
112
- }>, options?: Readonly<AssetBulkLoaderLoadOptions> | undefined): Promise<readonly unknown[]>;
113
129
  /**
114
130
  * Register a set of entities so that they can be deserialized (for example, when transferring
115
131
  * game state in across the network for multilayer).
@@ -299,6 +315,10 @@ export declare abstract class BaseEntity2d<State extends AnyObject = any, Params
299
315
  * constructed. You cannot have duplicate keys loaded at the same time.
300
316
  */
301
317
  static readonly entityKey: string;
318
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
319
+ static readonly collidesWith: EntityCollisionDefinition | undefined;
320
+ /** Cached entity classes this entity observes collisions with. */
321
+ static readonly collidesWithSet: ReadonlySet<Entity2dConstructor>;
302
322
  /** Shape definition of this entity's parameters. */
303
323
  static readonly paramsShape: Shape<AnyObject> | undefined;
304
324
  static readonly assets: MappedEntityAssets<BaseEntityAssetDefinitions | undefined> | undefined;
@@ -321,6 +341,8 @@ export declare abstract class BaseEntity2d<State extends AnyObject = any, Params
321
341
  dispatch(event: EntityEvent | EntityDestroyEvent): void;
322
342
  /** If true, this entity should no longer be used or operated upon. */
323
343
  readonly isDestroyed: boolean;
344
+ /** Static definition used to construct this entity. */
345
+ readonly entityDefinition: Entity2dConstructor;
324
346
  protected readonly abortController: AbortController;
325
347
  /** An `AbortSignal` that triggers when the entity is destroyed. */
326
348
  readonly abortSignal: AbortSignal;
package/dist/entity.js CHANGED
@@ -1,10 +1,129 @@
1
1
  import { assert, check } from '@augment-vir/assert';
2
- import { ConstructorInstanceMap, getObjectTypedEntries, makeWritable, mapObjectValues, } from '@augment-vir/common';
3
- import { System as HitboxSystem, } from 'detect-collisions';
2
+ import { ConstructorInstanceMap, getObjectTypedEntries, getObjectTypedValues, makeWritable, mapObjectValues, } from '@augment-vir/common';
3
+ import { System as HitboxSystem, Response, } from 'detect-collisions';
4
4
  import { assertValidShape, defineShape } from 'object-shape-tester';
5
5
  import { ParticleContainer } from 'pixi.js';
6
6
  import { defineTypedCustomEvent, GenericListenTarget } from 'typed-event-target';
7
7
  export { System as HitboxSystem } from 'detect-collisions';
8
+ /** Loads assets declared by the given entity classes. */
9
+ export async function loadEntityAssets({ assetLoader, entities, otherAssets, }, options) {
10
+ const assets = [
11
+ ...(otherAssets || []),
12
+ ...entities.flatMap((entity) => {
13
+ return getObjectTypedValues(entity.assets);
14
+ }),
15
+ ];
16
+ return await assetLoader.bulkLoadAssets(assets, options);
17
+ }
18
+ function extractEntityFromHitbox(hitbox) {
19
+ return hitbox.userData instanceof BaseEntity2d ? hitbox.userData : undefined;
20
+ }
21
+ function hasCollisionTargets(entity) {
22
+ return (!!entity.entityDefinition.collidesWith?.collidesWithSelf ||
23
+ entity.entityDefinition.collidesWithSet.size > 0);
24
+ }
25
+ function doesEntityCollideWith({ entity, otherEntity, }) {
26
+ return ((entity.entityDefinition.collidesWith?.collidesWithSelf &&
27
+ entity.entityDefinition === otherEntity.entityDefinition) ||
28
+ entity.entityDefinition.collidesWithSet.has(otherEntity.entityDefinition));
29
+ }
30
+ function shouldCheckEntityCollision({ firstEntity, secondEntity, }) {
31
+ return (doesEntityCollideWith({
32
+ entity: firstEntity,
33
+ otherEntity: secondEntity,
34
+ }) ||
35
+ doesEntityCollideWith({
36
+ entity: secondEntity,
37
+ otherEntity: firstEntity,
38
+ }));
39
+ }
40
+ function shouldNotifyEntityOfCollision({ entity, otherEntity, }) {
41
+ return doesEntityCollideWith({
42
+ entity,
43
+ otherEntity,
44
+ });
45
+ }
46
+ function createReversedCollision(collision) {
47
+ const reversedCollision = new Response();
48
+ reversedCollision.a = collision.b;
49
+ reversedCollision.aInB = collision.bInA;
50
+ reversedCollision.b = collision.a;
51
+ reversedCollision.bInA = collision.aInB;
52
+ reversedCollision.overlap = collision.overlap;
53
+ reversedCollision.overlapN.x = -collision.overlapN.x;
54
+ reversedCollision.overlapN.y = -collision.overlapN.y;
55
+ reversedCollision.overlapV.x = -collision.overlapV.x;
56
+ reversedCollision.overlapV.y = -collision.overlapV.y;
57
+ return reversedCollision;
58
+ }
59
+ function hasAlreadyCheckedHitboxPair({ checkedHitboxPairs, firstHitbox, secondHitbox, }) {
60
+ return (checkedHitboxPairs.get(firstHitbox)?.has(secondHitbox) ||
61
+ checkedHitboxPairs.get(secondHitbox)?.has(firstHitbox));
62
+ }
63
+ function markHitboxPairAsChecked({ checkedHitboxPairs, firstHitbox, secondHitbox, }) {
64
+ const firstHitboxPairs = checkedHitboxPairs.get(firstHitbox);
65
+ if (firstHitboxPairs) {
66
+ firstHitboxPairs.add(secondHitbox);
67
+ }
68
+ else {
69
+ checkedHitboxPairs.set(firstHitbox, new WeakSet([secondHitbox]));
70
+ }
71
+ }
72
+ /** A collision system that skips pairs no entity observes before running SAT collision checks. */
73
+ export class EntityHitboxSystem extends HitboxSystem {
74
+ checkedHitboxPairs;
75
+ checkAll(...[callback, response,]) {
76
+ const previousCheckedHitboxPairs = this.checkedHitboxPairs;
77
+ this.checkedHitboxPairs = new WeakMap();
78
+ try {
79
+ return this.all().some((hitbox) => {
80
+ const entity = extractEntityFromHitbox(hitbox);
81
+ return ((!entity || hasCollisionTargets(entity)) &&
82
+ this.checkOne(hitbox, callback, response));
83
+ });
84
+ }
85
+ finally {
86
+ this.checkedHitboxPairs = previousCheckedHitboxPairs;
87
+ }
88
+ }
89
+ checkOne(...[hitbox, callback, response,]) {
90
+ const entity = extractEntityFromHitbox(hitbox);
91
+ if (entity && !hasCollisionTargets(entity)) {
92
+ return false;
93
+ }
94
+ return super.checkOne(hitbox, callback, response);
95
+ }
96
+ checkCollision(...hitboxes) {
97
+ const [firstHitbox, secondHitbox,] = hitboxes;
98
+ const checkedHitboxPairs = this.checkedHitboxPairs;
99
+ if (checkedHitboxPairs &&
100
+ hasAlreadyCheckedHitboxPair({
101
+ checkedHitboxPairs,
102
+ firstHitbox,
103
+ secondHitbox,
104
+ })) {
105
+ return false;
106
+ }
107
+ if (checkedHitboxPairs) {
108
+ markHitboxPairAsChecked({
109
+ checkedHitboxPairs,
110
+ firstHitbox,
111
+ secondHitbox,
112
+ });
113
+ }
114
+ const firstEntity = extractEntityFromHitbox(firstHitbox);
115
+ const secondEntity = extractEntityFromHitbox(secondHitbox);
116
+ if (firstEntity &&
117
+ secondEntity &&
118
+ !shouldCheckEntityCollision({
119
+ firstEntity,
120
+ secondEntity,
121
+ })) {
122
+ return false;
123
+ }
124
+ return super.checkCollision(...hitboxes);
125
+ }
126
+ }
8
127
  /**
9
128
  * The top level storage class of all entities. Add entities with {@link EntityStore2d.addEntity}.
10
129
  *
@@ -36,7 +155,7 @@ export class EntityStore2d {
36
155
  constructor(args) {
37
156
  this.pixi = args.pixi;
38
157
  this.assetLoader = args.assetLoader;
39
- this.hitboxSystem = args.customHitboxSystem || new HitboxSystem();
158
+ this.hitboxSystem = args.customHitboxSystem || new EntityHitboxSystem();
40
159
  this.state = args.state;
41
160
  if (args.preregisteredEntities) {
42
161
  this.registerEntities({
@@ -45,21 +164,6 @@ export class EntityStore2d {
45
164
  });
46
165
  }
47
166
  }
48
- /**
49
- * Load all the assets for all the given entities. Without this, assets will be loaded on demand
50
- * only.
51
- */
52
- async loadEntityAssets({ entities, otherAssets, }, options) {
53
- const assets = [
54
- ...(otherAssets || []),
55
- ...entities.flatMap((entity) => {
56
- return Object.values(entity.assets).map((asset) => {
57
- return asset;
58
- });
59
- }),
60
- ];
61
- return await this.assetLoader.bulkLoadAssets(assets, options);
62
- }
63
167
  /**
64
168
  * Register a set of entities so that they can be deserialized (for example, when transferring
65
169
  * game state in across the network for multilayer).
@@ -111,9 +215,22 @@ export class EntityStore2d {
111
215
  secondaryEntity.isDestroyed) {
112
216
  return;
113
217
  }
114
- const result = primaryEntity.collide(secondaryEntity, response);
115
- if (result instanceof Promise) {
116
- collisionPromises.push(result);
218
+ function trackCollisionResult(result) {
219
+ if (result instanceof Promise) {
220
+ collisionPromises.push(result);
221
+ }
222
+ }
223
+ if (shouldNotifyEntityOfCollision({
224
+ entity: primaryEntity,
225
+ otherEntity: secondaryEntity,
226
+ })) {
227
+ trackCollisionResult(primaryEntity.collide(secondaryEntity, response));
228
+ }
229
+ if (shouldNotifyEntityOfCollision({
230
+ entity: secondaryEntity,
231
+ otherEntity: primaryEntity,
232
+ })) {
233
+ trackCollisionResult(secondaryEntity.collide(primaryEntity, createReversedCollision(response)));
117
234
  }
118
235
  });
119
236
  await Promise.all(collisionPromises);
@@ -254,6 +371,10 @@ export class BaseEntity2d {
254
371
  * constructed. You cannot have duplicate keys loaded at the same time.
255
372
  */
256
373
  static entityKey = 'BaseEntity';
374
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
375
+ static collidesWith;
376
+ /** Cached entity classes this entity observes collisions with. */
377
+ static collidesWithSet = new Set();
257
378
  /** Shape definition of this entity's parameters. */
258
379
  static paramsShape;
259
380
  static assets;
@@ -288,6 +409,8 @@ export class BaseEntity2d {
288
409
  }
289
410
  /** If true, this entity should no longer be used or operated upon. */
290
411
  isDestroyed = false;
412
+ /** Static definition used to construct this entity. */
413
+ entityDefinition;
291
414
  abortController = new AbortController();
292
415
  /** An `AbortSignal` that triggers when the entity is destroyed. */
293
416
  abortSignal = this.abortController.signal;
@@ -302,6 +425,7 @@ export class BaseEntity2d {
302
425
  hitboxSystem;
303
426
  getAsset;
304
427
  constructor(args) {
428
+ this.entityDefinition = this.constructor;
305
429
  this.entityStore = args.entityStore;
306
430
  this.params = args.params;
307
431
  this.pixi = args.pixi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antha/entity-2d",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "An Antha mod for handling 2D entities.",
5
5
  "keywords": [
6
6
  "vir",
@@ -39,8 +39,8 @@
39
39
  "@augment-vir/common": "^32.2.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@antha/engine": "^0.4.3",
43
- "@antha/web-test-runner-plugin-pixi": "^0.4.3",
42
+ "@antha/engine": "^0.6.0",
43
+ "@antha/web-test-runner-plugin-pixi": "^0.6.0",
44
44
  "@augment-vir/test": "^32.2.3",
45
45
  "@web/dev-server-esbuild": "^2.0.0",
46
46
  "@web/test-runner": "^1.0.0",
@@ -52,9 +52,9 @@
52
52
  "typed-event-target": "^4.3.3"
53
53
  },
54
54
  "peerDependencies": {
55
- "@antha/asset": "^0.4.3",
56
- "@antha/engine": "^0.4.3",
57
- "@antha/graphics-2d": "^0.4.3",
55
+ "@antha/asset": "^0.6.0",
56
+ "@antha/engine": "^0.6.0",
57
+ "@antha/graphics-2d": "^0.6.0",
58
58
  "detect-collisions": ">=10",
59
59
  "element-vir": ">=26",
60
60
  "object-shape-tester": ">=6",