PURPOSE

Resolves every per-frame collision interaction in the game: player bullets vs enemies, enemy bullets vs the player ship, and ship-vs-enemy body contact. Exposes CollisionResolver as a namespace object with a top-level resolve() that runs all passes for one frame, rebuilds the enemy spatial grid once for sharing across passes, and dispatches player-bullet hit detection through one of five collision modes attached to each bullet.

OWNS

  • CollisionResolver namespace object and its methods: rebuildEnemyGrid, resolvePlayerBulletEnemyCollisions, resolveBeamTrace, resolveLineCollision, resolveChainArc, resolveEnemyBulletPlayerCollisions, resolveShipEnemyBodyCollisions, separateEnemies, resolve.
  • Per-frame rebuild of enemyGrid (the broad-phase spatial grid) — clears it, then re-inserts every alive, tangible enemy. Filters out enemies with _spawnT > 0.15 (spawn immunity window), _frozenForLag (drained for lag-bar visual only), and _dying (death VFX playing).
  • Mode dispatch on b._collisionMode. Five modes: beam_trace (instant line trace), chain_arc (target-to-target jumps), target_only (locked-target homing only), first_hit (default — bullet dies on first hit), pierce_all (legacy, kept for back-compat via pierceCount).
  • Per-target contact-cooldown bookkeeping for sustained line weapons via b._contactCooldowns: Map<enemy, timer> in resolveLineCollision. Default cooldown b._contactCooldownSec || 0.3 seconds.
  • Spawn-immunity gating: enemies with _spawnT > 0.15 are excluded from every collision check in this file (via grid filtering and explicit re-checks in line/AoE paths).
  • Hit deduplication via b.hits: Set<enemy> for piercing/beam/line bullets and the AoE blast helper.
  • Ship-vs-enemy mass-based momentum exchange (resolveShipEnemyBodyCollisions): mass ratio drives plow-vs-bounce, speed gates ram damage at ramThreshold, T-bone detection on forward arc.
  • AoE blast helper _applyExplosiveBlast for arch === 'explosive' | 'homing' bullets: 50% direct damage with linear distance falloff (1 - 0.7 * dist/blastR).
  • Impact VFX helper _spawnImpactVfx for all three player-hit paths: universal ring, throttled camera shake per WEAPON_MAP[id].hitShake, tag-themed flourishes (bullet/energy/fire/bomb), arch-themed lingers (sniper scar / chain ghost-arc / beam exhaust), blood-splatter exit wounds for bullet-tag hits.
  • Camera-shake throttle _lastImpactShakeMs with IMPACT_SHAKE_THROTTLE_MS = 50.
  • Exported compatibility wrapper updateCollision() and stub applyChainExplosion() (no-op since v3.26).

READS FROM

  • world.playerBullets, world.enemyBullets, world.enemies (WorldState).
  • ship.x, ship.y, ship.vx, ship.vy, ship.alive, ship.invulnerable, ship.radius, ship.outerRadius, ship.hullPoly, ship.shipClass, ship.shipScale, ship.meleeMult, ship.ramThreshold, ship.ramDamageLo, ship.ramDamageHi, ship.contactCooldown.
  • Bullet fields: b.x, b.y, b.vx, b.vy, b.rad, b.dmg, b.l (lifetime), b.c1, b.c2, b.weaponId, b.arch, b.pierceCount, b.blastRadius, b.hits, b._collisionMode, b._targetId, b._beamAngle, b._beamRange, b._beamWidth, b._ball1X/Y, b._ball2X/Y, b._lineThickness, b._contactCooldownSec, b._chainRadius, b._chainAngle, b._chainCount, b._chainFalloff, b._chainJumpDelay, b._acquireRange, b._manualFire, b._chainFirstResolved, b._fizzling, b._dist, b._machineGun, b._sniperBullet, b._hideBeamLine.
  • Enemy fields: e.x, e.y, e.vx, e.vy, e.alive, e.radius, e.eid, e._spawnT, e._frozenForLag, e._dying, e._hitImmune, e._contactCooldown, e._immuneTextCooldown, e._tbonedTextCooldown, e._chargerPhase, e._chargerRecovering, e._lastDmgTag (stamped on hit).
  • WEAPON_MAP for damageTag, secondaryDamageTag, hitShake.amp, hitShake.dur.
  • ENEMY_PROJ_ARM_DISTANCE for enemy-bullet arming distance.
  • getEnemyCollisionRadius(e) for narrow-phase radius lookup.
  • enemyGrid for broad-phase queries (rebuilt by this file).
  • RapierWorld.isReady() to gate physics syncback after body-collision push-apart.
  • performance.now() for camera-shake throttle.

