diff --git a/Assets/Scripts/ServerShared/ScenarioStage.cs b/Assets/Scripts/ServerShared/ScenarioStage.cs index a813bbf49..b7a19b87f 100644 --- a/Assets/Scripts/ServerShared/ScenarioStage.cs +++ b/Assets/Scripts/ServerShared/ScenarioStage.cs @@ -168,7 +168,8 @@ public sealed class ScenarioStage } // Items in the entity's first cargo bay, one per name. A name is an equippable or consumable design (open: the stage - // chooses its product) or a product of one (pinned); a name that is neither is a failure. + // chooses its product) or a product of one (pinned), or a commodity (one unit: the crates an AmmoType weapon reloads + // from); a name that is none of these is a failure. public void Cargo(Entity entity, params string[] names) { if (entity == null) return; @@ -177,6 +178,13 @@ public sealed class ScenarioStage var bay = entity.CargoBays.FirstOrDefault(); foreach (var name in names) { + var commodity = _items.ItemData.GetAll().FirstOrDefault(data => data.Name == name); + if (commodity != null) + { + if (bay == null || !bay.TryStore(new SimpleCommodity { Data = new CultRecordRef(_items.ItemData.RefOf(commodity).Key), Quantity = 1 })) + _failures.Add($"{where}: no room for {name}"); + continue; + } var (data, product) = NamedOrProduct(name, where, found => found is EquippableItemData || found is ConsumableItemData); if (data == null) continue; var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data), diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index f0c3f5553..fd5076075 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -1,3 +1,4 @@ +using System.Linq; using static CultMath.math; using static ItemRotation; @@ -20,6 +21,8 @@ public sealed class Arcs : Scenario public static readonly CultMath.float2 Bow = float2(0, 400), Stern = float2(0, -400), Turret = float2(600, 300), Port = float2(-400, 0), Starboard = float2(400, 0); + public const int TurretAmmoCrates = 30; + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); public override void Stage(ScenarioStage stage) @@ -49,12 +52,14 @@ public sealed class Arcs : Scenario ("Compact Reactor", int2(3, 3), None), ("Turret Control Module", int2(3, 5), None), ("not if i see you first", int2(4, 5), None), // Compact Sensor - ("Panopticon Prime", int2(3, 6), None)); // Fire Control Array + ("Panopticon Prime", int2(3, 6), None), // Fire Control Array + ("Store-All Plus", int2(3, 1), None)); // Cargo Bay: the only 2x2 the turret hull has free stage.Player(fighter, float2(0, 0), facing: float2(0, 1)); stage.Place(stage.Bare("Longinus"), Bow); stage.Place(stage.Bare("Longinus"), Stern); - stage.Place(turret, Turret, stance: ScenarioStance.Hostile); + // The turret's guns reload from its hold, a crate of Ammo per 100-round magazine: both guns firing without pause spend about 3.5 crates a minute, so 30 outlast any session. + stage.Cargo(stage.Place(turret, Turret, stance: ScenarioStance.Hostile), Enumerable.Repeat("Ammo", TurretAmmoCrates).ToArray()); stage.Place(stage.Bare("Longinus"), Port); stage.Place(stage.Bare("Longinus"), Starboard); } diff --git a/Assets/Scripts/ServerShared/Scenarios/Pursuit.cs b/Assets/Scripts/ServerShared/Scenarios/Pursuit.cs index d6d630a88..20ad157a1 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Pursuit.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Pursuit.cs @@ -1,3 +1,4 @@ +using System.Linq; using static CultMath.math; using static ItemRotation; @@ -7,6 +8,7 @@ public sealed class Pursuit : Scenario // cloud breaking a sensor's lock, and well outside the 40 at which AStrongCloseSensorBurnsThrough shows a close // sensor burning through. private const float Astern = 300; + public const int MineCrates = 10; public override string Name => "Pursuit"; public override string Id => "pursuit"; @@ -55,7 +57,8 @@ public sealed class Pursuit : Scenario ("Store-All Plus", int2(2, 8), None)); // Cargo Bay var player = stage.Player(layer, float2(0, 0), facing: float2(0, 1)); - stage.Cargo(player, "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it"); + // The Mine Launcher reloads a magazine of six from a crate of Mines: ten crates are sixty-six mines, minutes of laying at its cooldown, more than a chase lasts. + stage.Cargo(player, new[] { "Morning Fog", "Morning Fog", "Morning Fog", "send it", "send it" }.Concat(Enumerable.Repeat("Mines", MineCrates)).ToArray()); stage.Place(pursuer, float2(0, -Astern), facing: float2(0, 1), stance: ScenarioStance.Hostile, piloted: true); } } diff --git a/tests/Aetheria.Shared.Tests/ScenarioAmmunitionTests.cs b/tests/Aetheria.Shared.Tests/ScenarioAmmunitionTests.cs new file mode 100644 index 000000000..740e793a2 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ScenarioAmmunitionTests.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using UniRx; +using Xunit; + +// Scenarios that author a weapon's ammunition-fed crates in a hold (ruling ballistics-real-ammo): the staged hold is what the weapon reloads from, so a +// scenario whose check rests on a weapon firing past its first magazine states the crates it needs. Each test launches through the production path. +public sealed partial class RunStartTests +{ + // The Arcs turret fits two Light Machine Guns of a hundred rounds and a bay of Ammo: its hold holds what the scenario states, and across a minute + // in range of the player it fires past both first magazines, each reload taking a crate. Kills: a turret fit without a bay or with no authored + // crates (silent after one magazine), and Cargo failing to admit a commodity. + [Fact] + public void TheArcsTurretKeepsFiringPastItsFirstMagazine() + { + var (_, arena, staged, failures) = Launch(new Arcs(), Inputs(() => GalaxySeed)); + Assert.Empty(failures); + var player = staged.Player; + var turret = Assert.IsType(staged.Entities[2]); + var guns = turret.Weapons.ToList(); + Assert.Equal(2, guns.Count); + var ammo = guns[0].WeaponData.AmmoType.Key; + Assert.Equal(Arcs.TurretAmmoCrates, turret.CountItemsInCargo(ammo)); + + // The guns are Auto weapons: they fire on a hostile inside their range (400 m) with no pilot. The player starts 775 m off, so it flies up to 200 m. + arena.Update(0); + player.Position = turret.Position + new CultMath.float3(-200, 0, 0); + var shots = 0; + using var sub = arena.ShotCommitted.Subscribe(outcome => { if (ReferenceEquals(outcome.Source, turret)) shots++; }); + for (var tick = 0; tick < System.MathF.Round(60f / SimClock.GameStep); tick++) arena.Update(SimClock.GameStep); + + var firstMagazines = guns.Sum(gun => gun.WeaponData.MagazineSize); + Assert.True(shots > firstMagazines, $"the turret fired {shots} rounds, no more than its two first magazines of {firstMagazines}"); + Assert.True(turret.CountItemsInCargo(ammo) < Arcs.TurretAmmoCrates, "the turret reloaded without taking a crate"); + } + + // The Pursuit player's Mine Launcher draws its magazine of six from crates of Mines it carries beside the consumables: it lays past six, and the + // crate count falls by one per reload. Kills: authored cargo with no Mines (at most six mines), a reload that takes no crate. + [Fact] + public void ThePursuitPlayerLaysMoreThanOneMagazineOfMinesFromItsCrates() + { + var (_, arena, staged, failures) = Launch(new Pursuit(), Inputs(() => GalaxySeed)); + Assert.Empty(failures); + var player = staged.Player; + var launcher = player.Weapons.Single(weapon => weapon.WeaponData is MineLayerData); + var mines = launcher.WeaponData.AmmoType.Key; + Assert.Equal(Pursuit.MineCrates, player.CountItemsInCargo(mines)); + Assert.Equal(3, HeldOf(player, "Coolant Vent")); + + arena.Update(0); + var laid = 0; + using var sub = arena.Mines.ObserveAdd().Subscribe(_ => laid++); + var reloaded = new List(); + var last = Pursuit.MineCrates; + for (var tick = 0; tick < System.MathF.Round(120f / SimClock.GameStep) && laid <= 18; tick++) + { + launcher.Activate(); + arena.Update(SimClock.GameStep); + var crates = player.CountItemsInCargo(mines); + if (crates != last) { reloaded.Add(last - crates); last = crates; } + } + + Assert.True(laid > launcher.WeaponData.MagazineSize, $"the player laid {laid} mines, no more than one magazine of {launcher.WeaponData.MagazineSize}"); + Assert.NotEmpty(reloaded); + Assert.All(reloaded, step => Assert.Equal(1, step)); + } +}