379 lines
16 KiB
C#
379 lines
16 KiB
C#
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Xunit;
|
|
using static CultMath.math;
|
|
using float2 = CultMath.float2;
|
|
using float3 = CultMath.float3;
|
|
|
|
// The command seam, player half (aetheria-arena, cut command-seam-player): the player's input writes only its entity's
|
|
// PilotCommand, as the agents do, and PilotApplier lands it at the next zone step. Its target presses are verbs the entity's
|
|
// chooser answers, its stance and aim-cycle presses are one-shots, its helm and tractor are held, and its triggers are
|
|
// edges: a press is one pull and a let-go one release.
|
|
public sealed partial class RunStartTests
|
|
{
|
|
// East, west 200 away and a third about 277 away, both ahead of east and seen by it once the zone has stepped.
|
|
private (Zone arena, Ship east, Ship west, Ship third) PlayerSeamTrio()
|
|
{
|
|
Ship third = null;
|
|
var (arena, east, west) = SeamPair(false, (stage, e, w) =>
|
|
third = stage.Place(EvLonginus(stage), float2(-150, 120), facing: float2(1, 0)) as Ship);
|
|
for (var step = 0; step < 600 && !(east.VisibleEntities.Contains(west) && east.VisibleEntities.Contains(third)); step++) arena.Update(SeamDt);
|
|
Assert.Contains(west, east.VisibleEntities);
|
|
Assert.Contains(third, east.VisibleEntities);
|
|
return (arena, east, west, third);
|
|
}
|
|
|
|
[Fact]
|
|
public void HelmDemandIsAnIntent()
|
|
{
|
|
var (arena, east, _) = SeamPair(false);
|
|
var held = new HelmInput { Held = true };
|
|
var heading = float2(0, 1); // a quarter turn from its facing
|
|
Assert.Equal(Steering.Toward(east, heading), held.Demand(heading, .5f).Resolve(east));
|
|
Assert.Equal(.5f, new HelmInput().Demand(heading, .5f).Resolve(east));
|
|
Assert.Equal(-1f, new HelmInput().Demand(heading, -4f).Resolve(east));
|
|
|
|
// One face-aim demand, held: it re-resolves against the hull at every step as the hull comes round.
|
|
east.Command.Turn = held.Demand(heading, 0);
|
|
var turns = new List<float>();
|
|
for (var step = 0; step < 600; step++)
|
|
{
|
|
var expected = Steering.Toward(east, heading);
|
|
arena.Update(SeamDt);
|
|
Assert.Equal(expected, east.Turn);
|
|
turns.Add(east.Turn);
|
|
}
|
|
Assert.True(turns.Distinct().Count() > 2, "the resolved turn changes as the hull turns");
|
|
}
|
|
|
|
[Fact]
|
|
public void TargetVerbsApplyOnceAtTheStep()
|
|
{
|
|
var (arena, east, west, third) = PlayerSeamTrio();
|
|
Assert.True(east.Target.Value.IsNone);
|
|
|
|
// The last verb in a step wins: Previous would pick the farthest (third), Next picks the nearest.
|
|
east.Command.Verb(TargetVerb.Previous);
|
|
east.Command.Verb(TargetVerb.Next);
|
|
Assert.True(east.Target.Value.IsNone, "nothing lands before the step");
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
|
|
east.Command.Verb(TargetVerb.Next);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(third, east.Target.Value.Entity);
|
|
|
|
// With nothing held, Previous picks the farthest and Next the nearest.
|
|
east.SetTarget(TargetRef.None);
|
|
east.Command.Verb(TargetVerb.Previous);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(third, east.Target.Value.Entity);
|
|
|
|
// Enemies only: the farther hostile, not the nearer neutral (Next, from nothing, would pick the neutral).
|
|
east.SetTarget(TargetRef.None);
|
|
east.SetIff(third, true);
|
|
east.Command.Verb(TargetVerb.NearestEnemy);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(third, east.Target.Value.Entity);
|
|
|
|
// The reticle picks along the aim, and picking the held target again clears it (no other verb answers None here).
|
|
east.Command.Verb(TargetVerb.Previous);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
east.Command.Aim = float3(-1, 0, 0);
|
|
east.Command.Verb(TargetVerb.UnderReticle);
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.Target.Value.IsNone);
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.Target.Value.IsNone, "the reticle press is not replayed");
|
|
}
|
|
|
|
[Fact]
|
|
public void IffAndCycleAimAreOneShots()
|
|
{
|
|
var (arena, east, west, _) = PlayerSeamTrio();
|
|
Assert.False(east.IsHostileTo(west));
|
|
east.Command.Iff(west, true);
|
|
Assert.False(east.IsHostileTo(west), "nothing lands before the step");
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.IsHostileTo(west));
|
|
// Not replayed: a stance set after the step stays.
|
|
east.SetIff(west, false);
|
|
arena.Update(SeamDt);
|
|
Assert.False(east.IsHostileTo(west));
|
|
east.Command.Iff(west, true);
|
|
east.Command.Iff(west, null);
|
|
arena.Update(SeamDt);
|
|
Assert.False(east.IsHostileTo(west), "the later stance in a step lands last");
|
|
|
|
var first = SeamGun(west).Item;
|
|
var second = west.Weapons.Select(w => w.Item).First(i => i != first && i.Data.Name == first.Data.Name);
|
|
east.RecordReveal(west, first, SignatureChannel.Emission, west.ClaimOf(first));
|
|
east.RecordReveal(west, second, SignatureChannel.Emission, west.ClaimOf(second));
|
|
east.Command.Verb(TargetVerb.Next);
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
var before = east.ResolvedAim;
|
|
|
|
east.Command.CycleAim();
|
|
Assert.Equal(before.Item, east.ResolvedAim.Item);
|
|
arena.Update(SeamDt);
|
|
var once = east.ResolvedAim;
|
|
Assert.True(once.Item != before.Item || once.Mount != before.Mount, "the cycle advanced the aim");
|
|
arena.Update(SeamDt);
|
|
Assert.Equal(once.Item, east.ResolvedAim.Item);
|
|
Assert.Equal(once.Mount, east.ResolvedAim.Mount);
|
|
}
|
|
|
|
private void SeamReveal(Ship east, Ship west, out EquippedItem first, out EquippedItem second)
|
|
{
|
|
first = SeamGun(west).Item;
|
|
var one = first;
|
|
second = west.Weapons.Select(w => w.Item).First(i => i != one && i.Data.Name == one.Data.Name);
|
|
east.RecordReveal(west, first, SignatureChannel.Emission, west.ClaimOf(first));
|
|
east.RecordReveal(west, second, SignatureChannel.Emission, west.ClaimOf(second));
|
|
}
|
|
|
|
private static void SeamAimEqual(AimTarget expected, AimTarget actual)
|
|
{
|
|
Assert.Equal(expected.Item, actual.Item);
|
|
Assert.Equal(expected.Mount, actual.Mount);
|
|
}
|
|
|
|
private static bool SeamAimDiffers(AimTarget a, AimTarget b) => a.Item != b.Item || a.Mount != b.Mount;
|
|
|
|
// Every aim-cycle press in a step cycles: two presses step the aim twice, as two direct cycles did.
|
|
[Fact]
|
|
public void CycleAimCountsEveryPress()
|
|
{
|
|
var (arena, east, west, _) = PlayerSeamTrio();
|
|
SeamReveal(east, west, out var first, out _);
|
|
east.SetTarget(new TargetRef(west));
|
|
Assert.True(east.TrySelectTargetItem(first));
|
|
east.CycleAim();
|
|
var once = east.ResolvedAim;
|
|
east.CycleAim();
|
|
var twice = east.ResolvedAim;
|
|
Assert.True(SeamAimDiffers(once, twice), "fixture: a second cycle moves the aim on");
|
|
|
|
Assert.True(east.TrySelectTargetItem(first));
|
|
east.Command.CycleAim();
|
|
east.Command.CycleAim();
|
|
arena.Update(SeamDt);
|
|
SeamAimEqual(twice, east.ResolvedAim);
|
|
}
|
|
|
|
// Presses in one step apply in a fixed order, whatever order they came in: the designation, then the verb (answered
|
|
// from the designated target), then the item selection, then the aim cycles (stepping from the verb's target and the
|
|
// selected item). Each case is checked against both orders by direct calls, and fails if the two orders agree.
|
|
[Fact]
|
|
public void SameStepPressesApplyDesignationVerbSelectionCycle()
|
|
{
|
|
var (arena, east, west, third) = PlayerSeamTrio();
|
|
SeamReveal(east, west, out var first, out _);
|
|
void Reset(Entity target)
|
|
{
|
|
east.SetTarget(target == null ? TargetRef.None : new TargetRef(target));
|
|
Assert.True(east.TrySelectTargetItem(null));
|
|
}
|
|
|
|
// Next steps on from the designated west (to third), not from nothing (to west).
|
|
Reset(null);
|
|
east.Command.Verb(TargetVerb.Next);
|
|
east.Command.Designate(new TargetRef(west));
|
|
arena.Update(SeamDt);
|
|
Assert.Same(third, east.Target.Value.Entity);
|
|
|
|
// The cycle steps the aim on the target the verb chose.
|
|
Reset(null);
|
|
east.TargetNext();
|
|
east.CycleAim();
|
|
var verbThenCycle = east.ResolvedAim;
|
|
Reset(null);
|
|
east.CycleAim();
|
|
east.TargetNext();
|
|
Assert.True(SeamAimDiffers(verbThenCycle, east.ResolvedAim), "fixture: the verb and the cycle do not commute");
|
|
Reset(null);
|
|
east.Command.CycleAim();
|
|
east.Command.Verb(TargetVerb.Next);
|
|
arena.Update(SeamDt);
|
|
SeamAimEqual(verbThenCycle, east.ResolvedAim);
|
|
|
|
// The cycle steps on from the selected item.
|
|
Reset(west);
|
|
Assert.True(east.TrySelectTargetItem(first));
|
|
east.CycleAim();
|
|
var selectThenCycle = east.ResolvedAim;
|
|
Reset(west);
|
|
east.CycleAim();
|
|
Assert.True(east.TrySelectTargetItem(first));
|
|
Assert.True(SeamAimDiffers(selectThenCycle, east.ResolvedAim), "fixture: the selection and the cycle do not commute");
|
|
Reset(west);
|
|
east.Command.CycleAim();
|
|
east.Command.SelectItem(first);
|
|
arena.Update(SeamDt);
|
|
SeamAimEqual(selectThenCycle, east.ResolvedAim);
|
|
}
|
|
|
|
// A stance press flips the stance on the target as it stands when the press is applied, after the step's target verbs,
|
|
// so two in a step cancel, as two flips in place did before the seam; none is replayed.
|
|
[Fact]
|
|
public void StanceTogglesInAStepCompose()
|
|
{
|
|
var (arena, east, west, third) = PlayerSeamTrio();
|
|
Assert.True(east.Target.Value.IsNone);
|
|
Assert.False(east.IsHostileTo(west));
|
|
// No target is applied yet: the verb in the same step picks the nearest (west), and the press flips it.
|
|
east.Command.Verb(TargetVerb.Next);
|
|
east.Command.ToggleStance();
|
|
Assert.False(east.IsHostileTo(west), "nothing lands before the step");
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
Assert.True(east.IsHostileTo(west), "the press lands on the target the step's verb picked");
|
|
Assert.False(east.IsHostileTo(third));
|
|
east.Command.Iff(west, true);
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.IsHostileTo(west), "a stance set is a set, not a flip");
|
|
|
|
// Next then the press in one step: the press flips the new target, not the one applied when it was pressed.
|
|
east.Command.Verb(TargetVerb.Next);
|
|
east.Command.ToggleStance();
|
|
arena.Update(SeamDt);
|
|
Assert.Same(third, east.Target.Value.Entity);
|
|
Assert.True(east.IsHostileTo(third), "the press flips the new target");
|
|
Assert.True(east.IsHostileTo(west), "the old target is untouched");
|
|
|
|
// Declaring the target neutral drops it (it leaves VisibleEnemies), so a second press in that step finds no
|
|
// target, as a second press in place did before the seam.
|
|
east.Command.ToggleStance();
|
|
east.Command.ToggleStance();
|
|
arena.Update(SeamDt);
|
|
Assert.False(east.IsHostileTo(third));
|
|
Assert.True(east.Target.Value.IsNone);
|
|
|
|
// From neutral, two flips in a step cancel.
|
|
east.Command.Iff(west, false);
|
|
arena.Update(SeamDt);
|
|
east.Command.Designate(new TargetRef(west));
|
|
arena.Update(SeamDt);
|
|
Assert.Same(west, east.Target.Value.Entity);
|
|
east.Command.ToggleStance();
|
|
east.Command.ToggleStance();
|
|
arena.Update(SeamDt);
|
|
Assert.False(east.IsHostileTo(west), "two flips in a step cancel");
|
|
Assert.Equal(false, east.IffOverride(west));
|
|
|
|
east.Command.Designate(new TargetRef(west));
|
|
east.Command.ToggleStance();
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.IsHostileTo(west));
|
|
arena.Update(SeamDt);
|
|
Assert.True(east.IsHostileTo(west), "the press is not replayed");
|
|
}
|
|
|
|
// The player's switches are presses on its command too: each flips at the next step, two in a step cancel, and none is
|
|
// replayed. (The Longinus has no shield: the shield switch is pinned on a shielded
|
|
// target in FireControlCut124Tests below, and here its press is a no-op.)
|
|
[Fact]
|
|
public void PlayerSwitchesArePressesAtTheStep()
|
|
{
|
|
var (arena, east, _) = SeamPair(false);
|
|
Assert.Null(east.Shield);
|
|
Assert.NotNull(east.Sensor);
|
|
(bool, bool) Switches() => (east.OverrideShutdown, east.HeatsinksEnabled);
|
|
Assert.Equal((false, true), Switches());
|
|
|
|
east.Command.ToggleOverrideShutdown();
|
|
east.Command.ToggleHeatsinks();
|
|
east.Command.ToggleShield();
|
|
Assert.Equal((false, true), Switches());
|
|
arena.Update(SeamDt);
|
|
Assert.Equal((true, false), Switches());
|
|
arena.Update(SeamDt);
|
|
Assert.Equal((true, false), Switches());
|
|
|
|
east.Command.ToggleOverrideShutdown();
|
|
east.Command.ToggleOverrideShutdown();
|
|
east.Command.ToggleHeatsinks();
|
|
east.Command.ToggleHeatsinks();
|
|
arena.Update(SeamDt);
|
|
Assert.Equal((true, false), Switches());
|
|
}
|
|
|
|
// A trigger that counts its edges: a semi-auto fires once per Activate.
|
|
private sealed class SeamTrigger : ActivatedBehavior
|
|
{
|
|
public int Pulled, Released;
|
|
public SeamTrigger(EquippedItem item) : base(null, item) { }
|
|
internal override void Activate() => Pulled++;
|
|
internal override void Deactivate() => Released++;
|
|
}
|
|
|
|
// A press is one pull at the next step and the button held after it pulls nothing more, so a semi-auto fires once;
|
|
// the let-go is one release.
|
|
[Fact]
|
|
public void PlayerTriggerIsAnEdge()
|
|
{
|
|
var (arena, east, _) = SeamPair(false);
|
|
var trigger = new SeamTrigger(SeamGun(east).Item);
|
|
east.Command.Pull(trigger);
|
|
Assert.Equal(0, trigger.Pulled);
|
|
for (var step = 0; step < 10; step++) arena.Update(SeamDt);
|
|
Assert.Equal((1, 0), (trigger.Pulled, trigger.Released));
|
|
|
|
east.Command.Release(trigger);
|
|
for (var step = 0; step < 10; step++) arena.Update(SeamDt);
|
|
Assert.Equal((1, 1), (trigger.Pulled, trigger.Released));
|
|
}
|
|
|
|
[Fact]
|
|
public void TractorIsHeld()
|
|
{
|
|
var (arena, east, _) = SeamPair(false);
|
|
east.Command.Tractor = 1;
|
|
for (var step = 0; step < 5; step++)
|
|
{
|
|
east.TractorCommand = 0; // a disturbance between steps: the held command lands again at every step
|
|
arena.Update(SeamDt);
|
|
Assert.Equal(1f, east.TractorCommand);
|
|
}
|
|
east.Command.Tractor = .25f;
|
|
arena.Update(SeamDt);
|
|
Assert.Equal(.25f, east.TractorCommand);
|
|
}
|
|
}
|
|
|
|
// The shield switch on the seam (aetheria-arena, seam fixes): the player's shield press is a PilotCommand press, applied at
|
|
// the step like the other switches. Here because this class builds a shielded target.
|
|
public sealed partial class FireControlCut124Tests
|
|
{
|
|
[Fact]
|
|
public void TheShieldSwitchIsAPressAtTheStep()
|
|
{
|
|
var e = Build(TestSettings(), SolidShape(21, 3), equipShield: true);
|
|
var shield = e.Target.Shield;
|
|
Assert.NotNull(shield);
|
|
Assert.True(shield.Item.Enabled.Value);
|
|
|
|
e.Target.Command.ToggleShield();
|
|
Assert.True(shield.Item.Enabled.Value, "nothing lands before the step");
|
|
e.Zone.Update(.1f);
|
|
Assert.False(shield.Item.Enabled.Value);
|
|
e.Zone.Update(.1f);
|
|
Assert.False(shield.Item.Enabled.Value, "the press is not replayed");
|
|
|
|
e.Target.Command.ToggleShield();
|
|
e.Target.Command.ToggleShield();
|
|
e.Zone.Update(.1f);
|
|
Assert.False(shield.Item.Enabled.Value, "two presses in a step cancel");
|
|
e.Target.Command.ToggleShield();
|
|
e.Zone.Update(.1f);
|
|
Assert.True(shield.Item.Enabled.Value);
|
|
}
|
|
}
|