Orbit swept-arc hitbox

What this is

The orbit behavior used by sweep and fire_ring is a melee-style swing modeled as a rotating blade that pivots around the ship. Each fire cycle spawns bladeCount blades evenly spaced around the orbit and gives them a lifetime tuned to clear one full 360° rotation (oneRotationSec = (2π / orbitSpeed) × 1.30). The blade does not collide as a circle every frame; instead, on every tick, the engine computes the angular wedge the blade tip swept since the previous tick and tests enemies against that wedge. This is the swept-arc hitbox — a continuous-collision approximation that prevents fast-spinning blades from tunnelling past enemies between frames.

The behavior is registered under id orbit in engine/weapons/bullets.ts and is dispatched from weapons.ts when a weapon spec declares behavior: 'orbit'.

Hitbox shape

The hitbox each tick is an angular wedge (annular sector) around the ship, not a point or a circle on the blade itself.

FieldSourceMeaning
_orbitRadiusspec orbitRadius, scaled per levelDistance from ship center to the blade tip
_bladeSizespec bladeSize, scaled per levelContact-detection radius of the blade tip
TIP_EXTENSIONconstant in bullets.ts (20)Fixed forward grace added to the max hit radius
_prevOrbitAnglebullet stateBlade angle at the previous tick
_orbitAnglebullet stateBlade angle this tick

Per-tick the wedge is defined by:

QuantityFormula
Outer radius (hitRMax)_orbitRadius + TIP_EXTENSION + _bladeSize
Wedge start angle_prevOrbitAngle mod 2π
Wedge end angle_orbitAngle mod 2π
Wedge angular width (sweepDelta)(arcEnd − arcStart) mod 2π, clamped to [0, 2π]
Enemy angular toleranceasin(min(1, (enemyRadius + bladeSize) / enemyDist))

There is no inner-radius cull, so the wedge is effectively a pie slice from the ship out to hitRMax. An enemy is hit when:

  1. Its center-to-ship distance minus its radius is ≤ hitRMax (radial check).
  2. Its bearing from the ship falls inside [arcStart, arcStart + sweepDelta] expanded on both sides by the angular tolerance (angular check).

The first frame after spawn the engine sets _prevOrbitAngle = _orbitAngle, so the very first sweep is a single tick of rotation, not a full circle. This prevents recycled bullet-pool slots from leaking the prior bullet’s final angle and instakilling enemies behind the ship on respawn (the fix is documented inline in the behavior).

How sweep coverage is computed

Damage during one fire cycle is not “1 hit per blade”; it is “any enemy whose bearing the blade crosses, throttled per enemy by contactCooldown”.

The angular velocity is not constant. The blade ramps up with a power curve so a swing starts slow and whips through:

PhaseFormulaNotes
Accel ramp pctaccelPct = min(1, lifePct / 0.7)Ramps over the first 70% of bullet lifetime
Speed multiplierspeedMult = 0.3 + 0.7 × accelPct^0.6Starts at 0.3, eases to 1.0
Angular velocityorbitSpeed = _orbitSpeed × speedMultrad/s
Visual fadephaseAlpha ramps 1 → 0 over last 15% of lifeVisual only; damage still applies

The integral of speedMult over [0, 1] lifetime is approximately 0.816. The fire function in weapons.ts therefore sets each bullet’s lifetime = (2π / orbitSpeed) × 1.30, giving roughly 0.816 × 1.30 ≈ 1.06 revolutions per cycle — a full 360° sweep with a small overshoot.

Per-enemy throttling uses a Map<enemy, lastHitTime> (_contactCooldowns) on the bullet. An enemy is skipped this tick if (_orbitAge − lastHit) < _contactCooldownSec, which is sourced from the spec field contactCooldown (0.18 s for both sweep and fire_ring). Within one revolution a slow-moving enemy can be hit once; an enemy that crosses the blade arc multiple times (or sits inside a long sweep) can be hit again after the cooldown expires.

Collision-side notes:

AspectBehavior
collisionMode: 'pierce_all'Spec value; orbit hitbox ignores it and is handled entirely inside the orbit behavior, not by the standard collision resolver
canHitDestructibles: trueSpec value; honored because the behavior calls damageEnemy for any entity in the wedge passing the e.alive check
Enemy gatesSkipped if not alive, frozen for lag, dying, or within first 0.15 s of spawn (_spawnT > 0.15)
Impact VFXOne small spark burst at (ship + cos(eAngle) × min(eDist, orbitR), ship + sin(eAngle) × min(eDist, orbitR)) per accepted hit

Which weapons use it

The orbit swept-arc hitbox is shared by every weapon whose spec has behavior: 'orbit'. As of last_verified_commit, two player weapons match.

WeaponfamilydamageTagbulletArchetypeL1 orbitRadius (px)L1 orbitSpeed (rad/s)L1 bladeSize (px)bladeCount progressioncontactCooldown (s)
sweepsweepenergysweep_laser1904.03[[1, 1]] (always 1 blade)0.18
fire_ringsweepfiresweep_laser1305.08[[1,3],[5,4],[10,5],[15,6],[20,8]]0.18

The orbit_ring behavior (Star Halo legendary) is a different code path — same orbit motion, different collision shape (point bullets fired from a ring), not the swept-arc wedge. Shield’s shield_arc behavior also orbits but uses an arc-segment collision check inside its own behavior, not the wedge described here.