PURPOSE
Maintains a Rapier2D kinematic sensor body for every alive enemy in the world so the physics engine can emit intersection events when an enemy overlaps the ship collider. Bodies are pooled by enemy object identity: created on first sync, position-updated each frame, removed when the enemy stops being collidable. Sensors do not produce a physics response; they only detect overlap. The actual damage and push-apart response runs elsewhere in the collision resolver.
OWNS
enemyBodies:Map<any, EnemyPhysics>— primary pool, keyed by the live enemy object reference, holding theRAPIER.RigidBodyandRAPIER.Colliderpair for that enemy.handleToEnemy:Map<number, any>— reverse lookup from collider handle back to the enemy object, used when processing intersection events from the world step.- The
EnemyPhysicsshape{ body, collider }. - The public module object
RapierEnemieswith methodssync,getEnemyByHandle,clear,getCount,getPhysics.
READS FROM
RapierWorld— callsisReady()andgetWorld()to access the active Rapier world; bails out silently if the world is not ready.COLLISION_GROUP_ENEMYfrom./rapier-ship— applied viasetCollisionGroupsso enemy sensors filter into the correct interaction layer with the ship body.@dimforge/rapier2d-compat— the Rapier API surface (RigidBodyDesc.kinematicPositionBased,ColliderDesc.ball,ActiveEvents.COLLISION_EVENTS).- Each enemy object:
e.alive,e.x,e.y, and the soft-skip flagse._spawnT,e._frozenForLag,e._dying. - A caller-supplied
getRadius(e)callback to size each ball collider at creation time.
PUSHES TO
- The Rapier world: creates kinematic rigid bodies (
world.createRigidBody), creates sensor ball colliders (world.createCollider), drives positions viabody.setNextKinematicTranslation, and removes bodies viaworld.removeRigidBodyon death orclear(). - The two internal maps (
enemyBodies,handleToEnemy) — inserted on body creation, deleted on per-enemy removal and on fullclear().
The module does not push to enemy objects, the ship, the bridge, or any rendering pipeline. Damage propagation happens downstream when the collision resolver reads intersection events and uses getEnemyByHandle to map handles back to enemies.
DOES NOT
- Does not perform damage, knockback, or any gameplay response — sensors are non-physical and only emit events.
- Does not push or steer enemies; positions are driven by AI and copied in. The Rapier body is kinematic-position-based, so it never reacts to forces.
- Does not handle the ship body, projectiles, destructibles, or arena walls.
- Does not resize an existing collider when an enemy’s radius changes; radius is sampled once at body creation.
- Does not process intersection events itself or read
world.intersectionPairsWith— that is the collision resolver’s job. - Does not persist anything; all state is in-process and is wiped on
clear(). - Does not log, render, or telemetry-report.
Signals
- Body creation: triggered when
syncsees an alive, collidable enemy that has no entry inenemyBodies. - Body removal: triggered when an enemy previously in
enemyBodiesis absent from the per-framealiveset — happens whene.aliveflips false,_dyingflips true,_frozenForLagflips true, or_spawnTexceeds0.15. - Position update: each frame for every alive enemy with an existing body, via
setNextKinematicTranslation. Rapier interpolates this on the next step. - Full pool drop:
clear()is called externally on level transitions and arena resets (sealed arena, bridge teardown points). - Intersection events: the sensor collider has
ActiveEvents.COLLISION_EVENTSset, so Rapier will emit start/stop intersection pairs for it; the resolver consumes those viagetEnemyByHandle.
Entry points
RapierEnemies.sync(enemies, getRadius)— primary tick entry called frombridge.tsonce per physics step withworld.enemiesandgetEnemyCollisionRadius.RapierEnemies.getEnemyByHandle(handle)— used by collision-event consumers to turn a Rapier collider handle into the enemy object.RapierEnemies.clear()— called from sealed-arena teardown and from bridge reset paths (level change, full reset).RapierEnemies.getCount()— used by the Rapier debug render overlay to display the active sensor count.RapierEnemies.getPhysics(enemy)— direct accessor for the body/collider pair for advanced queries.
Pattern notes
- Pool-by-identity: the enemy object reference itself is the map key, so the module does not need an explicit id field on enemies. Identity equality is the contract — re-using or re-pooling enemy objects elsewhere will collide with this map.
- Two-pass sync: first pass walks
enemiesand either updates an existing body or creates a new one while building analiveset; second pass walks theenemyBodiesmap and removes any entry not inalive. This keeps the pool exactly equal to the set of collidable enemies each frame. - Soft-skip predicates (
!e.alive,_spawnT > 0.15,_frozenForLag,_dying) intentionally treat “not currently collidable” the same as “dead” for the purposes of the body pool — a frozen or dying enemy is removed from Rapier and re-created if it becomes collidable again. - Kinematic-position-based bodies are driven via
setNextKinematicTranslation, which is the Rapier-recommended way to feed externally simulated motion through the solver without losing intersection events. - Sensor colliders (
setSensor(true)) generate intersection events but no contact force — the existing push-apart logic in the collision resolver provides the response, so the Rapier integration adds detection only. - Reverse-lookup map is kept in lockstep with the primary pool: both entries are added on creation and both are deleted on removal, including in
clear(). - The module guards every public method against
RapierWorld.isReady()so it is safe to call before the Rapier world is initialized.