Combat lets go of every held weapon it no longer commands: beams and locks, not only AutoWeapons; its end releases a same-step pull
One sweep over what is held (Firing, or pulled this step): released when uncommanded, and on exit regardless of command. Tests through Minion and Zone.Update for a beam, direct for a lock, the not-the-pick group, the cooling candidate clause by gun kind, the same-step pull on exit, and PilotCommand.Commands. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ca5425fcc
commit
780e68be71
2 changed files with 135 additions and 8 deletions
|
|
@ -161,16 +161,17 @@ public class CombatState : BaseState
|
|||
// Fire charged guns!
|
||||
}
|
||||
|
||||
public override void OnExitState() => ReleaseUnwanted();
|
||||
public override void OnExitState() => ReleaseUnwanted(ending: true);
|
||||
|
||||
// The AI's trigger level for an AutoWeapon is what it commands this step: the weapon holds a down trigger until it is
|
||||
// released, so every held one this step has no command for is let go, whether the target is gone, untracked, out of reach,
|
||||
// its group is not the pick, or Combat has ended. A plain gun is tapped and lets go of itself.
|
||||
private void ReleaseUnwanted()
|
||||
// The AI's trigger level is what it commands this step: a weapon holds a down trigger until it is released (a gun, a lock,
|
||||
// a beam), so every held one this step has no command for is let go, whether the target is gone, untracked, out of reach,
|
||||
// its group is not the pick, or Combat has ended. Ending lets go of every held one, a weapon pulled this very step
|
||||
// (held once the pull is applied) included, since nothing after Combat will command it.
|
||||
private void ReleaseUnwanted(bool ending = false)
|
||||
{
|
||||
var command = _agent.Ship.Command;
|
||||
foreach (var weapon in _agent.Ship.Weapons)
|
||||
if (weapon is AutoWeapon && weapon.Firing && !command.Commands(weapon)) command.Release(weapon);
|
||||
if ((weapon.Firing || command.Pulls(weapon)) && (ending || !command.Commands(weapon))) command.Release(weapon);
|
||||
}
|
||||
|
||||
// Overdrive to close a distant target ahead, through the same consumable gate as the vent (Agent.VentOnTrack).
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
private Rig Build(
|
||||
(ItemRotation Rotation, float Arc)[] mounts = null, bool beam = false, WeaponFuse? fuse = null, float blast = 0f,
|
||||
float range = 100f, float targetRange = 60f, float accuracy = 1f, float minHit = .2f, bool lockWeapon = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false,
|
||||
bool turret = false, float energy = 0f, float heat = 0f, float visibility = 0f, int magazine = 0, bool singleAmmoBurst = false, bool decoy = false, bool charged = false, bool auto = false, float damage = 100f,
|
||||
float effective = float.PositiveInfinity, float damageAtMax = 0f, float velocity = 0f, float targetLayerHeight = 0f)
|
||||
{
|
||||
mounts ??= new[] { (ItemRotation.None, 0f) };
|
||||
|
|
@ -131,7 +131,7 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
behavior = new AutoWeaponData { Count = Constant(1), BurstTime = Constant(0), Cooldown = Constant(1), SingleAmmoBurst = singleAmmoBurst };
|
||||
else
|
||||
behavior = new InstantWeaponData { Count = Constant(1), BurstTime = Constant(0), Cooldown = Constant(1), SingleAmmoBurst = singleAmmoBurst };
|
||||
behavior.Damage = Constant(100);
|
||||
behavior.Damage = Constant(damage);
|
||||
behavior.Range = Constant(range);
|
||||
behavior.MinRange = Constant(0);
|
||||
behavior.EffectiveRange = Constant(effective);
|
||||
|
|
@ -1159,6 +1159,132 @@ public sealed class FireControlPerWeaponTests : IDisposable
|
|||
Assert.InRange(rounds, 8, 10);
|
||||
}
|
||||
|
||||
private static CombatState CombatOf(Rig r) =>
|
||||
new CombatState(new Minion(r.Shooter, FactionDoctrine.Default(r.Shooter.ItemManager.GameplaySettings).Combatant));
|
||||
|
||||
// Every weapon that holds a down trigger is Combat's to let go, not only an AutoWeapon: a beam the pilot holds keeps firing
|
||||
// at nothing otherwise. Through the whole agent (Minion in Zone.Agents, Zone.Update). Kills: a release sweep that
|
||||
// reads one weapon kind.
|
||||
[Theory]
|
||||
[InlineData(Loss.NoTarget)]
|
||||
[InlineData(Loss.Untracked)]
|
||||
public void ACombatPilotThatLosesItsTargetReleasesItsHeldBeam(Loss loss)
|
||||
{
|
||||
var r = Build(beam: true);
|
||||
var beam = (ConstantWeapon) r.Gun.Weapon;
|
||||
r.Zone.Agents.Add(new Minion(r.Shooter, FactionDoctrine.Default(r.Shooter.ItemManager.GameplaySettings).Combatant));
|
||||
const float dt = 1f / 30f;
|
||||
for (var i = 0; i < 30; i++) r.Zone.Update(dt);
|
||||
Assert.True(beam.Firing, "control: the pilot holds the beam while it has a target");
|
||||
|
||||
LoseTheTarget(r, loss);
|
||||
r.Zone.Update(dt);
|
||||
Assert.False(beam.Firing, "the beam is let go the step the target is lost");
|
||||
for (var i = 0; i < 30; i++) r.Zone.Update(dt);
|
||||
Assert.False(beam.Firing);
|
||||
}
|
||||
|
||||
// A held lock launcher is let go the same way.
|
||||
[Fact]
|
||||
public void CombatUpdateReleasesAHeldLockWeaponWhenItHasNoTarget()
|
||||
{
|
||||
var r = Build(lockWeapon: true);
|
||||
var launcher = r.Gun.Weapon;
|
||||
var combat = CombatOf(r);
|
||||
launcher.Activate();
|
||||
Assert.True(launcher.Firing, "control: the trigger is down");
|
||||
LoseTheTarget(r, Loss.NoTarget);
|
||||
|
||||
combat.Update(.1f);
|
||||
r.Zone.Update(.1f);
|
||||
|
||||
Assert.False(launcher.Firing);
|
||||
}
|
||||
|
||||
// The target stands and a group is a candidate, but a stronger group is the pick: the held gun in the other group is let
|
||||
// go the same step. Kills: a final release that runs only when no group is picked.
|
||||
[Fact]
|
||||
public void CombatReleasesAHeldAutoWeaponWhoseGroupIsNotThePick()
|
||||
{
|
||||
var r = Build(auto: true, damage: 40f, decoy: true);
|
||||
var gun = (InstantWeapon) r.Gun.Weapon;
|
||||
var combat = CombatOf(r);
|
||||
gun.Activate();
|
||||
Assert.True(gun.Firing, "control: the trigger is down");
|
||||
|
||||
combat.Update(.1f);
|
||||
|
||||
Assert.True(r.Shooter.Command.Pulls(r.Decoy.Weapon), "control: the stronger group is the pick and fires");
|
||||
Assert.False(r.Shooter.Command.Pulls(gun));
|
||||
r.Zone.Update(.1f);
|
||||
Assert.False(gun.Firing);
|
||||
}
|
||||
|
||||
// Which cooling gun stays a candidate: a held AutoWeapon only. A plain gun that is down and cooling, and an AutoWeapon
|
||||
// let go and cooling, are not, so the ready decoy is the pick; the held AutoWeapon's group is. Kills: dropping the
|
||||
// Firing clause, widening it to every gun, or widening the candidate to every AutoWeapon.
|
||||
[Theory]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(true, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
public void ACoolingGunIsACandidateOnlyWhileAnAutoWeaponIsHeld(bool auto, bool held, bool gunIsThePick)
|
||||
{
|
||||
var r = Build(auto: auto, decoy: true);
|
||||
var gun = (InstantWeapon) r.Gun.Weapon;
|
||||
Aim(r, float2(0, 1));
|
||||
gun.Activate();
|
||||
r.Zone.Update(.1f);
|
||||
if (!held) gun.Deactivate();
|
||||
Assert.False(gun.CanFire, "fixture: the gun fired and is cooling");
|
||||
Assert.Equal(held, gun.Firing);
|
||||
var combat = CombatOf(r);
|
||||
|
||||
combat.Update(.1f);
|
||||
|
||||
var command = r.Shooter.Command;
|
||||
Assert.Equal(gunIsThePick, command.Pulls(gun));
|
||||
Assert.Equal(!gunIsThePick, command.Pulls(r.Decoy.Weapon));
|
||||
}
|
||||
|
||||
// Combat's end lets go of a weapon it pulled this very step, which is not held until the pull is applied.
|
||||
// Kills: an exit sweep that spares what the pilot commanded.
|
||||
[Fact]
|
||||
public void CombatExitReleasesAWeaponItPulledInTheSameStep()
|
||||
{
|
||||
var r = Build(auto: true);
|
||||
var gun = (InstantWeapon) r.Gun.Weapon;
|
||||
var combat = CombatOf(r);
|
||||
|
||||
combat.Update(.1f);
|
||||
Assert.True(r.Shooter.Command.Pulls(gun), "control: Combat pulled the gun this step");
|
||||
combat.OnExitState();
|
||||
r.Zone.Update(.1f);
|
||||
|
||||
Assert.False(gun.Firing);
|
||||
}
|
||||
|
||||
// The pilot has commanded a weapon this step when any trigger, pull or release, names it: not only the last one, and not
|
||||
// only a pull.
|
||||
[Fact]
|
||||
public void ACommandNamesEveryWeaponAnyOfItsTriggersNamed()
|
||||
{
|
||||
var r = Build(mounts: new[] { (ItemRotation.None, 0f), (ItemRotation.None, 0f) }, decoy: true);
|
||||
var command = r.Shooter.Command;
|
||||
var pulled = r.Guns[0].Weapon;
|
||||
var released = r.Guns[1].Weapon;
|
||||
var untouched = r.Decoy.Weapon;
|
||||
Assert.False(command.Commands(pulled));
|
||||
|
||||
command.Pull(pulled);
|
||||
command.Release(released);
|
||||
|
||||
Assert.True(command.Commands(pulled));
|
||||
Assert.True(command.Commands(released));
|
||||
Assert.False(command.Commands(untouched));
|
||||
Assert.True(command.Pulls(pulled));
|
||||
Assert.False(command.Pulls(released));
|
||||
}
|
||||
|
||||
// Kills: Trigger that leaves _burstStarted true from the previous burst, so the next burst reads as started
|
||||
// (and ready) before its first round has flown.
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in a new issue