PUSHES TO

  • damageEnemy(e, dmg, game, ship, world) for every player-bullet hit, AoE shrapnel, line tick, chain primary, and ram hit.
  • damagePlayer(ship, dmg, game, hitAngle, hpMult?) for enemy-bullet hits (with 0.35 HP multiplier for _machineGun shooter rounds).
  • Sig.fire('bullet_hit', eid, 0, dmg, 0, tag) once per direct projectile hit, plus a second fire on the secondary damage tag if the weapon spec defines one.
  • Sig.fire('tbone_hit', 0, 0, e.x, e.y, 'tbone' | 'ram') on ship ram hits.
  • telemetry.recordWeaponHit(weaponId, dmg) for every player-bullet damage event (projectile, beam, line, chain primary, AoE shrapnel).
  • PostFx.spawn(...) for 'streak', 'impact_ring', 'scar', 'ghost_arc', 'exhaust'.
  • Particles.add(...) and Particles.burst(...) for sparks, smoke, blood splatter, shrapnel.
  • ExplosionFX.impact(...) for sniper-bullet detonations on player hit.
  • AoeExplosion.spawn(...) for explosive/homing blasts.
  • DmgNumbers.addLabel(...) for the white T-BONED label.
  • Camera.shake(amp, dur) (throttled by IMPACT_SHAKE_THROTTLE_MS).
  • Juice.fire('player_hit' | 'ram_hit_heavy' | 'ram_hit_light').
  • RapierShip.teleportKeepVelocity and RapierShip.setLinvel to sync ship state back to the Rapier body after body-collision resolution.
  • spawnFlameZone(x, y, 30, dps, 0.4) for the lgd_incendiary_rifle Impact Ember echo.
  • Mutates bullets: b.l = 0 to kill (first-hit, no-target chain, fizzle), b.pierceCount--, b.hits.add(e).
  • Mutates bullets in resolveChainArc: writes _chainSourceX/Y, _chainCurrentDmg, _chainJumpsLeft, _chainHitSet, _chainOriginX/Y, _chainMaxTotalRange, _chainTimer, _chainFirstResolved for the chain_sequential behavior in bullets.ts to continue jumps.
  • Mutates enemies: _lastDmgTag (damage-source tag), vx/vy (knockback and separation), x/y (positional push-apart from ship), _contactCooldown (post-ship-hit cooldown).
  • Mutates ship vx/vy (speed bleed on ram/contact) and x/y (push-apart on body collision).

DOES NOT

  • Does not handle bullet-terrain or bullet-wall collisions despite the header comment — only bullet-enemy, enemy-bullet-player, and ship-enemy body collisions exist here. Terrain handling lives elsewhere.
  • Does not handle the shield arc (barrier weapon) — that bullet _collisionMode path early-returns; arc-segment geometry is owned by the shield_arc behavior.
  • Does not advance the chain past its first target — resolveChainArc only resolves the first hit, sets _chainFirstResolved = true, then hands off to the chain_sequential behavior in bullets.ts which runs one jump per frame.
  • Does not currently apply enemy-enemy separation. separateEnemies exists and is fully implemented (grid + O(n²) fallback) but is intentionally not called from resolve() — the comment notes “Enemies pass through each other. Flock behaviors (orb boids) handle group spacing naturally.”
  • Does not damage destructibles via AoE anymore — the comment in _applyExplosiveBlast notes “crate pool destroys on ship contact only.”
  • Does not apply the bigger_area horizontal bonus — both _applyBonusExplosion and applyChainExplosion are no-op stubs since v3.26; the horizontal was removed and is “expected to return via artifacts or ship stats.”
  • Does not collide with enemies during their spawn window (_spawnT > 0.15), drain animation (_frozenForLag), or death VFX (_dying).
  • Does not collide enemy bullets that haven’t armed (_dist < ENEMY_PROJ_ARM_DISTANCE) or are fizzling (_fizzling).
  • Does not collide with the player while ship.invulnerable is set or while the ship is dead.
  • Does not exit early when grid is empty — every grid query has an enemies fallback for unit tests that call collision methods directly without a frame-level resolve() call.
  • Does not consume dt in player-bullet, enemy-bullet, or beam/chain paths — only resolveShipEnemyBodyCollisions and separateEnemies need a timestep (for knockback and decay).

