@antha/entity-2d 0.4.3 → 0.5.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, 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?: ReadonlyArray<Entity2dConstructor> | 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: ReadonlyArray<Entity2dConstructor> | undefined;
103
+ /** Cached entity classes this entity observes collisions with. */
104
+ collidesWithSet: ReadonlySet<Entity2dConstructor> | undefined;
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,6 +30,8 @@ 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);
33
35
  static entityKey = key;
34
36
  static paramsShape = paramsShape;
35
37
  static assets = assets;
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
  /**
@@ -73,6 +73,13 @@ export type EntityStore2dConstructorParams<State extends AnyObject = any> = {
73
73
  * @category Internal
74
74
  */
75
75
  export type Entity2dConstructor = Constructor<BaseEntity2d> & StaticEntity2dParts;
76
+ /** A collision system that skips pairs no entity observes before running SAT collision checks. */
77
+ export declare class EntityHitboxSystem extends HitboxSystem {
78
+ protected checkedHitboxPairs: WeakMap<Hitbox, WeakSet<Hitbox>> | undefined;
79
+ checkAll(...[callback, response,]: Parameters<HitboxSystem['checkAll']>): any;
80
+ checkOne(...[hitbox, callback, response,]: Parameters<HitboxSystem['checkOne']>): boolean;
81
+ checkCollision(...hitboxes: Parameters<HitboxSystem['checkCollision']>): boolean;
82
+ }
76
83
  /**
77
84
  * The top level storage class of all entities. Add entities with {@link EntityStore2d.addEntity}.
78
85
  *
@@ -299,6 +306,10 @@ export declare abstract class BaseEntity2d<State extends AnyObject = any, Params
299
306
  * constructed. You cannot have duplicate keys loaded at the same time.
300
307
  */
301
308
  static readonly entityKey: string;
309
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
310
+ static readonly collidesWith: ReadonlyArray<Entity2dConstructor> | undefined;
311
+ /** Cached entity classes this entity observes collisions with. */
312
+ static readonly collidesWithSet: ReadonlySet<Entity2dConstructor> | undefined;
302
313
  /** Shape definition of this entity's parameters. */
303
314
  static readonly paramsShape: Shape<AnyObject> | undefined;
304
315
  static readonly assets: MappedEntityAssets<BaseEntityAssetDefinitions | undefined> | undefined;
@@ -321,6 +332,8 @@ export declare abstract class BaseEntity2d<State extends AnyObject = any, Params
321
332
  dispatch(event: EntityEvent | EntityDestroyEvent): void;
322
333
  /** If true, this entity should no longer be used or operated upon. */
323
334
  readonly isDestroyed: boolean;
335
+ /** Static definition used to construct this entity. */
336
+ readonly entityDefinition: Entity2dConstructor;
324
337
  protected readonly abortController: AbortController;
325
338
  /** An `AbortSignal` that triggers when the entity is destroyed. */
326
339
  readonly abortSignal: AbortSignal;
package/dist/entity.js CHANGED
@@ -1,10 +1,120 @@
1
1
  import { assert, check } from '@augment-vir/assert';
2
2
  import { ConstructorInstanceMap, getObjectTypedEntries, makeWritable, mapObjectValues, } from '@augment-vir/common';
