Evasion r6: drop the aether drive from the envelope; the gemini fixtures fly thruster hulls
Ruling retire-longinusx. AetherDrive.Manoeuvre and the drive line in Ship.RecalculateEnvelope go: the envelope sums thrusters only. TheDrivesReportReadsItsStateAndTheShipsSpeed, the EvGemini fixture and the drive-only asserts are deleted. Sideways jinks and the shooters' target move to the generated Djinni; the gun platforms (FastBlast+-, ColdFire) are EvLonginus, the Duel's guns on a Longinus. TheThreeShipsOrder becomes TheShipsOrder. Not built or run on its own: verified together with the mount term at the head. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
593958ec2d
commit
e360ceeb27
3 changed files with 77 additions and 173 deletions
|
|
@ -126,31 +126,6 @@ public class AetherDrive : Behavior, IPowerConsumer
|
|||
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): Medium -- mobility, same as Thruster.
|
||||
public int DefaultPowerTier => PowerTiers.Medium;
|
||||
|
||||
// What this drive adds to the ship's manoeuvre envelope: Execute's own arithmetic at full input on every axis,
|
||||
// as a rate per second at the rotors' current state (the rotors spend what they hold, so a drive that has been
|
||||
// thrusting reports less). Each axis delivers Rpm * lambda * RotorMass * efficiency / Mass, the efficiency
|
||||
// falling as the ship's speed along that axis approaches the rotor speed, so the push along the current
|
||||
// velocity is smaller than against it; the turn is the z axis through AetherTorqueMultiplier, the same both
|
||||
// ways. Zero when the item is offline or unpowered, the gates Execute applies.
|
||||
public ManoeuvreEnvelope Manoeuvre()
|
||||
{
|
||||
if (Item == null || !Item.Active.Value || Item.PowerSupply <= 1e-4f) return default;
|
||||
var forward = normalize(Entity.Direction);
|
||||
var right = forward.Rotate(ItemRotation.Clockwise);
|
||||
var speed = float2(dot(Entity.Velocity, forward), dot(Entity.Velocity, right));
|
||||
var rotorSpeed = max(Rpm * _data.RotorDiameter / 100, 1);
|
||||
var couplingEfficiency = Evaluate(_data.CouplingEfficiency);
|
||||
var perSecond = Rpm * _data.CouplingLambda * Item.Evaluate(_data.LambdaMultiplier) * _data.RotorMass / Entity.Mass;
|
||||
float Push(float axisRate, float axisSpeed, float axisRotorSpeed, float direction) =>
|
||||
axisRate * saturate(1 - axisSpeed / axisRotorSpeed * direction) * couplingEfficiency;
|
||||
return new ManoeuvreEnvelope(
|
||||
Push(perSecond.x, speed.x, rotorSpeed.x, 1), Push(perSecond.x, speed.x, rotorSpeed.x, -1),
|
||||
Push(perSecond.y, speed.y, rotorSpeed.y, -1), Push(perSecond.y, speed.y, rotorSpeed.y, 1),
|
||||
perSecond.z * ItemManager.GameplaySettings.AetherTorqueMultiplier,
|
||||
perSecond.z * ItemManager.GameplaySettings.AetherTorqueMultiplier);
|
||||
}
|
||||
|
||||
public override bool Execute(float dt)
|
||||
{
|
||||
var rotorSpeed = Rpm * _data.RotorDiameter / 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ public class Ship : Entity
|
|||
#region ThrustCalculation
|
||||
|
||||
// What this ship can do at this instant (see ManoeuvreEnvelope): the sum of what each live propulsor reports with
|
||||
// its own Execute arithmetic. Derived each update, never saved. The aggregates above are not an envelope (they
|
||||
// its own Execute arithmetic; thrusters are its only propulsors. Derived each update, never saved. The aggregates above are not an envelope (they
|
||||
// mix units and cancel a lone off-axis strafer), so nothing in it reads them.
|
||||
public ManoeuvreEnvelope Envelope { get; private set; }
|
||||
|
||||
|
|
@ -153,7 +153,6 @@ public class Ship : Entity
|
|||
{
|
||||
var envelope = default(ManoeuvreEnvelope);
|
||||
foreach (var thruster in _allThrusters) envelope += thruster.Manoeuvre();
|
||||
foreach (var drive in _aetherDrives) envelope += drive.Manoeuvre();
|
||||
Envelope = envelope;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,25 +16,27 @@ using int2 = CultMath.int2;
|
|||
// The evasion term (docs/aetheria-release-map.md, "Evasion is how unpredictably the target can and does change its
|
||||
// motion"): how far a target can and does move off the line of fire in the solution window, one factor of every hit
|
||||
// price. The fixture is RunStartTests': the shipped catalog and the authored settings, so the ships fly in the real
|
||||
// Ship.Update with the real thrusters and aether drive. Ship flights step the ships directly (they are alone in
|
||||
// Ship.Update with the real thrusters. Ship flights step the ships directly (they are alone in
|
||||
// their arena); the one price test that rolls shots steps only FireControl.
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
private const float EvDt = 1f / 60f;
|
||||
|
||||
// The Duel's LonginusX fit: one Traction aether drive that pushes all four ways, with its guns.
|
||||
private static ScenarioFit EvGemini(ScenarioStage stage, string gun = "FastBlast+-") => stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), ItemRotation.None), ("Traction", int2(2, 3), ItemRotation.None), ("Core Power", int2(2, 1), ItemRotation.None),
|
||||
// The Duel's guns on a Longinus: the generated Longinus's thrusters (two Large Drive forward, two Talaria on the
|
||||
// flanks) and power, the Duel's cockpit, guns, launchers and sensor, and the Fire Control Array as its targeting gear.
|
||||
private static ScenarioFit EvLonginus(ScenarioStage stage, string gun = "FastBlast+-") => stage.Fit("Longinus",
|
||||
("Cockpit 2x2", int2(2, 6), ItemRotation.None), ("Core Power", int2(2, 4), ItemRotation.None),
|
||||
("Large Drive", int2(1, 0), ItemRotation.Reversed), ("Large Drive", int2(3, 0), ItemRotation.Reversed),
|
||||
("Talaria", int2(2, 14), ItemRotation.CounterClockwise), ("Talaria", int2(3, 14), ItemRotation.Clockwise),
|
||||
("GT 3K", int2(0, 5), ItemRotation.None), ("GT 3K", int2(5, 5), ItemRotation.None), (gun, int2(1, 8), ItemRotation.None),
|
||||
(gun, int2(4, 8), ItemRotation.None), ("Iapyx", int2(1, 2), ItemRotation.CounterClockwise), ("Iapyx", int2(4, 2), ItemRotation.Clockwise),
|
||||
("not if i see you first", int2(3, 10), ItemRotation.None), ("PotaT+-", int2(1, 5), ItemRotation.None),
|
||||
("Fire Control Array", int2(2, 5), ItemRotation.None), ("Store-All Plus", int2(2, 8), ItemRotation.None));
|
||||
(gun, int2(4, 8), ItemRotation.None), ("Iapyx", int2(2, 2), ItemRotation.CounterClockwise), ("Iapyx", int2(3, 2), ItemRotation.Clockwise),
|
||||
("not if i see you first", int2(3, 10), ItemRotation.None), ("PotaT+-", int2(1, 6), ItemRotation.None),
|
||||
("Fire Control Array", int2(1, 10), ItemRotation.None), ("Store-All Plus", int2(2, 8), ItemRotation.None));
|
||||
|
||||
// The catalog's three fitted ships, far apart in the fixture arena: the generated Longinus and Djinni at the
|
||||
// fixture seed and the omnidirectional LonginusX.
|
||||
private (Zone arena, Ship longinus, Ship djinni, Ship gemini) EvFleet()
|
||||
// The catalog's two generated ships, far apart in the fixture arena, at the fixture seed.
|
||||
private (Zone arena, Ship longinus, Ship djinni) EvFleet()
|
||||
{
|
||||
Ship longinus = null, djinni = null, gemini = null;
|
||||
Ship longinus = null, djinni = null;
|
||||
var scenario = new Scripted(false, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(-50000, -50000));
|
||||
|
|
@ -42,17 +44,15 @@ public sealed partial class RunStartTests
|
|||
// first, then the Longinus, as the map's probe did (M23).
|
||||
djinni = stage.Place(stage.Generated("Djinni"), float2(-10000, -30000), facing: float2(0, 1)) as Ship;
|
||||
longinus = stage.Place(stage.Generated("Longinus"), float2(-30000, -30000), facing: float2(0, 1)) as Ship;
|
||||
gemini = stage.Place(EvGemini(stage), float2(10000, -30000), facing: float2(0, 1)) as Ship;
|
||||
});
|
||||
var (_, arena, _, failures) = Launch(scenario, Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
Assert.NotNull(longinus);
|
||||
Assert.NotNull(djinni);
|
||||
Assert.NotNull(gemini);
|
||||
return (arena, longinus, djinni, gemini);
|
||||
return (arena, longinus, djinni);
|
||||
}
|
||||
|
||||
// Idle long enough for stats, power and the aether rotors to reach their idle state, then at rest, facing +y,
|
||||
// Idle long enough for stats and power to reach their idle state, then at rest, facing +y,
|
||||
// and, after three seconds of coasting, with nothing observed: the track forgets on its own.
|
||||
private static void EvSettle(Ship ship)
|
||||
{
|
||||
|
|
@ -117,8 +117,7 @@ public sealed partial class RunStartTests
|
|||
foreach (var jink in new[] { EvJink.Lateral, EvJink.Longitudinal, EvJink.Random, EvJink.Weave })
|
||||
foreach (var halfPeriod in new[] { .5f, 1f })
|
||||
{
|
||||
// A fresh ship each flight where the caller wants one: an aether drive heats and spends its rotors, and a
|
||||
// drive that has run itself offline has no envelope.
|
||||
// A fresh ship each flight where the caller wants one: a ship keeps the track its last flight left.
|
||||
var ship = flightShip();
|
||||
EvSettle(ship);
|
||||
EvFly(ship, jink, halfPeriod, 4f);
|
||||
|
|
@ -132,19 +131,17 @@ public sealed partial class RunStartTests
|
|||
return (noseOn, broadside);
|
||||
}
|
||||
|
||||
// Ruling evasion-is-unpredictability: the Gemini is evasive because it can floor it in any direction at any time,
|
||||
// the Longinus needs to aim itself where it wants to go, and the Djinni is somewhere between.
|
||||
// Ruling evasion-is-unpredictability: a ship is evasive by what it can do to its motion at any time. The Longinus has
|
||||
// to aim itself where it wants to go and the Djinni is a lighter ship of small thrusters.
|
||||
[Fact]
|
||||
public void TheThreeShipsOrder()
|
||||
public void TheShipsOrder()
|
||||
{
|
||||
var (_, longinus, djinni, _) = EvFleet();
|
||||
var g = EvBest(() => EvFleet().gemini);
|
||||
var (_, longinus, djinni) = EvFleet();
|
||||
var l = EvBest(() => longinus);
|
||||
var d = EvBest(() => djinni);
|
||||
Console.WriteLine($"EVASION window {_items.GameplaySettings.SolutionWindow}: gemini-like nose-on {g.noseOn:F2} broadside {g.broadside:F2}; " +
|
||||
$"longinus {l.noseOn:F2} {l.broadside:F2}; djinni {d.noseOn:F2} {d.broadside:F2}");
|
||||
Assert.True(g.noseOn > l.noseOn && l.noseOn > d.noseOn, $"nose-on order: {g.noseOn} > {l.noseOn} > {d.noseOn}");
|
||||
Assert.True(g.broadside > l.broadside && l.broadside > d.broadside, $"broadside order: {g.broadside} > {l.broadside} > {d.broadside}");
|
||||
Console.WriteLine($"EVASION window {_items.GameplaySettings.SolutionWindow}: longinus {l.noseOn:F2} {l.broadside:F2}; djinni {d.noseOn:F2} {d.broadside:F2}");
|
||||
Assert.True(l.noseOn > d.noseOn, $"nose-on order: {l.noseOn} > {d.noseOn}");
|
||||
Assert.True(l.broadside > d.broadside, $"broadside order: {l.broadside} > {d.broadside}");
|
||||
// Re-measured (M25): the Djinni's thrusters push 1.7 times harder than the figure the map priced it with, so
|
||||
// it evades more than the map's 1.7 m and the Longinus leads it by less than the map's factor of eight.
|
||||
Assert.True(l.noseOn >= 2 * d.noseOn && l.broadside >= 2 * d.broadside, "the Longinus evades at least twice the Djinni");
|
||||
|
|
@ -248,7 +245,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void ReachIsWhatTheShipCanDo()
|
||||
{
|
||||
var (_, longinus, djinni, _) = EvFleet();
|
||||
var (_, longinus, djinni) = EvFleet();
|
||||
const float window = .5f;
|
||||
var results = new List<(string name, (float noseOn, float broadside) reach, (float noseOn, float broadside) measured)>();
|
||||
foreach (var (name, ship) in new[] { ("longinus", longinus), ("djinni", djinni) })
|
||||
|
|
@ -278,7 +275,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void TheEnvelopeComesFromTheGear()
|
||||
{
|
||||
var (_, longinus, _, gemini) = EvFleet();
|
||||
var (_, longinus, _) = EvFleet();
|
||||
EvSettle(longinus);
|
||||
// A thruster's Thrust property is refreshed only while it fires: turn each way so the flank thrusters hold what
|
||||
// they push with, then read the envelope at rest.
|
||||
|
|
@ -314,13 +311,6 @@ public sealed partial class RunStartTests
|
|||
longinus.Update(EvDt);
|
||||
longinus.Update(EvDt);
|
||||
Assert.InRange(longinus.Envelope.Forward, envelope.Forward * .49f, envelope.Forward * .51f);
|
||||
|
||||
// The LonginusX's Traction drive pushes and turns every way.
|
||||
EvSettle(gemini);
|
||||
gemini.Update(EvDt);
|
||||
var drive = gemini.Envelope;
|
||||
Assert.True(drive.Forward > 1f && drive.Reverse > 1f && drive.Left > 1f && drive.Right > 1f, $"traction: {drive.Forward} {drive.Reverse} {drive.Left} {drive.Right}");
|
||||
Assert.True(drive.Clockwise > 0f && drive.CounterClockwise > 0f);
|
||||
}
|
||||
|
||||
// Which thruster pushes which way and turns which way is read from the gear, not assumed symmetric: destroy the
|
||||
|
|
@ -329,7 +319,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void TheEnvelopeFollowsWhichThrustersAreLeft()
|
||||
{
|
||||
var (_, _, djinni, gemini) = EvFleet();
|
||||
var (_, _, djinni) = EvFleet();
|
||||
var settings = _items.GameplaySettings;
|
||||
EvSettle(djinni);
|
||||
// Refresh every thruster's live Thrust by firing it: strafe, burn and turn each way.
|
||||
|
|
@ -370,14 +360,6 @@ public sealed partial class RunStartTests
|
|||
djinni.MovementDirection = float2(-1, 0);
|
||||
djinni.Update(EvDt);
|
||||
Assert.True(djinni.Acceleration.x < -1f, "it still strafes left");
|
||||
|
||||
// The Traction drive's turn is the rate it turns the hull at when told to turn right.
|
||||
EvSettle(gemini);
|
||||
gemini.MovementDirection = float2(0, 0);
|
||||
gemini.LookDirection = float3(1, 0, 0);
|
||||
gemini.Update(EvDt);
|
||||
var turned = Math.Atan2(gemini.Direction.x, gemini.Direction.y);
|
||||
Assert.InRange((float) turned / EvDt, gemini.Envelope.Clockwise * .9f, gemini.Envelope.Clockwise * 1.1f);
|
||||
}
|
||||
|
||||
// The reach and the evasion read each side of the envelope for themselves: a forward-only ship that turns clockwise
|
||||
|
|
@ -425,7 +407,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void ManoeuvreIsObserved()
|
||||
{
|
||||
var (_, longinus, djinni, gemini) = EvFleet();
|
||||
var (_, longinus, djinni) = EvFleet();
|
||||
|
||||
// Coasting: no acceleration, an all-zero track.
|
||||
EvSettle(longinus);
|
||||
|
|
@ -449,15 +431,6 @@ public sealed partial class RunStartTests
|
|||
djinni.Update(EvDt);
|
||||
Assert.InRange(djinni.Acceleration.y, djinni.Envelope.Forward * .98f, djinni.Envelope.Forward * 1.02f);
|
||||
|
||||
// Strafing at full thrust for one update on the Traction drive: the drive's own push over the step.
|
||||
EvSettle(gemini);
|
||||
gemini.MovementDirection = float2(1, 0);
|
||||
gemini.Update(EvDt);
|
||||
var drive = gemini.GetBehaviors<AetherDrive>().Single();
|
||||
Assert.True(drive.ThrustDirection.x > 0f);
|
||||
Assert.InRange(gemini.Acceleration.x, drive.ThrustDirection.x / EvDt * .999f, drive.ThrustDirection.x / EvDt * 1.001f);
|
||||
Assert.InRange(gemini.Acceleration.x, gemini.Envelope.Right * .95f, gemini.Envelope.Right * 1.05f);
|
||||
|
||||
// The same jink observed at two step sizes reads the same unpredictability.
|
||||
var fine = EvSquare(40, .5f, 4f, 1f / 60f, .75f);
|
||||
var coarse = EvSquare(40, .5f, 4f, 1f / 30f, .75f);
|
||||
|
|
@ -471,34 +444,34 @@ public sealed partial class RunStartTests
|
|||
public void ASteadyBurnAndDitheringAreNotEvasion()
|
||||
{
|
||||
var window = _items.GameplaySettings.SolutionWindow;
|
||||
// A fresh ship for each phase: an aether drive heats and spends its rotors as it flies.
|
||||
var gemini = EvFleet().gemini;
|
||||
float Nose() => FireControl.Evasion(gemini, normalize(gemini.Direction));
|
||||
// A fresh ship for each phase: a ship keeps the track and the heat its last flight left.
|
||||
var djinni = EvFleet().djinni;
|
||||
float Nose() => FireControl.Evasion(djinni, normalize(djinni.Direction));
|
||||
|
||||
EvSettle(gemini);
|
||||
EvFly(gemini, EvJink.Lateral, .5f, 4f);
|
||||
EvSettle(djinni);
|
||||
EvFly(djinni, EvJink.Lateral, .5f, 4f);
|
||||
var jinking = Nose();
|
||||
Assert.True(jinking > 1f, $"fixture: the jink evades ({jinking})");
|
||||
|
||||
gemini = EvFleet().gemini;
|
||||
EvSettle(gemini);
|
||||
gemini.MovementDirection = float2(0, 1);
|
||||
for (var step = 0; step < (int) Math.Round(3 * window / EvDt); step++) gemini.Update(EvDt);
|
||||
djinni = EvFleet().djinni;
|
||||
EvSettle(djinni);
|
||||
djinni.MovementDirection = float2(0, 1);
|
||||
for (var step = 0; step < (int) Math.Round(3 * window / EvDt); step++) djinni.Update(EvDt);
|
||||
Assert.True(Nose() < .05f * jinking, $"a steady burn: {Nose()} against {jinking}");
|
||||
|
||||
gemini = EvFleet().gemini;
|
||||
EvSettle(gemini);
|
||||
EvFly(gemini, EvJink.Lateral, .05f, 4f);
|
||||
djinni = EvFleet().djinni;
|
||||
EvSettle(djinni);
|
||||
EvFly(djinni, EvJink.Lateral, .05f, 4f);
|
||||
Assert.True(Nose() < .25f * jinking, $"a dither: {Nose()} against {jinking}");
|
||||
|
||||
gemini = EvFleet().gemini;
|
||||
EvSettle(gemini);
|
||||
for (var step = 0; step < 240; step++) gemini.Update(EvDt);
|
||||
djinni = EvFleet().djinni;
|
||||
EvSettle(djinni);
|
||||
for (var step = 0; step < 240; step++) djinni.Update(EvDt);
|
||||
Assert.Equal(0f, Nose());
|
||||
}
|
||||
|
||||
// Two shooters on opposite sides of the same target, 200 m away: one with a projectile gun and one with a laser
|
||||
// (a beam's authored velocity is 0, a flight time of zero). The target is the omnidirectional LonginusX.
|
||||
// (a beam's authored velocity is 0, a flight time of zero). The target is the generated Djinni, the catalog's one ship that strafes.
|
||||
private sealed class EvRange
|
||||
{
|
||||
public Zone Arena;
|
||||
|
|
@ -512,9 +485,9 @@ public sealed partial class RunStartTests
|
|||
var scenario = new Scripted(false, stage =>
|
||||
{
|
||||
stage.Player(stage.Bare("Djinni"), float2(-50000, -50000));
|
||||
gunner = stage.Place(EvGemini(stage), float2(0, 0), facing: float2(0, 1)) as Ship;
|
||||
target = stage.Place(EvGemini(stage), float2(0, 200), facing: float2(0, 1)) as Ship;
|
||||
lasing = stage.Place(EvGemini(stage, "ColdFire"), float2(0, 400), facing: float2(0, -1)) as Ship;
|
||||
gunner = stage.Place(EvLonginus(stage), float2(0, 0), facing: float2(0, 1)) as Ship;
|
||||
target = stage.Place(stage.Generated("Djinni"), float2(0, 200), facing: float2(0, 1)) as Ship;
|
||||
lasing = stage.Place(EvLonginus(stage, "ColdFire"), float2(0, 400), facing: float2(0, -1)) as Ship;
|
||||
});
|
||||
var (_, arena, _, failures) = Launch(scenario, Inputs(() => GalaxySeed));
|
||||
Assert.True(failures.Count == 0, string.Join("; ", failures));
|
||||
|
|
@ -640,32 +613,32 @@ public sealed partial class RunStartTests
|
|||
}
|
||||
|
||||
// Finding coasting-memory: a ship that stops jinking stops evading. The track is never reset by hand here: the
|
||||
// Gemini jinks for four seconds, the input is cut, and evasion must read exactly 0 within the window plus the
|
||||
// drive's own spin-down (a tick or two: the probe reads 0.783 s against a window of 0.75), and stay 0, with the moments back at exactly zero.
|
||||
// Djinni jinks for four seconds, the input is cut, and evasion must read exactly 0 within the window plus a tick
|
||||
// or two, and stay 0, with the moments back at exactly zero.
|
||||
[Fact]
|
||||
public void AShipThatStopsJinkingStopsEvading()
|
||||
{
|
||||
var (_, _, _, gemini) = EvFleet();
|
||||
var (_, _, djinni) = EvFleet();
|
||||
var window = _items.GameplaySettings.SolutionWindow;
|
||||
EvSettle(gemini);
|
||||
EvFly(gemini, EvJink.Lateral, 1f, 4f);
|
||||
var jinking = Math.Max(FireControl.Evasion(gemini, float2(0, 1)), FireControl.Evasion(gemini, EvRight(gemini)));
|
||||
Assert.True(jinking > 5f, $"the jink should evade, read {jinking}");
|
||||
EvSettle(djinni);
|
||||
EvFly(djinni, EvJink.Lateral, 1f, 4f);
|
||||
var jinking = Math.Max(FireControl.Evasion(djinni, float2(0, 1)), FireControl.Evasion(djinni, EvRight(djinni)));
|
||||
Assert.True(jinking > 1f, $"the jink should evade, read {jinking}");
|
||||
|
||||
var zeroAt = -1f;
|
||||
const float bound = 2f;
|
||||
for (var step = 1; step * EvDt <= bound + 3f; step++)
|
||||
{
|
||||
gemini.Update(EvDt);
|
||||
var nose = FireControl.Evasion(gemini, float2(0, 1));
|
||||
var side = FireControl.Evasion(gemini, EvRight(gemini));
|
||||
djinni.Update(EvDt);
|
||||
var nose = FireControl.Evasion(djinni, float2(0, 1));
|
||||
var side = FireControl.Evasion(djinni, EvRight(djinni));
|
||||
if (zeroAt < 0f && nose == 0f && side == 0f) zeroAt = step * EvDt;
|
||||
if (zeroAt >= 0f) Assert.True(nose == 0f && side == 0f, $"evasion came back at {step * EvDt:F2} s: {nose}, {side}");
|
||||
}
|
||||
Assert.True(zeroAt >= 0f && zeroAt <= window + 3 * EvDt, $"evasion reached zero at {zeroAt:F2} s, window {window}");
|
||||
Assert.Equal(0f, gemini.Manoeuvre.Moments.x);
|
||||
Assert.Equal(0f, gemini.Manoeuvre.Moments.y);
|
||||
Assert.Equal(0f, gemini.Manoeuvre.Moments.z);
|
||||
Assert.Equal(0f, djinni.Manoeuvre.Moments.x);
|
||||
Assert.Equal(0f, djinni.Manoeuvre.Moments.y);
|
||||
Assert.Equal(0f, djinni.Manoeuvre.Moments.z);
|
||||
}
|
||||
|
||||
// Finding limit-clamp-evasion: a velocity limiter's clamp is the ceiling, not a change of vector. A Longinus
|
||||
|
|
@ -673,7 +646,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void ASpeedAboveTheLimitIsNotEvasion()
|
||||
{
|
||||
var (_, longinus, _, _) = EvFleet();
|
||||
var (_, longinus, _) = EvFleet();
|
||||
EvSettle(longinus);
|
||||
longinus.Velocity = float2(0, 400);
|
||||
// Read every tick: the track forgets within a window, so only the ticks right after the clamp can see it.
|
||||
|
|
@ -693,7 +666,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void TheAggregatesReadLiveThrust()
|
||||
{
|
||||
var (_, _, djinni, _) = EvFleet();
|
||||
var (_, _, djinni) = EvFleet();
|
||||
EvSettle(djinni);
|
||||
djinni.Update(EvDt);
|
||||
Assert.InRange(djinni.ForwardThrust, djinni.Envelope.Forward * djinni.Mass * .99f, djinni.Envelope.Forward * djinni.Mass * 1.01f);
|
||||
|
|
@ -752,7 +725,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void ReachMatchesABruteForceSearchOverTurnStrategies()
|
||||
{
|
||||
var gemini = new ManoeuvreEnvelope(20, 20, 20, 20, 3, 2.5f);
|
||||
var omni = new ManoeuvreEnvelope(20, 20, 20, 20, 3, 2.5f);
|
||||
var longinus = new ManoeuvreEnvelope(50, 0, 23, 23, 2.3f, 2.3f);
|
||||
var lopsided = new ManoeuvreEnvelope(30, 10, 5, 15, 2, 1.4f);
|
||||
var forwardOnly = new ManoeuvreEnvelope(40, 0, 0, 0, 3, 1.5f);
|
||||
|
|
@ -764,9 +737,9 @@ public sealed partial class RunStartTests
|
|||
var round = new[] { 135f, -135f, 180f, 100f, -100f };
|
||||
var cases = new (ManoeuvreEnvelope envelope, float[] windows, float[] bearings)[]
|
||||
{
|
||||
(gemini, new[] { .5f, .75f }, quarter), (longinus, new[] { .5f, .75f }, quarter), (lopsided, new[] { .5f, .75f }, quarter),
|
||||
(omni, new[] { .5f, .75f }, quarter), (longinus, new[] { .5f, .75f }, quarter), (lopsided, new[] { .5f, .75f }, quarter),
|
||||
(forwardOnly, new[] { .5f, .75f }, quarter), (slow, new[] { .5f, .75f }, quarter),
|
||||
(gemini, new[] { .5f, .75f }, round), (slow, new[] { .5f, .75f }, round), (cannotTurn, new[] { .5f, .75f }, round.Concat(quarter).ToArray()),
|
||||
(omni, new[] { .5f, .75f }, round), (slow, new[] { .5f, .75f }, round), (cannotTurn, new[] { .5f, .75f }, round.Concat(quarter).ToArray()),
|
||||
(lopsided, new[] { .5f }, new[] { 135f, -135f, 180f, -100f }),
|
||||
// Ships that can thrust one way only, over windows long enough for every face to finish its turn: the face that
|
||||
// must turn the long way round, past a right angle and past the wrap at 180 degrees, is the best one.
|
||||
|
|
@ -866,7 +839,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void AStepOfNoTimeHasNoAcceleration()
|
||||
{
|
||||
var (_, longinus, _, _) = EvFleet();
|
||||
var (_, longinus, _) = EvFleet();
|
||||
EvSettle(longinus);
|
||||
longinus.MovementDirection = float2(0, 1);
|
||||
for (var step = 0; step < 10; step++) longinus.Update(EvDt);
|
||||
|
|
@ -913,7 +886,7 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void EvasionIsReadFromTheHeadingNotTheVelocity()
|
||||
{
|
||||
var (_, longinus, _, _) = EvFleet();
|
||||
var (_, longinus, _) = EvFleet();
|
||||
var window = _items.GameplaySettings.SolutionWindow;
|
||||
EvSettle(longinus);
|
||||
longinus.Update(EvDt);
|
||||
|
|
@ -938,62 +911,20 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void TheShipsTrackObservesAtTheSolutionWindow()
|
||||
{
|
||||
var gemini = EvFleet().gemini;
|
||||
var djinni = EvFleet().djinni;
|
||||
var window = _items.GameplaySettings.SolutionWindow;
|
||||
EvSettle(gemini);
|
||||
var reference = gemini.Manoeuvre;
|
||||
EvSettle(djinni);
|
||||
var reference = djinni.Manoeuvre;
|
||||
for (var step = 0; step < 120; step++)
|
||||
{
|
||||
gemini.MovementDirection = float2(step / 30 % 2 == 0 ? 1 : -1, 0);
|
||||
gemini.Update(EvDt);
|
||||
reference.Observe(gemini.Acceleration, EvDt, window);
|
||||
djinni.MovementDirection = float2(step / 30 % 2 == 0 ? 1 : -1, 0);
|
||||
djinni.Update(EvDt);
|
||||
reference.Observe(djinni.Acceleration, EvDt, window);
|
||||
}
|
||||
Assert.True(reference.Moments.x > 1f, $"fixture: the jink is observed ({reference.Moments.x})");
|
||||
Assert.InRange(gemini.Manoeuvre.Moments.x, reference.Moments.x * .999f, reference.Moments.x * 1.001f);
|
||||
Assert.InRange(gemini.Manoeuvre.Moments.z, reference.Moments.z * .999f - .001f, reference.Moments.z * 1.001f + .001f);
|
||||
Assert.InRange(gemini.Manoeuvre.Trend.x, reference.Trend.x - .001f * Math.Abs(reference.Trend.x) - .001f, reference.Trend.x + .001f * Math.Abs(reference.Trend.x) + .001f);
|
||||
}
|
||||
|
||||
// Finding envelope-inputs-unpinned: a drive that is offline reports nothing, and a working one reports less along the
|
||||
// way the ship is already outrunning the rotor: half the rotor's speed forward leaves half the push forward and the
|
||||
// whole push back, and the same across the ship.
|
||||
[Fact]
|
||||
public void TheDrivesReportReadsItsStateAndTheShipsSpeed()
|
||||
{
|
||||
var gemini = EvFleet().gemini;
|
||||
EvSettle(gemini);
|
||||
gemini.Update(EvDt);
|
||||
var drive = gemini.GetBehaviors<AetherDrive>().Single();
|
||||
var rest = drive.Manoeuvre();
|
||||
Assert.True(rest.Forward > 1f && rest.Reverse > 1f && rest.Left > 1f && rest.Right > 1f && rest.Clockwise > .1f, "fixture: a settled drive pushes every way");
|
||||
var rotor = max(drive.Rpm * drive.DriveData.RotorDiameter / 100, 1);
|
||||
var forward = normalize(gemini.Direction);
|
||||
var right = EvRight(gemini);
|
||||
|
||||
gemini.Velocity = forward * (.5f * rotor.x);
|
||||
var ahead = drive.Manoeuvre();
|
||||
Assert.InRange(ahead.Forward / rest.Forward, .49f, .51f);
|
||||
Assert.InRange(ahead.Reverse / rest.Reverse, .99f, 1.01f);
|
||||
gemini.Velocity = -forward * (.5f * rotor.x);
|
||||
var astern = drive.Manoeuvre();
|
||||
Assert.InRange(astern.Reverse / rest.Reverse, .49f, .51f);
|
||||
Assert.InRange(astern.Forward / rest.Forward, .99f, 1.01f);
|
||||
gemini.Velocity = right * (.5f * rotor.y);
|
||||
var starboard = drive.Manoeuvre();
|
||||
Assert.InRange(starboard.Right / rest.Right, .49f, .51f);
|
||||
Assert.InRange(starboard.Left / rest.Left, .99f, 1.01f);
|
||||
gemini.Velocity = -right * (.5f * rotor.y);
|
||||
var port = drive.Manoeuvre();
|
||||
Assert.InRange(port.Left / rest.Left, .49f, .51f);
|
||||
Assert.InRange(port.Right / rest.Right, .99f, 1.01f);
|
||||
|
||||
gemini.Velocity = float2(0, 0);
|
||||
drive.Item.EquippableItem.Durability = 0f;
|
||||
gemini.Update(EvDt);
|
||||
gemini.Update(EvDt);
|
||||
Assert.False(drive.Item.Active.Value, "fixture: the drive is offline");
|
||||
var offline = drive.Manoeuvre();
|
||||
Assert.Equal(0f, offline.Forward + offline.Reverse + offline.Left + offline.Right + offline.Clockwise + offline.CounterClockwise);
|
||||
Assert.InRange(djinni.Manoeuvre.Moments.x, reference.Moments.x * .999f, reference.Moments.x * 1.001f);
|
||||
Assert.InRange(djinni.Manoeuvre.Moments.z, reference.Moments.z * .999f - .001f, reference.Moments.z * 1.001f + .001f);
|
||||
Assert.InRange(djinni.Manoeuvre.Trend.x, reference.Trend.x - .001f * Math.Abs(reference.Trend.x) - .001f, reference.Trend.x + .001f * Math.Abs(reference.Trend.x) + .001f);
|
||||
}
|
||||
|
||||
// The pure track forgets: after jinking, a track fed a constant acceleration (none at all, or a steady burn) reads
|
||||
|
|
@ -1034,10 +965,9 @@ public sealed partial class RunStartTests
|
|||
[Fact]
|
||||
public void EveryAggregateAndTheCompensationReadLiveThrust()
|
||||
{
|
||||
var (_, _, djinni, _) = EvFleet();
|
||||
var (_, _, djinni) = EvFleet();
|
||||
EvSettle(djinni);
|
||||
djinni.Update(EvDt);
|
||||
Assert.Empty(djinni.GetBehaviors<AetherDrive>());
|
||||
var thrusters = djinni.GetBehaviors<Thruster>().Where(t => t.Item.Active.Value).ToList();
|
||||
float Live(Thruster t) => t.Evaluate(((ThrusterData) t.Data).Thrust);
|
||||
float Mount(ItemRotation rotation, Func<Thruster, float> value) => thrusters.Where(t => t.Item.EquippableItem.Rotation == rotation).Sum(value);
|
||||
|
|
|
|||
Loading…
Reference in a new issue