@aura3d/physics 1.0.6

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.
Files changed (147) hide show
  1. package/README.md +26 -0
  2. package/dist/CharacterController.d.ts +52 -0
  3. package/dist/CharacterController.d.ts.map +1 -0
  4. package/dist/CharacterController.js +146 -0
  5. package/dist/CharacterController.js.map +1 -0
  6. package/dist/ClothFixtures.d.ts +87 -0
  7. package/dist/ClothFixtures.d.ts.map +1 -0
  8. package/dist/ClothFixtures.js +264 -0
  9. package/dist/ClothFixtures.js.map +1 -0
  10. package/dist/Collider.d.ts +27 -0
  11. package/dist/Collider.d.ts.map +1 -0
  12. package/dist/Collider.js +45 -0
  13. package/dist/Collider.js.map +1 -0
  14. package/dist/CollisionEvents.d.ts +26 -0
  15. package/dist/CollisionEvents.d.ts.map +1 -0
  16. package/dist/CollisionEvents.js +46 -0
  17. package/dist/CollisionEvents.js.map +1 -0
  18. package/dist/CollisionVolumes.d.ts +57 -0
  19. package/dist/CollisionVolumes.d.ts.map +1 -0
  20. package/dist/CollisionVolumes.js +159 -0
  21. package/dist/CollisionVolumes.js.map +1 -0
  22. package/dist/Constraint.d.ts +31 -0
  23. package/dist/Constraint.d.ts.map +1 -0
  24. package/dist/Constraint.js +103 -0
  25. package/dist/Constraint.js.map +1 -0
  26. package/dist/Constraints.d.ts +2 -0
  27. package/dist/Constraints.d.ts.map +1 -0
  28. package/dist/Constraints.js +2 -0
  29. package/dist/Constraints.js.map +1 -0
  30. package/dist/Crowd.d.ts +64 -0
  31. package/dist/Crowd.d.ts.map +1 -0
  32. package/dist/Crowd.js +209 -0
  33. package/dist/Crowd.js.map +1 -0
  34. package/dist/ECSPhysicsBridge.d.ts +17 -0
  35. package/dist/ECSPhysicsBridge.d.ts.map +1 -0
  36. package/dist/ECSPhysicsBridge.js +37 -0
  37. package/dist/ECSPhysicsBridge.js.map +1 -0
  38. package/dist/FireSmokeFixtures.d.ts +103 -0
  39. package/dist/FireSmokeFixtures.d.ts.map +1 -0
  40. package/dist/FireSmokeFixtures.js +271 -0
  41. package/dist/FireSmokeFixtures.js.map +1 -0
  42. package/dist/FluidFixtures.d.ts +75 -0
  43. package/dist/FluidFixtures.d.ts.map +1 -0
  44. package/dist/FluidFixtures.js +219 -0
  45. package/dist/FluidFixtures.js.map +1 -0
  46. package/dist/FractureFixtures.d.ts +68 -0
  47. package/dist/FractureFixtures.d.ts.map +1 -0
  48. package/dist/FractureFixtures.js +199 -0
  49. package/dist/FractureFixtures.js.map +1 -0
  50. package/dist/HitboxWorld.d.ts +250 -0
  51. package/dist/HitboxWorld.d.ts.map +1 -0
  52. package/dist/HitboxWorld.js +771 -0
  53. package/dist/HitboxWorld.js.map +1 -0
  54. package/dist/KinematicBody.d.ts +157 -0
  55. package/dist/KinematicBody.d.ts.map +1 -0
  56. package/dist/KinematicBody.js +405 -0
  57. package/dist/KinematicBody.js.map +1 -0
  58. package/dist/KinematicWorld.d.ts +58 -0
  59. package/dist/KinematicWorld.d.ts.map +1 -0
  60. package/dist/KinematicWorld.js +246 -0
  61. package/dist/KinematicWorld.js.map +1 -0
  62. package/dist/Navigation.d.ts +67 -0
  63. package/dist/Navigation.d.ts.map +1 -0
  64. package/dist/Navigation.js +261 -0
  65. package/dist/Navigation.js.map +1 -0
  66. package/dist/PhysicsDebugDraw.d.ts +11 -0
  67. package/dist/PhysicsDebugDraw.d.ts.map +1 -0
  68. package/dist/PhysicsDebugDraw.js +64 -0
  69. package/dist/PhysicsDebugDraw.js.map +1 -0
  70. package/dist/PhysicsSandboxFixtures.d.ts +53 -0
  71. package/dist/PhysicsSandboxFixtures.d.ts.map +1 -0
  72. package/dist/PhysicsSandboxFixtures.js +280 -0
  73. package/dist/PhysicsSandboxFixtures.js.map +1 -0
  74. package/dist/PhysicsStepper.d.ts +15 -0
  75. package/dist/PhysicsStepper.d.ts.map +1 -0
  76. package/dist/PhysicsStepper.js +37 -0
  77. package/dist/PhysicsStepper.js.map +1 -0
  78. package/dist/PhysicsWorld.d.ts +101 -0
  79. package/dist/PhysicsWorld.d.ts.map +1 -0
  80. package/dist/PhysicsWorld.js +868 -0
  81. package/dist/PhysicsWorld.js.map +1 -0
  82. package/dist/PlatformerFixtures.d.ts +70 -0
  83. package/dist/PlatformerFixtures.d.ts.map +1 -0
  84. package/dist/PlatformerFixtures.js +120 -0
  85. package/dist/PlatformerFixtures.js.map +1 -0
  86. package/dist/Raycast.d.ts +23 -0
  87. package/dist/Raycast.d.ts.map +1 -0
  88. package/dist/Raycast.js +271 -0
  89. package/dist/Raycast.js.map +1 -0
  90. package/dist/RigidBody.d.ts +69 -0
  91. package/dist/RigidBody.d.ts.map +1 -0
  92. package/dist/RigidBody.js +255 -0
  93. package/dist/RigidBody.js.map +1 -0
  94. package/dist/ScenePhysicsBridge.d.ts +42 -0
  95. package/dist/ScenePhysicsBridge.d.ts.map +1 -0
  96. package/dist/ScenePhysicsBridge.js +168 -0
  97. package/dist/ScenePhysicsBridge.js.map +1 -0
  98. package/dist/Shape.d.ts +50 -0
  99. package/dist/Shape.d.ts.map +1 -0
  100. package/dist/Shape.js +128 -0
  101. package/dist/Shape.js.map +1 -0
  102. package/dist/SoftBodyFixtures.d.ts +70 -0
  103. package/dist/SoftBodyFixtures.d.ts.map +1 -0
  104. package/dist/SoftBodyFixtures.js +272 -0
  105. package/dist/SoftBodyFixtures.js.map +1 -0
  106. package/dist/Steering.d.ts +161 -0
  107. package/dist/Steering.d.ts.map +1 -0
  108. package/dist/Steering.js +347 -0
  109. package/dist/Steering.js.map +1 -0
  110. package/dist/VehicleDynamics.d.ts +152 -0
  111. package/dist/VehicleDynamics.d.ts.map +1 -0
  112. package/dist/VehicleDynamics.js +335 -0
  113. package/dist/VehicleDynamics.js.map +1 -0
  114. package/dist/index.d.ts +29 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +29 -0
  117. package/dist/index.js.map +1 -0
  118. package/package.json +17 -0
  119. package/src/CharacterController.ts +184 -0
  120. package/src/ClothFixtures.ts +359 -0
  121. package/src/Collider.ts +67 -0
  122. package/src/CollisionEvents.ts +70 -0
  123. package/src/CollisionVolumes.ts +218 -0
  124. package/src/Constraint.ts +120 -0
  125. package/src/Constraints.ts +1 -0
  126. package/src/Crowd.ts +283 -0
  127. package/src/ECSPhysicsBridge.ts +53 -0
  128. package/src/FireSmokeFixtures.ts +382 -0
  129. package/src/FluidFixtures.ts +301 -0
  130. package/src/FractureFixtures.ts +277 -0
  131. package/src/HitboxWorld.ts +1179 -0
  132. package/src/KinematicBody.ts +553 -0
  133. package/src/KinematicWorld.ts +310 -0
  134. package/src/Navigation.ts +321 -0
  135. package/src/PhysicsDebugDraw.ts +71 -0
  136. package/src/PhysicsSandboxFixtures.ts +394 -0
  137. package/src/PhysicsStepper.ts +47 -0
  138. package/src/PhysicsWorld.ts +986 -0
  139. package/src/PlatformerFixtures.ts +205 -0
  140. package/src/Raycast.ts +315 -0
  141. package/src/RigidBody.ts +311 -0
  142. package/src/ScenePhysicsBridge.ts +207 -0
  143. package/src/Shape.ts +182 -0
  144. package/src/SoftBodyFixtures.ts +363 -0
  145. package/src/Steering.ts +531 -0
  146. package/src/VehicleDynamics.ts +524 -0
  147. package/src/index.ts +28 -0