3
- import { System as HitboxSystem, } from 'detect-collisions';
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
+ function extractEntityFromHitbox(hitbox) {
9
+ return hitbox.userData instanceof BaseEntity2d ? hitbox.userData : undefined;
10
+ }
11
+ function hasCollisionTargets(entity) {
12
+ return ((entity.entityDefinition.collidesWithSet?.size ??
13
+ entity.entityDefinition.collidesWith?.length ??
14
+ 0) > 0);
15
+ }
16
+ function doesEntityCollideWith({ entity, otherEntity, }) {
17
+ return (entity.entityDefinition.collidesWithSet?.has(otherEntity.entityDefinition) ??
18
+ entity.entityDefinition.collidesWith?.includes(otherEntity.entityDefinition) ??
19
+ false);
20
+ }
21
+ function shouldCheckEntityCollision({ firstEntity, secondEntity, }) {
22
+ return (doesEntityCollideWith({
23
+ entity: firstEntity,
24
+ otherEntity: secondEntity,
25
+ }) ||
26
+ doesEntityCollideWith({
27
+ entity: secondEntity,
28
+ otherEntity: firstEntity,
29
+ }));
30
+ }
31
+ function shouldNotifyEntityOfCollision({ entity, otherEntity, }) {
32
+ return doesEntityCollideWith({
33
+ entity,
34
+ otherEntity,
35
+ });
36
+ }
37
+ function createReversedCollision(collision) {
38
+ const reversedCollision = new Response();
39
+ reversedCollision.a = collision.b;
40
+ reversedCollision.aInB = collision.bInA;
41
+ reversedCollision.b = collision.a;
42
+ reversedCollision.bInA = collision.aInB;
43
+ reversedCollision.overlap = collision.overlap;
44
+ reversedCollision.overlapN.x = -collision.overlapN.x;
45
+ reversedCollision.overlapN.y = -collision.overlapN.y;
46
+ reversedCollision.overlapV.x = -collision.overlapV.x;
47
+ reversedCollision.overlapV.y = -collision.overlapV.y;
48
+ return reversedCollision;
49
+ }
50
+ function hasAlreadyCheckedHitboxPair({ checkedHitboxPairs, firstHitbox, secondHitbox, }) {
51
+ return (checkedHitboxPairs.get(firstHitbox)?.has(secondHitbox) ||
52
+ checkedHitboxPairs.get(secondHitbox)?.has(firstHitbox));
53
+ }
54
+ function markHitboxPairAsChecked({ checkedHitboxPairs, firstHitbox, secondHitbox, }) {
55
+ const firstHitboxPairs = checkedHitboxPairs.get(firstHitbox);
56
+ if (firstHitboxPairs) {
57
+ firstHitboxPairs.add(secondHitbox);
58
+ }
59
+ else {
60
+ checkedHitboxPairs.set(firstHitbox, new WeakSet([secondHitbox]));
61
+ }
62
+ }
63
+ /** A collision system that skips pairs no entity observes before running SAT collision checks. */
64
+ export class EntityHitboxSystem extends HitboxSystem {
65
+ checkedHitboxPairs;
66
+ checkAll(...[callback, response,]) {
67
+ const previousCheckedHitboxPairs = this.checkedHitboxPairs;
68
+ this.checkedHitboxPairs = new WeakMap();
69
+ try {
70
+ return this.all().some((hitbox) => {
71
+ const entity = extractEntityFromHitbox(hitbox);
72
+ return ((!entity || hasCollisionTargets(entity)) &&
73
+ this.checkOne(hitbox, callback, response));
74
+ });
75
+ }
76
+ finally {
77
+ this.checkedHitboxPairs = previousCheckedHitboxPairs;
78
+ }
79
+ }
80
+ checkOne(...[hitbox, callback, response,]) {
81
+ const entity = extractEntityFromHitbox(hitbox);
82
+ if (entity && !hasCollisionTargets(entity)) {
83
+ return false;
84
+ }
85
+ return super.checkOne(hitbox, callback, response);
86
+ }
87
+ checkCollision(...hitboxes) {
88
+ const [firstHitbox, secondHitbox,] = hitboxes;
89
+ const checkedHitboxPairs = this.checkedHitboxPairs;
90
+ if (checkedHitboxPairs &&
91
+ hasAlreadyCheckedHitboxPair({
92
+ checkedHitboxPairs,
93
+ firstHitbox,
94
+ secondHitbox,
95
+ })) {
96
+ return false;
97
+ }
98
+ if (checkedHitboxPairs) {
99
+ markHitboxPairAsChecked({
100
+ checkedHitboxPairs,
101
+ firstHitbox,
102
+ secondHitbox,
103
+ });
104
+ }
105
+ const firstEntity = extractEntityFromHitbox(firstHitbox);
106
+ const secondEntity = extractEntityFromHitbox(secondHitbox);
107
+ if (firstEntity &&
108
+ secondEntity &&
109
+ !shouldCheckEntityCollision({
110
+ firstEntity,
111
+ secondEntity,
112
+ })) {
113
+ return false;
114
+ }
115
+ return super.checkCollision(...hitboxes);
116
+ }
117
+ }
8
118
  /**
9
119
  * The top level storage class of all entities. Add entities with {@link EntityStore2d.addEntity}.
10
120
  *
@@ -36,7 +146,7 @@ export class EntityStore2d {
36
146
  constructor(args) {
37
147
  this.pixi = args.pixi;
38
148
  this.assetLoader = args.assetLoader;
39
- this.hitboxSystem = args.customHitboxSystem || new HitboxSystem();
149
+ this.hitboxSystem = args.customHitboxSystem || new EntityHitboxSystem();
40
150
  this.state = args.state;
41
151
  if (args.preregisteredEntities) {
42
152
  this.registerEntities({
@@ -111,9 +221,22 @@ export class EntityStore2d {
111
221
  secondaryEntity.isDestroyed) {
112
222
  return;
113
223
  }
114
- const result = primaryEntity.collide(secondaryEntity, response);
115
- if (result instanceof Promise) {
116
- collisionPromises.push(result);
224
+ function trackCollisionResult(result) {
225
+ if (result instanceof Promise) {
226
+ collisionPromises.push(result);
227
+ }
228
+ }
229
+ if (shouldNotifyEntityOfCollision({
230
+ entity: primaryEntity,
231
+ otherEntity: secondaryEntity,
232
+ })) {
233
+ trackCollisionResult(primaryEntity.collide(secondaryEntity, response));
234
+ }
235
+ if (shouldNotifyEntityOfCollision({
236
+ entity: secondaryEntity,
237
+ otherEntity: primaryEntity,
238
+ })) {
239
+ trackCollisionResult(secondaryEntity.collide(primaryEntity, createReversedCollision(response)));
117
240
  }
118
241
  });
119
242
  await Promise.all(collisionPromises);
@@ -254,6 +377,10 @@ export class BaseEntity2d {
254
377
  * constructed. You cannot have duplicate keys loaded at the same time.
255
378
  */
