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
CollisionResolvernamespace 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 viapierceCount). - Per-target contact-cooldown bookkeeping for sustained line weapons via
b._contactCooldowns: Map<enemy, timer>inresolveLineCollision. Default cooldownb._contactCooldownSec || 0.3seconds. - Spawn-immunity gating: enemies with
_spawnT > 0.15are 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 atramThreshold, T-bone detection on forward arc. - AoE blast helper
_applyExplosiveBlastforarch === 'explosive' | 'homing'bullets: 50% direct damage with linear distance falloff (1 - 0.7 * dist/blastR). - Impact VFX helper
_spawnImpactVfxfor all three player-hit paths: universal ring, throttled camera shake perWEAPON_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
_lastImpactShakeMswithIMPACT_SHAKE_THROTTLE_MS = 50. - Exported compatibility wrapper
updateCollision()and stubapplyChainExplosion()(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_MAPfordamageTag,secondaryDamageTag,hitShake.amp,hitShake.dur.ENEMY_PROJ_ARM_DISTANCEfor enemy-bullet arming distance.getEnemyCollisionRadius(e)for narrow-phase radius lookup.enemyGridfor 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 (with0.35HP multiplier for_machineGunshooter 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(...)andParticles.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 whiteT-BONEDlabel.Camera.shake(amp, dur)(throttled byIMPACT_SHAKE_THROTTLE_MS).Juice.fire('player_hit' | 'ram_hit_heavy' | 'ram_hit_light').RapierShip.teleportKeepVelocityandRapierShip.setLinvelto sync ship state back to the Rapier body after body-collision resolution.spawnFlameZone(x, y, 30, dps, 0.4)for thelgd_incendiary_rifleImpact Ember echo.- Mutates bullets:
b.l = 0to 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,_chainFirstResolvedfor thechain_sequentialbehavior inbullets.tsto 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) andx/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 (
barrierweapon) — that bullet_collisionModepath early-returns; arc-segment geometry is owned by theshield_arcbehavior. - Does not advance the chain past its first target —
resolveChainArconly resolves the first hit, sets_chainFirstResolved = true, then hands off to thechain_sequentialbehavior inbullets.tswhich runs one jump per frame. - Does not currently apply enemy-enemy separation.
separateEnemiesexists and is fully implemented (grid + O(n²) fallback) but is intentionally not called fromresolve()— 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
_applyExplosiveBlastnotes “crate pool destroys on ship contact only.” - Does not apply the
bigger_areahorizontal bonus — both_applyBonusExplosionandapplyChainExplosionare 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.invulnerableis set or while the ship is dead. - Does not exit early when grid is empty — every grid query has an
enemiesfallback for unit tests that call collision methods directly without a frame-levelresolve()call. - Does not consume
dtin player-bullet, enemy-bullet, or beam/chain paths — onlyresolveShipEnemyBodyCollisionsandseparateEnemiesneed a timestep (for knockback and decay).
Signals
- Fires
bullet_hit(eid, 0, dmg, 0, damageTag) on direct projectile-vs-enemy hits inresolvePlayerBulletEnemyCollisions. Fires a secondbullet_hitwithsecondaryDamageTagif the weapon spec has one (legendary dual-tag weapons). - Fires
tbone_hit(0, 0, x, y, ‘tbone’ | ‘ram’) on ship ram contact —tbonewhen the enemy is in the forward ±40° arc,ramotherwise. - Does not fire
bullet_hitfromresolveBeamTrace,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 callsresolvePlayerBulletEnemyCollisions,resolveEnemyBulletPlayerCollisions,resolveShipEnemyBodyCollisionsin that order, then decays four per-enemy timers (_hitImmune,_contactCooldown,_immuneTextCooldown,_tbonedTextCooldown).updateCollision(world, ship, game, dt = 0.016)— exported wrapper that callsCollisionResolver.resolvefor 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 queriesenemyGridand falls back to the fullworld.enemieslist if the query returns empty, so unit tests can call any sub-method standalone. - Mode-dispatch table:
b._collisionModeis checked at the top ofresolvePlayerBulletEnemyCollisionsand routes toresolveBeamTrace/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(the30covers 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.15are filtered at grid-insert time inrebuildEnemyGrid, and re-checked explicitly inresolveLineCollisionand_applyExplosiveBlastbecause 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 / totalMassandenemyFrac = 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 onshipSpeed >= RAM_THRESHOLD(default 150) and scales linearly fromRAM_DMG_LOtoRAM_DMG_HIover the range[RAM_THRESHOLD, 5000]. - Stub functions
_applyBonusExplosionandapplyChainExplosionare kept as call sites in the hit pipeline so the horizontal can be re-enabled without touching the resolver loop. NATIVE_AOE_MODESset is declared but unused inside the resolver — it’s a marker for upstreambigger_arealogic that excludes weapons with native expanding AoE.