Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code

Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.

Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
  blast radius detonated at the hull. Armed is now the one owner of the arming
  rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
  point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
  OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
  throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
  and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
  Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
  recomputing visible, in-range and locked; FireControlDiagnostic drops those
  three fields and the debug HUD shows the one. Combat's group range test is
  FireControl.InReach, the range half of Designated (a LockWeapon must be
  activated to acquire its lock, so Combat cannot ask Designated itself).

Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 20:31:59 +02:00
commit 57c2560fc0
8 changed files with 325 additions and 69 deletions

View file

@ -194,7 +194,9 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
public override void Activate()
{
if(!_firing && !_reloading)
// A refused beam never starts (operator ruling 2026-09-30): OnStartFiring drives the beam's visuals and
// audio, so starting it only for Execute to stop it a tick later would flash a beam that fires nothing.
if(!_firing && !_reloading && !FireControl.Refuses(this, Entity))
{
_firing = true;
OnStartFiring?.Invoke();

View file

@ -41,6 +41,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
protected readonly InputCapacitor _capacitor = new InputCapacitor();
protected int _burstRemaining;
private bool _burstPaid;
private float _burstTimer;
private float _burstInterval;
protected float _cooldown; // Normalized
@ -107,10 +108,10 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
// whose first round FireControl would refuse.
if (FireControl.Refuses(this, Entity)) return;
// If 1 ammo is consumed per burst, perform ammo and energy consumption here
// UseAmmo returns false when triggering reload; cancel firing if that is the case
if(_data.SingleAmmoBurst && (!TrySpendActivationEnergy() || !UseAmmo())) return;
// A burst that pays once (SingleAmmoBurst) pays at its first round that is not refused, in Execute, judged
// with the Range that round flies with: paying here would judge with the Range of the previous tick, and a
// Range that fell in between would spend the ammo on rounds Fire then refuses.
_burstPaid = false;
_burstRemaining = (int) BurstCount;
_burstInterval = BurstTime / _burstRemaining;
_burstTimer = 0;
@ -234,13 +235,14 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
// spent (operator ruling 2026-09-30): it costs no ammo, energy, sound, heat, wear or visibility.
// The decision is FireControl's; Fire makes the same one.
var refused = FireControl.Refuses(this, Entity);
// If multiple ammo is consumed per burst, perform ammo and energy consumption here
// UseAmmo returns false when triggering reload; cancel firing if that is the case
if (!refused && !_data.SingleAmmoBurst && (!TrySpendActivationEnergy() || !UseAmmo()))
// Ammo and energy are consumed here, per round, or once per burst at its first unrefused round
// (SingleAmmoBurst). UseAmmo returns false when triggering reload; cancel firing if that is the case
if (!refused && !_burstPaid && (!TrySpendActivationEnergy() || !UseAmmo()))
{
_burstRemaining = 0;
return false;
}
if (!refused) _burstPaid = _data.SingleAmmoBurst;
_burstRemaining--;
_burstTimer -= _burstInterval;