@@ -0,0 +1,771 @@
1
+ import { aabbPenetration, guardbox, hitbox, hurtbox, overlapsVolume, pushbox, resolveCollisionVolume, withVolumeOwner } from "./CollisionVolumes.js";
2
+ import { cloneVec3, validateFiniteVec3 } from "./Shape.js";
3
+ export class HitboxWorld {
4
+ detectPushboxOverlaps;
5
+ lockOnKnockout;
6
+ combatantsById = new Map();
7
+ hitboxesById = new Map();
8
+ nextHitboxId = 1;
9
+ frame = 0;
10
+ roundLocked = false;
11
+ lastEvents = [];
12
+ pendingEvents = [];
13
+ constructor(descriptor = {}) {
14
+ this.detectPushboxOverlaps = descriptor.detectPushboxOverlaps ?? true;
15
+ this.lockOnKnockout = descriptor.lockOnKnockout ?? true;
16
+ }
17
+ registerCombatant(descriptor) {
18
+ if (this.combatantsById.has(descriptor.id)) {
19
+ throw new Error(`Combatant ${String(descriptor.id)} already exists.`);
20
+ }
21
+ const combatant = createCombatantRecord(descriptor);
22
+ this.combatantsById.set(combatant.id, combatant);
23
+ return snapshotCombatant(combatant);
24
+ }
25
+ upsertCombatant(descriptor) {
26
+ const existing = this.combatantsById.get(descriptor.id);
27
+ if (!existing) {
28
+ return this.registerCombatant(descriptor);
29
+ }
30
+ applyCombatantDescriptor(existing, descriptor);
31
+ return snapshotCombatant(existing);
32
+ }
33
+ removeCombatant(id) {
34
+ this.combatantsById.delete(id);
35
+ for (const hitboxRecord of this.hitboxesById.values()) {
36
+ if (hitboxRecord.ownerId === id) {
37
+ hitboxRecord.expired = true;
38
+ }
39
+ }
40
+ }
41
+ getCombatant(id) {
42
+ const combatant = this.combatantsById.get(id);
43
+ return combatant ? snapshotCombatant(combatant) : undefined;
44
+ }
45
+ setPose(id, pose) {
46
+ const combatant = this.requireCombatant(id);
47
+ if (pose.position) {
48
+ validateFiniteVec3(pose.position, "combatant pose position");
49
+ combatant.position = cloneVec3(pose.position);
50
+ }
51
+ if (pose.facing) {
52
+ combatant.facing = pose.facing < 0 ? -1 : 1;
53
+ }
54
+ }
55
+ setGuarding(id, blocking) {
56
+ const combatant = this.requireCombatant(id);
57
+ combatant.blocking = !combatant.knockedOut && blocking;
58
+ }
59
+ setHurtboxes(id, volumes) {
60
+ this.requireCombatant(id).hurtboxes = volumes.map((volume) => requireVolumeKind(volume, "hurtbox"));
61
+ }
62
+ setGuardBoxes(id, volumes) {
63
+ this.requireCombatant(id).guardBoxes = volumes.map((volume) => requireVolumeKind(volume, "guardbox"));
64
+ }
65
+ setPushbox(id, volume) {
66
+ this.requireCombatant(id).pushbox = volume === null ? null : requireVolumeKind(volume, "pushbox");
67
+ }
68
+ spawnHitbox(descriptor) {
69
+ const owner = this.requireCombatant(descriptor.ownerId);
70
+ const id = descriptor.id ?? `${String(descriptor.ownerId)}:${descriptor.moveId}:${this.nextHitboxId}`;
71
+ this.nextHitboxId += 1;
72
+ const activeFrames = normalizeActiveFrames(descriptor.activeFrames ?? { start: 0, end: 2 });
73
+ if (this.hitboxesById.has(id)) {
74
+ throw new Error(`Hitbox ${id} already exists.`);
75
+ }
76
+ const record = {
77
+ id,
78
+ ownerId: descriptor.ownerId,
79
+ team: descriptor.team ?? owner.team,
80
+ moveId: descriptor.moveId,
81
+ boxes: descriptor.boxes.map((volume) => requireVolumeKind(volume, "hitbox")),
82
+ activeFrames,
83
+ recoveryFrames: nonNegativeInteger(descriptor.recoveryFrames ?? 0, "hitbox recoveryFrames"),
84
+ damage: nonNegativeFinite(descriptor.damage ?? 10, "hitbox damage"),
85
+ guardDamage: nonNegativeFinite(descriptor.guardDamage ?? Math.ceil((descriptor.damage ?? 10) * 0.35), "hitbox guardDamage"),
86
+ hitstunFrames: nonNegativeInteger(descriptor.hitstunFrames ?? 14, "hitbox hitstunFrames"),
87
+ blockstunFrames: nonNegativeInteger(descriptor.blockstunFrames ?? 8, "hitbox blockstunFrames"),
88
+ hitStopFrames: nonNegativeInteger(descriptor.hitStopFrames ?? 6, "hitbox hitStopFrames"),
89
+ selfHitStopFrames: nonNegativeInteger(descriptor.selfHitStopFrames ?? descriptor.hitStopFrames ?? 6, "hitbox selfHitStopFrames"),
90
+ knockback: cloneVec3(descriptor.knockback ?? [2.4, 0.8, 0]),
91
+ blockKnockback: cloneVec3(descriptor.blockKnockback ?? [1.1, 0, 0]),
92
+ priority: finite(descriptor.priority ?? 0, "hitbox priority"),
93
+ canHitOwner: descriptor.canHitOwner ?? false,
94
+ canHitSameTeam: descriptor.canHitSameTeam ?? false,
95
+ hitOnce: descriptor.hitOnce ?? true,
96
+ maxHits: positiveInteger(descriptor.maxHits ?? Number.MAX_SAFE_INTEGER, "hitbox maxHits"),
97
+ metadata: descriptor.metadata,
98
+ ageFrames: 0,
99
+ hitCount: 0,
100
+ expired: false,
101
+ hitTargets: new Set(),
102
+ recoveryStarted: false
103
+ };
104
+ validateFiniteVec3(record.knockback, "hitbox knockback");
105
+ validateFiniteVec3(record.blockKnockback, "hitbox blockKnockback");
106
+ if (this.roundLocked || owner.knockedOut) {
107
+ return snapshotHitbox({
108
+ ...record,
109
+ expired: true
110
+ });
111
+ }
112
+ this.hitboxesById.set(record.id, record);
113
+ this.pendingEvents.push({
114
+ type: "hitbox-spawned",
115
+ frame: this.frame,
116
+ hitboxId: record.id,
117
+ ownerId: record.ownerId,
118
+ moveId: record.moveId,
119
+ activeFrames: record.activeFrames
120
+ });
121
+ return snapshotHitbox(record);
122
+ }
123
+ clearHitboxes(ownerId) {
124
+ for (const [id, record] of this.hitboxesById) {
125
+ if (ownerId === undefined || record.ownerId === ownerId) {
126
+ this.hitboxesById.delete(id);
127
+ }
128
+ }
129
+ }
130
+ reset(combatants) {
131
+ this.hitboxesById.clear();
132
+ this.pendingEvents.length = 0;
133
+ this.lastEvents = [];
134
+ this.roundLocked = false;
135
+ if (combatants) {
136
+ this.combatantsById.clear();
137
+ for (const combatant of combatants) {
138
+ this.registerCombatant(combatant);
139
+ }
140
+ }
141
+ else {
142
+ for (const combatant of this.combatantsById.values()) {
143
+ resetCombatantRecord(combatant);
144
+ }
145
+ }
146
+ this.lastEvents = [
147
+ {
148
+ type: "round-reset",
149
+ frame: this.frame,
150
+ combatants: this.combatants().map((combatant) => combatant.id)
151
+ }
152
+ ];
153
+ return this.snapshot();
154
+ }
155
+ step(frames = 1) {
156
+ const frameCount = positiveInteger(frames, "HitboxWorld.step frames");
157
+ const events = [];
158
+ events.push(...this.drainPendingEvents());
159
+ for (let index = 0; index < frameCount; index += 1) {
160
+ events.push(...this.stepOneFrame());
161
+ }
162
+ this.lastEvents = events;
163
+ return events;
164
+ }
165
+ drainEvents() {
166
+ const events = [...this.pendingEvents, ...this.lastEvents];
167
+ this.pendingEvents.length = 0;
168
+ this.lastEvents = [];
169
+ return events;
170
+ }
171
+ snapshot() {
172
+ return {
173
+ frame: this.frame,
174
+ roundLocked: this.roundLocked,
175
+ combatants: this.combatants(),
176
+ hitboxes: this.hitboxes(),
177
+ events: this.lastEvents.map(cloneCombatEvent)
178
+ };
179
+ }
180
+ combatants() {
181
+ return Array.from(this.combatantsById.values()).sort(compareCombatants).map(snapshotCombatant);
182
+ }
183
+ hitboxes() {
184
+ return Array.from(this.hitboxesById.values()).sort(compareHitboxes).map(snapshotHitbox);
185
+ }
186
+ stepOneFrame() {
187
+ const events = [];
188
+ if (this.detectPushboxOverlaps) {
189
+ events.push(...this.collectPushboxEvents());
190
+ }
191
+ if (this.roundLocked) {
192
+ this.frame += 1;
193
+ return events;
194
+ }
195
+ for (const record of this.sortedHitboxRecords()) {
196
+ if (record.expired || record.hitCount >= record.maxHits) {
197
+ record.expired = true;
198
+ continue;
199
+ }
200
+ const owner = this.combatantsById.get(record.ownerId);
201
+ if (!owner) {
202
+ record.expired = true;
203
+ continue;
204
+ }
205
+ if (owner.knockedOut) {
206
+ record.expired = true;
207
+ continue;
208
+ }
209
+ if (isHitboxActive(record)) {
210
+ events.push(...this.resolveHitbox(record, owner));
211
+ if (this.roundLocked) {
212
+ break;
213
+ }
214
+ }
215
+ if (!record.recoveryStarted && record.ageFrames > record.activeFrames.end && record.recoveryFrames > 0) {
216
+ record.recoveryStarted = true;
217
+ owner.recoveryFrames = Math.max(owner.recoveryFrames, record.recoveryFrames);
218
+ events.push({
219
+ type: "recovery-start",
220
+ frame: this.frame,
221
+ combatantId: owner.id,
222
+ frames: record.recoveryFrames
223
+ });
224
+ }
225
+ }
226
+ if (this.roundLocked) {
227
+ this.expireAllHitboxes(events);
228
+ events.push(...this.tickTimers());
229
+ this.frame += 1;
230
+ return events;
231
+ }
232
+ events.push(...this.tickTimers());
233
+ for (const record of this.sortedHitboxRecords()) {
234
+ const owner = this.combatantsById.get(record.ownerId);
235
+ if (owner && owner.hitStopFrames <= 0) {
236
+ record.ageFrames += 1;
237
+ }
238
+ if (record.ageFrames > record.activeFrames.end + record.recoveryFrames || record.hitCount >= record.maxHits || record.expired) {
239
+ this.expireHitbox(record, events);
240
+ }
241
+ }
242
+ this.frame += 1;
243
+ return events;
244
+ }
245
+ resolveHitbox(record, owner) {
246
+ const events = [];
247
+ const hitVolumes = record.boxes.map((volume) => resolveCollisionVolume(withVolumeOwner(volume, owner.id), owner.position, owner.facing));
248
+ for (const defender of this.sortedCombatantRecords()) {
249
+ if (!canHitCombatant(record, owner, defender)) {
250
+ continue;
251
+ }
252
+ if (defender.invulnerableFrames > 0) {
253
+ continue;
254
+ }
255
+ if (record.hitOnce && record.hitTargets.has(defender.id)) {
256
+ continue;
257
+ }
258
+ const blockEvent = defender.blocking ? this.tryResolveBlock(record, owner, defender, hitVolumes) : null;
259
+ if (blockEvent) {
260
+ record.hitTargets.add(defender.id);
261
+ record.hitCount += 1;
262
+ events.push(blockEvent);
263
+ events.push(...this.applyBlockTimers(record, owner, defender));
264
+ continue;
265
+ }
266
+ const hitEvent = this.tryResolveHit(record, owner, defender, hitVolumes);
267
+ if (hitEvent) {
268
+ record.hitTargets.add(defender.id);
269
+ record.hitCount += 1;
270
+ events.push(hitEvent);
271
+ events.push(...this.applyHitTimers(record, owner, defender));
272
+ if (defender.health <= 0 && !defender.knockedOut) {
273
+ events.push(...this.knockOutCombatant(defender, owner, record));
274
+ if (this.roundLocked) {
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ }
280
+ return events;
281
+ }
282
+ tryResolveBlock(record, owner, defender, hitVolumes) {
283
+ const guardVolumes = (defender.guardBoxes.length > 0 ? defender.guardBoxes : defender.hurtboxes).map((volume) => resolveCollisionVolume(withVolumeOwner(volume, defender.id), defender.position, defender.facing));
284
+ const pair = firstOverlappingPair(hitVolumes, guardVolumes);
285
+ if (!pair) {
286
+ return null;
287
+ }
288
+ defender.guard = Math.max(0, defender.guard - record.guardDamage);
289
+ return {
290
+ type: "blocked",
291
+ frame: this.frame,
292
+ attackerId: owner.id,
293
+ defenderId: defender.id,
294
+ hitboxId: record.id,
295
+ moveId: record.moveId,
296
+ hitboxVolumeId: pair.attack.id,
297
+ guardboxId: pair.defense.id,
298
+ guardDamage: record.guardDamage,
299
+ defenderGuard: defender.guard,
300
+ blockstunFrames: record.blockstunFrames,
301
+ hitStopFrames: record.hitStopFrames,
302
+ knockback: orientVec3(record.blockKnockback, owner.facing),
303
+ point: contactPoint(pair.attack, pair.defense),
304
+ normal: combatNormal(owner, defender)
305
+ };
306
+ }
307
+ tryResolveHit(record, owner, defender, hitVolumes) {
308
+ const hurtVolumes = defender.hurtboxes.map((volume) => resolveCollisionVolume(withVolumeOwner(volume, defender.id), defender.position, defender.facing));
309
+ const pair = firstOverlappingPair(hitVolumes, hurtVolumes);
310
+ if (!pair) {
311
+ return null;
312
+ }
313
+ defender.health = Math.max(0, defender.health - record.damage);
314
+ return {
315
+ type: "hit",
316
+ frame: this.frame,
317
+ attackerId: owner.id,
318
+ defenderId: defender.id,
319
+ hitboxId: record.id,
320
+ moveId: record.moveId,
321
+ hitboxVolumeId: pair.attack.id,
322
+ hurtboxId: pair.defense.id,
323
+ damage: record.damage,
324
+ defenderHealth: defender.health,
325
+ hitstunFrames: record.hitstunFrames,
326
+ hitStopFrames: record.hitStopFrames,
327
+ knockback: orientVec3(record.knockback, owner.facing),
328
+ point: contactPoint(pair.attack, pair.defense),
329
+ normal: combatNormal(owner, defender)
330
+ };
331
+ }
332
+ knockOutCombatant(defender, owner, record) {
333
+ defender.knockedOut = true;
334
+ defender.blocking = false;
335
+ defender.invulnerableFrames = 0;
336
+ defender.hitStopFrames = 0;
337
+ defender.hitstunFrames = 0;
338
+ defender.blockstunFrames = 0;
339
+ defender.recoveryFrames = 0;
340
+ if (this.lockOnKnockout) {
341
+ this.roundLocked = true;
342
+ }
343
+ return [
344
+ {
345
+ type: "knockout",
346
+ frame: this.frame,
347
+ combatantId: defender.id,
348
+ attackerId: owner.id,
349
+ moveId: record.moveId
350
+ }
351
+ ];
352
+ }
353
+ applyHitTimers(record, owner, defender) {
354
+ const events = [];
355
+ events.push(...setTimer(defender, "hitstunFrames", record.hitstunFrames, "hitstun-start", this.frame));
356
+ events.push(...setTimer(defender, "hitStopFrames", record.hitStopFrames, "hitstop-start", this.frame));
357
+ events.push(...setTimer(owner, "hitStopFrames", record.selfHitStopFrames, "hitstop-start", this.frame));
358
+ return events;
359
+ }
360
+ applyBlockTimers(record, owner, defender) {
361
+ const events = [];
362
+ events.push(...setTimer(defender, "blockstunFrames", record.blockstunFrames, "blockstun-start", this.frame));
363
+ events.push(...setTimer(defender, "hitStopFrames", record.hitStopFrames, "hitstop-start", this.frame));
364
+ events.push(...setTimer(owner, "hitStopFrames", record.selfHitStopFrames, "hitstop-start", this.frame));
365
+ return events;
366
+ }
367
+ tickTimers() {
368
+ const events = [];
369
+ for (const combatant of this.sortedCombatantRecords()) {
370
+ events.push(...tickTimer(combatant, "hitStopFrames", "hitstop-end", this.frame));
371
+ events.push(...tickTimer(combatant, "hitstunFrames", "hitstun-end", this.frame));
372
+ events.push(...tickTimer(combatant, "blockstunFrames", "blockstun-end", this.frame));
373
+ events.push(...tickTimer(combatant, "recoveryFrames", "recovery-end", this.frame));
374
+ if (combatant.invulnerableFrames > 0) {
375
+ combatant.invulnerableFrames -= 1;
376
+ }
377
+ }
378
+ return events;
379
+ }
380
+ collectPushboxEvents() {
381
+ const events = [];
382
+ const combatants = this.sortedCombatantRecords().filter((combatant) => combatant.pushbox !== null && !combatant.knockedOut);
383
+ for (let aIndex = 0; aIndex < combatants.length; aIndex += 1) {
384
+ for (let bIndex = aIndex + 1; bIndex < combatants.length; bIndex += 1) {
385
+ const combatantA = combatants[aIndex];
386
+ const combatantB = combatants[bIndex];
387
+ const pushA = resolveCollisionVolume(withVolumeOwner(combatantA.pushbox, combatantA.id), combatantA.position, combatantA.facing);
388
+ const pushB = resolveCollisionVolume(withVolumeOwner(combatantB.pushbox, combatantB.id), combatantB.position, combatantB.facing);
389
+ if (!overlapsVolume(pushA, pushB)) {
390
+ continue;
391
+ }
392
+ const penetration = aabbPenetration(pushA.bounds, pushB.bounds);
393
+ if (!penetration) {
394
+ continue;
395
+ }
396
+ events.push({
397
+ type: "pushbox-overlap",
398
+ frame: this.frame,
399
+ combatantA: combatantA.id,
400
+ combatantB: combatantB.id,
401
+ normal: penetration.normal,
402
+ penetration: penetration.depth
403
+ });
404
+ }
405
+ }
406
+ return events;
407
+ }
408
+ expireHitbox(record, events) {
409
+ if (!this.hitboxesById.has(record.id)) {
410
+ return;
411
+ }
412
+ if (record.hitCount === 0) {
413
+ events.push({
414
+ type: "whiff",
415
+ frame: this.frame,
416
+ hitboxId: record.id,
417
+ ownerId: record.ownerId,
418
+ moveId: record.moveId
419
+ });
420
+ }
421
+ events.push({
422
+ type: "hitbox-expired",
423
+ frame: this.frame,
424
+ hitboxId: record.id,
425
+ ownerId: record.ownerId,
426
+ moveId: record.moveId,
427
+ hitCount: record.hitCount
428
+ });
429
+ this.hitboxesById.delete(record.id);
430
+ }
431
+ expireAllHitboxes(events) {
432
+ for (const record of this.sortedHitboxRecords()) {
433
+ this.expireHitbox(record, events);
434
+ }
435
+ }
436
+ requireCombatant(id) {
437
+ const combatant = this.combatantsById.get(id);
438
+ if (!combatant) {
439
+ throw new Error(`Combatant ${String(id)} does not exist.`);
440
+ }
441
+ return combatant;
442
+ }
443
+ sortedCombatantRecords() {
444
+ return Array.from(this.combatantsById.values()).sort(compareCombatants);
445
+ }
446
+ sortedHitboxRecords() {
447
+ return Array.from(this.hitboxesById.values()).sort(compareHitboxes);
448
+ }
449
+ drainPendingEvents() {
450
+ const events = [...this.pendingEvents];
451
+ this.pendingEvents.length = 0;
452
+ return events;
453
+ }
454
+ }
455
+ function createCombatantRecord(descriptor) {
456
+ const maxHealth = positiveFinite(descriptor.maxHealth ?? descriptor.health ?? 100, "combatant maxHealth");
457
+ const maxGuard = positiveFinite(descriptor.maxGuard ?? descriptor.guard ?? 100, "combatant maxGuard");
458
+ const position = cloneVec3(descriptor.position ?? [0, 0.9, 0]);
459
+ validateFiniteVec3(position, "combatant position");
460
+ const health = clampNonNegative(descriptor.health ?? maxHealth, maxHealth);
461
+ const guard = clampNonNegative(descriptor.guard ?? maxGuard, maxGuard);
462
+ const facing = descriptor.facing && descriptor.facing < 0 ? -1 : 1;
463
+ const blocking = descriptor.blocking ?? false;
464
+ const invulnerableFrames = nonNegativeInteger(descriptor.invulnerableFrames ?? 0, "combatant invulnerableFrames");
465
+ const meter = nonNegativeFinite(descriptor.meter ?? 0, "combatant meter");
466
+ return {
467
+ id: descriptor.id,
468
+ team: descriptor.team,
469
+ position,
470
+ facing,
471
+ hurtboxes: (descriptor.hurtboxes ?? [hurtbox({ id: "body", halfExtents: [0.36, 0.86, 0.28] })]).map((volume) => requireVolumeKind(volume, "hurtbox")),
472
+ guardBoxes: (descriptor.guardBoxes ?? [guardbox({ id: "guard", offset: [0.18, 0, 0], halfExtents: [0.28, 0.82, 0.3] })]).map((volume) => requireVolumeKind(volume, "guardbox")),
473
+ pushbox: descriptor.pushbox === null ? null : requireVolumeKind(descriptor.pushbox ?? pushbox({ id: "push", halfExtents: [0.32, 0.84, 0.26] }), "pushbox"),
474
+ health,
475
+ maxHealth,
476
+ guard,
477
+ maxGuard,
478
+ meter,
479
+ blocking,
480
+ invulnerableFrames,
481
+ hitStopFrames: 0,
482
+ hitstunFrames: 0,
483
+ blockstunFrames: 0,
484
+ recoveryFrames: 0,
485
+ knockedOut: descriptor.knockedOut ?? health <= 0,
486
+ metadata: descriptor.metadata,
487
+ baseline: {
488
+ position: cloneVec3(position),
489
+ facing,
490
+ health,
491
+ maxHealth,
492
+ guard,
493
+ maxGuard,
494
+ meter,
495
+ blocking,
496
+ invulnerableFrames
497
+ }
498
+ };
499
+ }
500
+ function applyCombatantDescriptor(record, descriptor) {
501
+ if (descriptor.team !== undefined) {
502
+ record.team = descriptor.team;
503
+ }
504
+ if (descriptor.position) {
505
+ validateFiniteVec3(descriptor.position, "combatant position");
506
+ record.position = cloneVec3(descriptor.position);
507
+ }
508
+ if (descriptor.facing) {
509
+ record.facing = descriptor.facing < 0 ? -1 : 1;
510
+ }
511
+ if (descriptor.hurtboxes) {
512
+ record.hurtboxes = descriptor.hurtboxes.map((volume) => requireVolumeKind(volume, "hurtbox"));
513
+ }
514
+ if (descriptor.guardBoxes) {
515
+ record.guardBoxes = descriptor.guardBoxes.map((volume) => requireVolumeKind(volume, "guardbox"));
516
+ }
517
+ if (descriptor.pushbox !== undefined) {
518
+ record.pushbox = descriptor.pushbox === null ? null : requireVolumeKind(descriptor.pushbox, "pushbox");
519
+ }
520
+ if (descriptor.maxHealth !== undefined) {
521
+ record.maxHealth = positiveFinite(descriptor.maxHealth, "combatant maxHealth");
522
+ }
523
+ if (descriptor.health !== undefined) {
524
+ record.health = clampNonNegative(descriptor.health, record.maxHealth);
525
+ record.knockedOut = descriptor.knockedOut ?? record.health <= 0;
526
+ }
527
+ if (descriptor.maxGuard !== undefined) {
528
+ record.maxGuard = positiveFinite(descriptor.maxGuard, "combatant maxGuard");
529
+ }
530
+ if (descriptor.guard !== undefined) {
531
+ record.guard = clampNonNegative(descriptor.guard, record.maxGuard);
532
+ }
533
+ if (descriptor.meter !== undefined) {
534
+ record.meter = nonNegativeFinite(descriptor.meter, "combatant meter");
535
+ }
536
+ if (descriptor.blocking !== undefined) {
537
+ record.blocking = descriptor.blocking;
538
+ }
539
+ if (descriptor.invulnerableFrames !== undefined) {
540
+ record.invulnerableFrames = nonNegativeInteger(descriptor.invulnerableFrames, "combatant invulnerableFrames");
541
+ }
542
+ if (descriptor.knockedOut !== undefined) {
543
+ record.knockedOut = descriptor.knockedOut;
544
+ if (record.knockedOut) {
545
+ record.health = 0;
546
+ record.blocking = false;
547
+ }
548
+ }
549
+ if (descriptor.metadata !== undefined) {
550
+ record.metadata = descriptor.metadata;
551
+ }
552
+ }
553
+ function canHitCombatant(record, owner, defender) {
554
+ if (owner.knockedOut || defender.knockedOut) {
555
+ return false;
556
+ }
557
+ if (owner.id === defender.id && !record.canHitOwner) {
558
+ return false;
559
+ }
560
+ if (!record.canHitSameTeam && record.team !== undefined && defender.team !== undefined && record.team === defender.team) {
561
+ return false;
562
+ }
563
+ return true;
564
+ }
565
+ function resetCombatantRecord(record) {
566
+ record.position = cloneVec3(record.baseline.position);
567
+ record.facing = record.baseline.facing;
568
+ record.health = record.baseline.health;
569
+ record.maxHealth = record.baseline.maxHealth;
570
+ record.guard = record.baseline.guard;
571
+ record.maxGuard = record.baseline.maxGuard;
572
+ record.meter = record.baseline.meter;
573
+ record.blocking = record.baseline.blocking;
574
+ record.invulnerableFrames = record.baseline.invulnerableFrames;
575
+ record.hitStopFrames = 0;
576
+ record.hitstunFrames = 0;
577
+ record.blockstunFrames = 0;
578
+ record.recoveryFrames = 0;
579
+ record.knockedOut = record.health <= 0;
580
+ }
581
+ function firstOverlappingPair(attackVolumes, defenseVolumes) {
582
+ const attacks = [...attackVolumes].sort(compareResolvedVolumes);
583
+ const defenses = [...defenseVolumes].sort(compareResolvedVolumes);
584
+ for (const attack of attacks) {
585
+ for (const defense of defenses) {
586
+ if (overlapsVolume(attack, defense)) {
587
+ return { attack, defense };
588
+ }
589
+ }
590
+ }
591
+ return null;
592
+ }
593
+ function setTimer(combatant, key, frames, eventType, frame) {
594
+ if (frames <= 0 || combatant[key] >= frames) {
595
+ return [];
596
+ }
597
+ combatant[key] = frames;
598
+ return [
599
+ {
600
+ type: eventType,
601
+ frame,
602
+ combatantId: combatant.id,
603
+ frames
604
+ }
605
+ ];
606
+ }
607
+ function tickTimer(combatant, key, eventType, frame) {
608
+ if (combatant[key] <= 0) {
609
+ return [];
610
+ }
611
+ combatant[key] -= 1;
612
+ if (combatant[key] > 0) {
613
+ return [];
614
+ }
615
+ return [
616
+ {
617
+ type: eventType,
618
+ frame,
619
+ combatantId: combatant.id
620
+ }
621
+ ];
622
+ }
623
+ function isHitboxActive(record) {
624
+ return record.ageFrames >= record.activeFrames.start && record.ageFrames <= record.activeFrames.end;
625
+ }
626
+ function normalizeActiveFrames(value) {
627
+ const start = nonNegativeInteger(value.start, "hitbox activeFrames.start");
628
+ const end = nonNegativeInteger(value.end, "hitbox activeFrames.end");
629
+ if (end < start) {
630
+ throw new Error("hitbox activeFrames.end must be greater than or equal to activeFrames.start.");
631
+ }
632
+ return { start, end };
633
+ }
634
+ function requireVolumeKind(volume, kind) {
635
+ if (volume.kind !== kind) {
636
+ throw new Error(`Expected ${kind} volume but received ${volume.kind}.`);
637
+ }
638
+ return volume;
639
+ }
640
+ function contactPoint(attack, defense) {
641
+ return [
642
+ (Math.max(attack.bounds.min[0], defense.bounds.min[0]) + Math.min(attack.bounds.max[0], defense.bounds.max[0])) * 0.5,
643
+ (Math.max(attack.bounds.min[1], defense.bounds.min[1]) + Math.min(attack.bounds.max[1], defense.bounds.max[1])) * 0.5,
644
+ (Math.max(attack.bounds.min[2], defense.bounds.min[2]) + Math.min(attack.bounds.max[2], defense.bounds.max[2])) * 0.5
645
+ ];
646
+ }
647
+ function combatNormal(owner, defender) {
648
+ if (owner.position[0] === defender.position[0]) {
649
+ return [owner.facing, 0, 0];
650
+ }
651
+ return [owner.position[0] < defender.position[0] ? 1 : -1, 0, 0];
652
+ }
653
+ function orientVec3(value, facing) {
654
+ return [value[0] * facing, value[1], value[2]];
655
+ }
656
+ function snapshotCombatant(record) {
657
+ return {
658
+ id: record.id,
659
+ ...(record.team === undefined ? {} : { team: record.team }),
660
+ position: cloneVec3(record.position),
661
+ facing: record.facing,
662
+ health: record.health,
663
+ maxHealth: record.maxHealth,
664
+ guard: record.guard,
665
+ maxGuard: record.maxGuard,
666
+ meter: record.meter,
667
+ blocking: record.blocking,
668
+ invulnerableFrames: record.invulnerableFrames,
669
+ hitStopFrames: record.hitStopFrames,
670
+ hitstunFrames: record.hitstunFrames,
671
+ blockstunFrames: record.blockstunFrames,
672
+ recoveryFrames: record.recoveryFrames,
673
+ knockedOut: record.knockedOut
674
+ };
675
+ }
676
+ function snapshotHitbox(record) {
677
+ return {
678
+ id: record.id,
679
+ ownerId: record.ownerId,
680
+ moveId: record.moveId,
681
+ ageFrames: record.ageFrames,
682
+ activeFrames: record.activeFrames,
683
+ recoveryFrames: record.recoveryFrames,
684
+ hitCount: record.hitCount,
685
+ expired: record.expired
686
+ };
687
+ }
688
+ function cloneCombatEvent(event) {
689
+ switch (event.type) {
690
+ case "hit":
691
+ return {
692
+ ...event,
693
+ knockback: cloneVec3(event.knockback),
694
+ point: cloneVec3(event.point),
695
+ normal: cloneVec3(event.normal)
696
+ };
697
+ case "blocked":
698
+ return {
699
+ ...event,
700
+ knockback: cloneVec3(event.knockback),
701
+ point: cloneVec3(event.point),
702
+ normal: cloneVec3(event.normal)
703
+ };
704
+ case "pushbox-overlap":
705
+ return {
706
+ ...event,
707
+ normal: cloneVec3(event.normal)
708
+ };
709
+ default:
710
+ return { ...event };
711
+ }
712
+ }
713
+ function compareCombatants(a, b) {
714
+ return compareIds(a.id, b.id);
715
+ }
716
+ function compareHitboxes(a, b) {
717
+ if (a.priority !== b.priority) {
718
+ return b.priority - a.priority;
719
+ }
720
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
721
+ }
722
+ function compareResolvedVolumes(a, b) {
723
+ const left = `${a.kind}:${a.id}`;
724
+ const right = `${b.kind}:${b.id}`;
725
+ if (left === right) {
726
+ return 0;
727
+ }
728
+ return left < right ? -1 : 1;
729
+ }
730
+ function compareIds(a, b) {
731
+ const left = String(a);
732
+ const right = String(b);
733
+ if (left === right) {
734
+ return 0;
735
+ }
736
+ return left < right ? -1 : 1;
737
+ }
738
+ function finite(value, name) {
739
+ if (!Number.isFinite(value)) {
740
+ throw new Error(`${name} must be finite.`);
741
+ }
742
+ return value;
743
+ }
744
+ function nonNegativeFinite(value, name) {
745
+ if (!Number.isFinite(value) || value < 0) {
746
+ throw new Error(`${name} must be finite and non-negative.`);
747
+ }
748
+ return value;
749
+ }
750
+ function positiveFinite(value, name) {
751
+ if (!Number.isFinite(value) || value <= 0) {
752
+ throw new Error(`${name} must be finite and positive.`);
753
+ }
754
+ return value;
755
+ }
756
+ function nonNegativeInteger(value, name) {
757
+ if (!Number.isInteger(value) || value < 0) {
758
+ throw new Error(`${name} must be a non-negative integer.`);
759
+ }
760
+ return value;
761
+ }
762
+ function positiveInteger(value, name) {
763
+ if (!Number.isInteger(value) || value <= 0) {
764
+ throw new Error(`${name} must be a positive integer.`);
765
+ }
766
+ return value;
767
+ }
768
+ function clampNonNegative(value, max) {
769
+ return Math.max(0, Math.min(max, nonNegativeFinite(value, "combatant value")));
770
+ }
771
+ //# sourceMappingURL=HitboxWorld.js.map