256
379
  static entityKey = 'BaseEntity';
380
+ /** Entity classes this entity observes collisions with. Omit to observe none. */
381
+ static collidesWith;
382
+ /** Cached entity classes this entity observes collisions with. */
383
+ static collidesWithSet;
257
384
  /** Shape definition of this entity's parameters. */
258
385
  static paramsShape;
259
386
  static assets;
@@ -288,6 +415,8 @@ export class BaseEntity2d {
288
415
  }
289
416
  /** If true, this entity should no longer be used or operated upon. */
290
417
  isDestroyed = false;
418
+ /** Static definition used to construct this entity. */
419
+ entityDefinition;
291
420
  abortController = new AbortController();
292
421
  /** An `AbortSignal` that triggers when the entity is destroyed. */
293
422
  abortSignal = this.abortController.signal;
@@ -302,6 +431,7 @@ export class BaseEntity2d {
302
431
  hitboxSystem;
303
432
  getAsset;
304
433
  constructor(args) {
434
+ this.entityDefinition = this.constructor;
305
435
  this.entityStore = args.entityStore;
306
436
  this.params = args.params;
307
437
  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.5.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.5.0",
43
+ "@antha/web-test-runner-plugin-pixi": "^0.5.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.5.0",
56
+ "@antha/engine": "^0.5.0",
57
+ "@antha/graphics-2d": "^0.5.0",
58
58
  "detect-collisions": ">=10",
59
59
  "element-vir": ">=26",
60
60
  "object-shape-tester": ">=6",