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.
| Field | Source | Meaning |
|---|---|---|
_orbitRadius | spec orbitRadius, scaled per level | Distance from ship center to the blade tip |
_bladeSize | spec bladeSize, scaled per level | Contact-detection radius of the blade tip |
TIP_EXTENSION | constant in bullets.ts (20) | Fixed forward grace added to the max hit radius |
_prevOrbitAngle | bullet state | Blade angle at the previous tick |
_orbitAngle | bullet state | Blade angle this tick |
Per-tick the wedge is defined by:
| Quantity | Formula |
|---|---|
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 tolerance | asin(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:
- Its center-to-ship distance minus its radius is ≤
hitRMax(radial check). - 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:
| Phase | Formula | Notes |
|---|---|---|
| Accel ramp pct | accelPct = min(1, lifePct / 0.7) | Ramps over the first 70% of bullet lifetime |
| Speed multiplier | speedMult = 0.3 + 0.7 × accelPct^0.6 | Starts at 0.3, eases to 1.0 |
| Angular velocity | orbitSpeed = _orbitSpeed × speedMult | rad/s |
| Visual fade | phaseAlpha ramps 1 → 0 over last 15% of life | Visual 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:
| Aspect | Behavior |
|---|---|
collisionMode: 'pierce_all' | Spec value; orbit hitbox ignores it and is handled entirely inside the orbit behavior, not by the standard collision resolver |
canHitDestructibles: true | Spec value; honored because the behavior calls damageEnemy for any entity in the wedge passing the e.alive check |
| Enemy gates | Skipped if not alive, frozen for lag, dying, or within first 0.15 s of spawn (_spawnT > 0.15) |
| Impact VFX | One 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.
| Weapon | family | damageTag | bulletArchetype | L1 orbitRadius (px) | L1 orbitSpeed (rad/s) | L1 bladeSize (px) | bladeCount progression | contactCooldown (s) |
|---|---|---|---|---|---|---|---|---|
sweep | sweep | energy | sweep_laser | 190 | 4.0 | 3 | [[1, 1]] (always 1 blade) | 0.18 |
fire_ring | sweep | fire | sweep_laser | 130 | 5.0 | 8 | [[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.