Signals

  • Fires bullet_hit (eid, 0, dmg, 0, damageTag) on direct projectile-vs-enemy hits in resolvePlayerBulletEnemyCollisions. Fires a second bullet_hit with secondaryDamageTag if the weapon spec has one (legendary dual-tag weapons).
  • Fires tbone_hit (0, 0, x, y, ‘tbone’ | ‘ram’) on ship ram contact — tbone when the enemy is in the forward ±40° arc, ram otherwise.
  • Does not fire bullet_hit from resolveBeamTrace, resolveLineCollision, resolveChainArc, or _applyExplosiveBlast — those paths log telemetry directly but do not push a signal.

Entry points

  • CollisionResolver.resolve(world, ship, game, dt = 0.016) — top-level per-frame pass. Rebuilds the spatial grid, then calls resolvePlayerBulletEnemyCollisions, resolveEnemyBulletPlayerCollisions, resolveShipEnemyBodyCollisions in that order, then decays four per-enemy timers (_hitImmune, _contactCooldown, _immuneTextCooldown, _tbonedTextCooldown).
  • updateCollision(world, ship, game, dt = 0.016) — exported wrapper that calls CollisionResolver.resolve for legacy callers.
  • All sub-methods (resolveBeamTrace, resolveChainArc, resolveLineCollision, separateEnemies, rebuildEnemyGrid) are public on the namespace and callable directly by tests.
  • applyChainExplosion(...) — exported (currently a no-op stub) for the chain behavior to call at each jump hit.

Pattern notes

  • Single per-frame grid rebuild in resolve() is shared across all collision passes — every sub-method queries enemyGrid and falls back to the full world.enemies list if the query returns empty, so unit tests can call any sub-method standalone.
  • Mode-dispatch table: b._collisionMode is checked at the top of resolvePlayerBulletEnemyCollisions and routes to resolveBeamTrace / resolveChainArc / resolveLineCollision, otherwise falls through to the inline first-hit / target-only loop. The 'barrier' weapon id and the dual-ball Tesla detection (b._ball1X !== undefined) are also routed as special cases before the default loop.
  • Beam-trace and line-collision both reduce broad-phase via a single midpoint grid query with radius len/2 + width + 30 (the 30 covers the largest enemy radius).
  • Per-target contact cooldown lives on the bullet (b._contactCooldowns: Map) rather than the enemy — keeps per-enemy state out of the entity and lets multiple line weapons hit the same enemy independently.
  • Spawn-immunity is checked in two layers: enemies with _spawnT > 0.15 are filtered at grid-insert time in rebuildEnemyGrid, and re-checked explicitly in resolveLineCollision and _applyExplosiveBlast because those paths may fall back to the raw enemy list when the grid is empty.
  • b.hits: Set<enemy> is the universal dedup for piercing/beam/line/AoE — a hit-once enemy is added immediately so subsequent passes (beam frame two, AoE on top of direct hit) don’t double-count.
  • Camera shake is globally throttled by _lastImpactShakeMs (50ms minimum gap) so high-RPM weapons can’t strobe the camera.
  • The mass-based body collision uses shipFrac = enemyMass / totalMass and enemyFrac = shipMass / totalMass — the lighter party absorbs more of the push, so a heavy ship plows through light enemies and a light ship bounces off heavy ones. Ram damage gates on shipSpeed >= RAM_THRESHOLD (default 150) and scales linearly from RAM_DMG_LO to RAM_DMG_HI over the range [RAM_THRESHOLD, 5000].
  • Stub functions _applyBonusExplosion and applyChainExplosion are kept as call sites in the hit pipeline so the horizontal can be re-enabled without touching the resolver loop.
  • NATIVE_AOE_MODES set is declared but unused inside the resolver — it’s a marker for upstream bigger_area logic that excludes weapons with native expanding AoE.