Merge cut-scenarios-smoke (verdict cut-scenarios-smoke.s2)
Scenarios.cs Tests list: kept WeaponFeel, Pursuit and Crossing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
b6262467f0
13 changed files with 356 additions and 38 deletions
|
|
@ -186,7 +186,7 @@ public class LoadoutGenerator
|
|||
// not once a required item has fallen back to any manufacturer). The one availability rule for every selection.
|
||||
private ((FactionProductData product, T design)[] available, bool preferManufacturers) AvailableProducts<T>(Predicate<T> filter, bool required) where T : CraftedItemData
|
||||
{
|
||||
var hulls = ItemManager.ItemData.GetAll<HullData>().ToArray();
|
||||
var hulls = SoldHulls(ItemManager.ItemData);
|
||||
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
|
||||
.Select(product => (product, design: ItemManager.ItemData.Get(product.Design) as T))
|
||||
.Where(entry =>
|
||||
|
|
@ -231,6 +231,11 @@ public class LoadoutGenerator
|
|||
// Hardpoint gear that no hull in the catalog can mount has no home (operator, 2026-09-30: "we'll need to author a
|
||||
// bunch more hulls before all the gear variety in the game has a home"): generation never offers it, so no station
|
||||
// stocks it. It gains one the moment a hull with a hardpoint that takes it exists. Tool gear goes in any interior.
|
||||
// Only a hull some product sells gives gear a home: a hull no faction builds (the Smoke Test Hull) is a test fixture, so its
|
||||
// hardpoints never put gear on a shelf. The one definition of the hulls HasHome considers.
|
||||
public static HullData[] SoldHulls(CultCache items) =>
|
||||
items.GetAll<FactionProductData>().Select(product => items.Get(product.Design)).OfType<HullData>().Distinct().ToArray();
|
||||
|
||||
public static bool HasHome(EquippableItemData design, HullData[] hulls) =>
|
||||
design.HardpointType == HardpointType.Tool || design.HardpointType == HardpointType.Hull ||
|
||||
hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design)));
|
||||
|
|
|
|||
|
|
@ -4,34 +4,42 @@ using static ItemRotation;
|
|||
public sealed class Arcs : Scenario
|
||||
{
|
||||
public override string Name => "Arcs";
|
||||
public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the hull keeps its " +
|
||||
public override string Brief => "The Smoke Test Hull with forward mounts and a side mount on each flank; a bare hull off the bow, one off " +
|
||||
"the stern, one abeam to port, one abeam to starboard, and a hostile turret. Verify: the hull keeps its " +
|
||||
"heading while you aim at the stern hull, and the forward guns hold. Designate the bow hull and fire with the " +
|
||||
"aim anywhere: the forward guns fire on it and hit. The turret tracks you all the way round.";
|
||||
"aim anywhere: the forward guns fire on it and hit. The side guns fire at their beam hull and never through " +
|
||||
"the hull. Designate the starboard hull, aim ahead and fire its group: it fires on the hull without the aim " +
|
||||
"and the port group holds; undesignate and aim to port: the port group fires free. The turret tracks you " +
|
||||
"all the way round.";
|
||||
public override uint Seed => 103;
|
||||
public override bool Ambient => false;
|
||||
|
||||
// Where the targets sit: the player is at the origin facing +z, and every mount fires forward.
|
||||
public static readonly CultMath.float2 Bow = float2(0, 400), Stern = float2(0, -400), Turret = float2(600, 0);
|
||||
// Where the targets sit: the player is at the origin facing +z. The forward mounts fire forward, the starboard mount
|
||||
// (Clockwise) along +x and the port mount (CounterClockwise) along -x.
|
||||
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 override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var fighter = stage.Fit("Longinus",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Large Drive", int2(1, 0), Reversed),
|
||||
var fighter = stage.Fit("Smoke Test Hull",
|
||||
("Cockpit 2x2", int2(4, 6), None),
|
||||
("Large Drive", int2(3, 0), Reversed),
|
||||
("Talaria", int2(2, 14), CounterClockwise),
|
||||
("Talaria", int2(3, 14), Clockwise),
|
||||
("Core Power", int2(2, 4), None),
|
||||
("GT 3K", int2(0, 5), None),
|
||||
("GT 3K", int2(5, 5), None),
|
||||
("FastBlast+-", int2(1, 8), None),
|
||||
("FastBlast+-", int2(4, 8), None),
|
||||
("Iapyx", int2(2, 2), CounterClockwise),
|
||||
("Iapyx", int2(3, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("Fire Control Array", int2(1, 6), Clockwise));
|
||||
("Large Drive", int2(5, 0), Reversed),
|
||||
("Talaria", int2(4, 14), CounterClockwise),
|
||||
("Talaria", int2(5, 14), Clockwise),
|
||||
("Core Power", int2(4, 4), None),
|
||||
("GT 3K", int2(2, 5), None),
|
||||
("GT 3K", int2(7, 5), None),
|
||||
("FastBlast+-", int2(3, 8), None),
|
||||
("FastBlast+-", int2(6, 8), None),
|
||||
("Iapyx", int2(4, 2), CounterClockwise),
|
||||
("Iapyx", int2(5, 2), Clockwise),
|
||||
("not if i see you first", int2(5, 10), None),
|
||||
("Fire Control Array", int2(3, 6), Clockwise),
|
||||
("Autocannon", int2(8, 4), Clockwise),
|
||||
("ChargeBlast SG", int2(0, 4), CounterClockwise));
|
||||
var turret = stage.Fit("Turret",
|
||||
("ClearPath", int2(1, 2), None),
|
||||
("ClearPath", int2(5, 2), None),
|
||||
|
|
@ -46,5 +54,7 @@ public sealed class Arcs : Scenario
|
|||
stage.Place(stage.Bare("Longinus"), Bow);
|
||||
stage.Place(stage.Bare("Longinus"), Stern);
|
||||
stage.Place(turret, Turret, stance: ScenarioStance.Hostile);
|
||||
stage.Place(stage.Bare("Longinus"), Port);
|
||||
stage.Place(stage.Bare("Longinus"), Starboard);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ public static class Scenarios
|
|||
|
||||
public static readonly Scenario[] Tests =
|
||||
{
|
||||
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul(), new Pursuit(), new Crossing()
|
||||
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new WeaponFeel(), new LongHaul(), new Pursuit(), new Crossing()
|
||||
};
|
||||
}
|
||||
|
|
|
|||
38
Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs
Normal file
38
Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class WeaponFeel : Scenario
|
||||
{
|
||||
public override string Name => "Weapon Feel";
|
||||
public override string Brief => "The Smoke Test Hull with a ChargeBlast SG, an Autocannon and a FastBlast+- burst weapon; a bare Longinus dead ahead. " +
|
||||
"Verify: ChargeBlast SG and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
|
||||
"cadence is as before; a fast release-and-press on the ChargeBlast SG starts no second charge.";
|
||||
public override uint Seed => 107;
|
||||
public override bool Ambient => false;
|
||||
|
||||
// The one target, ahead of the nose: inside every forward mount's arc.
|
||||
public static readonly CultMath.float2 Target = float2(0, 400);
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var shooter = stage.Fit("Smoke Test Hull",
|
||||
("Cockpit 2x2", int2(4, 6), None),
|
||||
("Large Drive", int2(3, 0), Reversed),
|
||||
("Large Drive", int2(5, 0), Reversed),
|
||||
("Talaria", int2(4, 14), CounterClockwise),
|
||||
("Talaria", int2(5, 14), Clockwise),
|
||||
("Core Power", int2(4, 4), None),
|
||||
("ChargeBlast SG", int2(3, 17), None),
|
||||
("Autocannon", int2(5, 17), None),
|
||||
("FastBlast+-", int2(3, 8), None),
|
||||
("Iapyx", int2(4, 2), CounterClockwise),
|
||||
("Iapyx", int2(5, 2), Clockwise),
|
||||
("not if i see you first", int2(5, 10), None),
|
||||
("Fire Control Array", int2(3, 6), Clockwise));
|
||||
|
||||
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));
|
||||
stage.Place(stage.Bare("Longinus"), Target);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 74c554ab5a6a4ac5ad0334a29135dc7f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
BIN
GameData/Aetheria.cc
(Stored with Git LFS)
Binary file not shown.
|
|
@ -29,7 +29,7 @@ public sealed partial class RunStartTests
|
|||
|
||||
var products = PiratesProducts();
|
||||
Assert.Equal(8, products.Length);
|
||||
var hulls = _cache.GetAll<HullData>().ToArray();
|
||||
var hulls = LoadoutGenerator.SoldHulls(_cache);
|
||||
foreach (var product in products)
|
||||
{
|
||||
var design = _cache.Get(product.Design) as EquippableItemData;
|
||||
|
|
|
|||
|
|
@ -248,13 +248,14 @@ public sealed class RestoredHullsTests
|
|||
}
|
||||
|
||||
// The dock intro dereferences the ship's VelocityLimit unguarded (ActionGameManager), so every ship hull the catalog
|
||||
// ships carries exactly one, with a top speed that is a finite positive number. After the retirement
|
||||
// sells carries exactly one, with a top speed that is a finite positive number. After the retirement
|
||||
// (ruling retire-longinusx) the shipped ship hulls are the Longinus and the Djinni.
|
||||
[Fact]
|
||||
public void EveryShippedShipHullHasATopSpeed()
|
||||
{
|
||||
using var cache = OpenCatalog();
|
||||
var ships = cache.GetAll<HullData>().Where(h => h.HullType == HullType.Ship).OrderBy(h => h.Name, StringComparer.Ordinal).ToArray();
|
||||
var sold = cache.GetAll<FactionProductData>().Select(product => product.Design.Key).ToHashSet();
|
||||
var ships = cache.GetAll<HullData>().Where(h => h.HullType == HullType.Ship && sold.Contains(cache.RefOf(h).Key)).OrderBy(h => h.Name, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(new[] { "Djinni", "Longinus" }, ships.Select(h => h.Name));
|
||||
foreach (var hull in ships)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
[Fact]
|
||||
public void GenerationOffersExactlyTheSoldGearSomeHullCanFit()
|
||||
{
|
||||
var hulls = _cache.GetAll<HullData>().ToArray();
|
||||
var hulls = LoadoutGenerator.SoldHulls(_cache);
|
||||
var sold = _cache.GetAll<FactionProductData>().Where(p => p.Manufacturer.IsSet()).Select(p => p.Design.Key).ToHashSet();
|
||||
var generator = PreludeGenerator();
|
||||
var wrong = _cache.GetAll<EquippableItemData>()
|
||||
|
|
@ -366,7 +366,7 @@ public sealed partial class RunStartTests : IDisposable
|
|||
"the Autocannon fits the turret's 8-cell Ballistic hardpoint, so it is offered");
|
||||
}
|
||||
|
||||
// Gear no hull can fit is never offered; authoring a hull whose hardpoint takes it makes it offered.
|
||||
// Gear no sold hull can fit is never offered; a product selling a hull whose hardpoint takes it makes it offered.
|
||||
[Fact]
|
||||
public void GearNoHullCanFitIsOfferedOnlyOnceAHullTakesIt()
|
||||
{
|
||||
|
|
@ -378,10 +378,15 @@ public sealed partial class RunStartTests : IDisposable
|
|||
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
||||
var hardpointShape = new Shape(8, 8);
|
||||
foreach (var cell in hardpointShape.AllCoordinates) hardpointShape[cell] = true;
|
||||
_cache.Upsert(new HullData
|
||||
var bigMount = new HullData
|
||||
{
|
||||
Name = "Big Mount", HullType = HullType.Ship, Shape = shape, Durability = 100, Mass = 1000, Price = 1,
|
||||
Hardpoints = { new HardpointData { Type = HardpointType.Ballistic, Position = int2(0, 0), Shape = hardpointShape } }
|
||||
};
|
||||
_cache.Upsert(bigMount);
|
||||
_cache.Upsert(new FactionProductData
|
||||
{
|
||||
Name = "Big Mount", Design = new CultRecordRef<CraftedItemData>(_cache.RefOf(bigMount).Key), Manufacturer = _cache.RefOf(_protagonist)
|
||||
});
|
||||
Assert.True(Offered(generator, huge), "a hull whose hardpoint takes it gives it a home");
|
||||
}
|
||||
|
|
@ -439,17 +444,17 @@ public sealed partial class RunStartTests : IDisposable
|
|||
Assert.True(disagreements.Count == 0, string.Join("; ", disagreements.Take(10)) + $" ({disagreements.Count} in all)");
|
||||
}
|
||||
|
||||
// The sold hardpoint gear that no hull fits even loosely today, stated so that authoring a hull that takes one
|
||||
// shows up here as a changed list.
|
||||
// The sold hardpoint gear the generator gives no home today: the production rule (LoadoutGenerator.HasHome over the hulls a
|
||||
// product sells). The Smoke Test Hull, which no product sells, gives ChargeBlast SG and ChargeBlast+- no home; authoring a
|
||||
// sold hull that takes one shows up here as a changed list.
|
||||
[Fact]
|
||||
public void TheSoldHardpointGearWithNoHomeToday()
|
||||
{
|
||||
var hulls = _cache.GetAll<HullData>().ToArray();
|
||||
var sold = _cache.GetAll<FactionProductData>().Select(product => product.Design.Key).ToHashSet();
|
||||
var hulls = LoadoutGenerator.SoldHulls(_cache);
|
||||
var homeless = _cache.GetAll<EquippableItemData>()
|
||||
.Where(design => sold.Contains(_cache.RefOf(design).Key) && design.Price > 0)
|
||||
.Where(design => design.HardpointType != HardpointType.Tool && design.HardpointType != HardpointType.Hull)
|
||||
.Where(design => !hulls.Any(hull => hull.Hardpoints.Any(hardpoint => hardpoint.Takes(design))))
|
||||
.Where(design => !LoadoutGenerator.HasHome(design, hulls))
|
||||
.Select(design => design.Name).OrderBy(name => name, StringComparer.Ordinal).ToArray();
|
||||
Assert.Equal(new[] { "CShot RainbowLite Lazer", "ChargeBlast SG", "ChargeBlast+-", "OK Disperser" }, homeless);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -477,8 +477,8 @@ public sealed partial class RunStartTests
|
|||
}
|
||||
}
|
||||
|
||||
// Arcs' geometry: the bow hull sits inside every one of the player's mount arcs, the stern hull outside all of them,
|
||||
// and the turret is hostile.
|
||||
// Arcs' geometry: the bow hull sits inside every one of the player's forward mount arcs, the stern hull outside all of
|
||||
// them (the side mounts are ArcsFlanks'), the turret is hostile and the bare hulls are neutral.
|
||||
[Fact]
|
||||
public void ArcsGeometry()
|
||||
{
|
||||
|
|
@ -489,7 +489,7 @@ public sealed partial class RunStartTests
|
|||
var turret = Assert.IsType<OrbitalEntity>(staged.Entities[2]);
|
||||
Assert.Equal((Arcs.Bow, Arcs.Stern, Arcs.Turret), (bow.Position.xz, stern.Position.xz, turret.Position.xz));
|
||||
|
||||
var mounts = player.Equipment.Where(item => item.Behaviors.OfType<Weapon>().Any()).ToList();
|
||||
var mounts = player.Equipment.Where(item => item.Behaviors.OfType<Weapon>().Any() && item.EquippableItem.Rotation == ItemRotation.None).ToList();
|
||||
Assert.NotEmpty(mounts);
|
||||
foreach (var mount in mounts)
|
||||
{
|
||||
|
|
@ -497,7 +497,7 @@ public sealed partial class RunStartTests
|
|||
Assert.False(FireControl.InArc(mount, stern.Position - player.Position), $"{mount.Data.Name} bears on the stern hull");
|
||||
}
|
||||
Assert.True(player.IsHostileTo(turret) && turret.IsHostileTo(player), "the turret is hostile");
|
||||
Assert.False(player.IsHostileTo(bow) || player.IsHostileTo(stern), "the bare hulls are neutral");
|
||||
Assert.False(player.IsHostileTo(bow) || player.IsHostileTo(stern) || player.IsHostileTo(staged.Entities[3]) || player.IsHostileTo(staged.Entities[4]), "the bare hulls are neutral");
|
||||
}
|
||||
|
||||
// Q3 (operator, 2026-09-30): a design no product sells is a scenario test design and nothing else. A test design
|
||||
|
|
|
|||
93
tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs
Normal file
93
tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using Xunit;
|
||||
|
||||
// AetherDb's smoke-hull command (aetheria-release, cut scenarios-smoke), run on a scratch copy of the shipped catalog
|
||||
// with the Smoke Test Hull removed from it.
|
||||
public sealed class SmokeHullCommandTests : IDisposable
|
||||
{
|
||||
private const string HullName = "Smoke Test Hull";
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "smoke-hull-command-" + Guid.NewGuid().ToString("N"));
|
||||
private string CatalogPath => Path.Combine(_root, "GameData", "Aetheria.cc");
|
||||
|
||||
public SmokeHullCommandTests()
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(_root, "GameData"));
|
||||
Directory.CreateDirectory(Path.Combine(_root, "Assets", "Resources"));
|
||||
var shipped = AetherDb.FindRoot();
|
||||
File.Copy(Path.Combine(shipped, "GameData", "Aetheria.cc"), CatalogPath);
|
||||
File.Copy(Path.Combine(shipped, "Assets", "Resources", "Settings.asset"), Path.Combine(_root, "Assets", "Resources", "Settings.asset"));
|
||||
using var cache = new CultCache(TestCatalog.Registry());
|
||||
cache.AddBackingStore(new SingleFileMessagePackBackingStore(CatalogPath), AetheriaStores.CatalogTypes);
|
||||
foreach (var hull in cache.GetAll<HullData>().Where(h => h.Name == HullName).ToArray()) Assert.True(cache.Remove(cache.RefOf(hull).Key));
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
|
||||
public void Dispose() => Directory.Delete(_root, true);
|
||||
|
||||
private (HullData[] Hulls, HullData Longinus, EquippableItemData[] Fitted, int Products) Read()
|
||||
{
|
||||
var db = AetherDb.Open(root: _root);
|
||||
try
|
||||
{
|
||||
var hulls = db.Cache.GetAll<HullData>().Where(h => h.Name == HullName).ToArray();
|
||||
var keys = hulls.Select(h => db.Cache.RefOf(h).Key).ToArray();
|
||||
var products = db.Cache.GetAll<FactionProductData>().Count(p => keys.Any(k => p.Design.Key.Equals(k)));
|
||||
var fitted = new[] { "ChargeBlast SG", "Autocannon" }
|
||||
.Select(name => db.Cache.GetAll<EquippableItemData>().Single(d => d.Name == name)).ToArray();
|
||||
return (hulls, db.Cache.GetAll<HullData>().Single(h => h.Name == "Longinus"), fitted, products);
|
||||
}
|
||||
finally { db.Cache.Dispose(); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_dry_run_writes_nothing_and_apply_lands_one_productless_hull_once()
|
||||
{
|
||||
var before = File.ReadAllBytes(CatalogPath);
|
||||
Assert.Empty(Read().Hulls);
|
||||
|
||||
Assert.Equal(0, Program.SmokeHullCatalog(apply: false, root: _root));
|
||||
Assert.Equal(before, File.ReadAllBytes(CatalogPath));
|
||||
Assert.Empty(Read().Hulls);
|
||||
|
||||
Assert.Equal(0, Program.SmokeHullCatalog(apply: true, root: _root));
|
||||
var landed = File.ReadAllBytes(CatalogPath);
|
||||
Assert.NotEqual(before, landed);
|
||||
var (hulls, longinus, fitted, products) = Read();
|
||||
var hull = Assert.Single(hulls);
|
||||
Assert.Equal(0, products);
|
||||
|
||||
// The Longinus's own hardpoints survive (moved over by the margin the flank blocks need), and four are added: each takes
|
||||
// the gear the scenarios fit to it.
|
||||
Assert.Equal(longinus.Hardpoints.Count + 4, hull.Hardpoints.Count);
|
||||
var added = hull.Hardpoints.Skip(longinus.Hardpoints.Count).ToArray();
|
||||
var (chargeBlast, autocannon) = (fitted[0], fitted[1]);
|
||||
Assert.Equal(longinus.Hardpoints.Select(h => (h.Type, h.Position.x + 2, h.Position.y, h.Rotation)),
|
||||
hull.Hardpoints.Take(longinus.Hardpoints.Count).Select(h => (h.Type, h.Position.x, h.Position.y, h.Rotation)));
|
||||
Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.None && h.Takes(chargeBlast));
|
||||
Assert.Single(added, h => h.Type == HardpointType.Ballistic && h.Rotation == ItemRotation.None && h.Takes(autocannon));
|
||||
Assert.Single(added, h => h.Type == HardpointType.Ballistic && h.Rotation == ItemRotation.Clockwise && h.Takes(autocannon));
|
||||
Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.CounterClockwise && h.Takes(chargeBlast));
|
||||
// Each is a full 2x2 block with the default firing arc (0 uses the game setting; the side mounts bear by it, never wider)
|
||||
// and the Longinus-class armour the command writes.
|
||||
Assert.All(added, hardpoint =>
|
||||
{
|
||||
Assert.Equal((2, 2, 0f, 20f), (hardpoint.Shape.Width, hardpoint.Shape.Height, hardpoint.FiringArc, hardpoint.Armor));
|
||||
Assert.Equal(4, hardpoint.Shape.Coordinates.Length);
|
||||
});
|
||||
foreach (var hardpoint in added)
|
||||
foreach (var cell in hardpoint.Shape.Coordinates)
|
||||
Assert.True(hull.Shape[hardpoint.Position + cell], "a new hardpoint lies outside the hull's cells");
|
||||
|
||||
// A second apply finds the hull authored and writes nothing; so does a dry run.
|
||||
Assert.Equal(0, Program.SmokeHullCatalog(apply: true, root: _root));
|
||||
Assert.Equal(landed, File.ReadAllBytes(CatalogPath));
|
||||
Assert.Equal(0, Program.SmokeHullCatalog(apply: false, root: _root));
|
||||
Assert.Equal(landed, File.ReadAllBytes(CatalogPath));
|
||||
Assert.Single(Read().Hulls);
|
||||
}
|
||||
}
|
||||
87
tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs
Normal file
87
tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
// aetheria-release, cut scenarios-smoke: what the Smoke Test Hull scenarios stage, read from the staged run and the
|
||||
// shipped catalog's hull.
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
private static bool Bears(EquippedItem mount, Entity player, Entity target) => FireControl.InArc(mount, target.Position - player.Position);
|
||||
|
||||
private static List<EquippedItem> WeaponMounts(Entity ship) =>
|
||||
ship.Equipment.Where(item => item.Behaviors.OfType<Weapon>().Any()).ToList();
|
||||
|
||||
// Weapon Feel: the player flies the Smoke Test Hull with ChargeBlast SG on the 2x2 Energy hardpoint, Autocannon
|
||||
// on the 2x2 Ballistic hardpoint and a burst weapon; the bare hull ahead lies in every mount's arc.
|
||||
[Fact]
|
||||
public void WeaponFeelConditions()
|
||||
{
|
||||
var staged = Stage(Arena(false), new WeaponFeel());
|
||||
var player = staged.Player;
|
||||
player.GenerateWeaponGroups();
|
||||
Assert.Equal("Smoke Test Hull", _items.GetData(player.EquippedHull.EquippableItem).Name);
|
||||
|
||||
EquippedItem Mount(string design) => Assert.Single(WeaponMounts(player), item => _items.GetData(item.EquippableItem).Name == design);
|
||||
var charge = Mount("ChargeBlast SG");
|
||||
var autocannon = Mount("Autocannon");
|
||||
Assert.Equal((HardpointType.Energy, 2, 2), (charge.Hardpoint.Type, charge.Hardpoint.Shape.Width, charge.Hardpoint.Shape.Height));
|
||||
Assert.Equal((HardpointType.Ballistic, 2, 2), (autocannon.Hardpoint.Type, autocannon.Hardpoint.Shape.Width, autocannon.Hardpoint.Shape.Height));
|
||||
|
||||
var mounts = WeaponMounts(player);
|
||||
Assert.Contains(mounts, item => ((WeaponItemData)_items.GetData(item.EquippableItem)).WeaponModifiers.HasFlag(WeaponModifiers.RapidFire));
|
||||
foreach (var mount in mounts)
|
||||
{
|
||||
Assert.Contains(player.WeaponGroups, group => group.items.Contains(mount));
|
||||
Assert.True(Bears(mount, player, Assert.Single(staged.Entities)), $"{_items.GetData(mount.EquippableItem).Name} cannot bear on the hull ahead");
|
||||
}
|
||||
}
|
||||
|
||||
// Arcs' flanks: each side mount bears on its own flank's hull and on nothing else, the forward mounts bear on the
|
||||
// bow and on neither flank, and the two side mounts sit in different weapon groups.
|
||||
[Fact]
|
||||
public void ArcsFlanks()
|
||||
{
|
||||
var staged = Stage(Arena(false), new Arcs());
|
||||
var player = staged.Player;
|
||||
player.GenerateWeaponGroups();
|
||||
var (bow, stern, port, starboard) = (staged.Entities[0], staged.Entities[1], staged.Entities[3], staged.Entities[4]);
|
||||
// The flank hulls lie abeam from the player's facing, read from the staged positions: within 5 degrees of the
|
||||
// perpendicular, starboard on the right of the facing and port on the left.
|
||||
var facing = player.Direction;
|
||||
var right = CultMath.math.float2(facing.y, -facing.x);
|
||||
float Abeam(Entity flank, CultMath.float2 side) => CultMath.math.dot(CultMath.math.normalize(flank.Position.xz - player.Position.xz), side);
|
||||
var abeam = CultMath.math.cos(CultMath.math.radians(5f));
|
||||
Assert.True(Abeam(starboard, right) >= abeam, "the starboard hull is not abeam to starboard");
|
||||
Assert.True(Abeam(port, -right) >= abeam, "the port hull is not abeam to port");
|
||||
|
||||
var mounts = WeaponMounts(player);
|
||||
var forward = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.None).ToList();
|
||||
var starboardMounts = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.Clockwise).ToList();
|
||||
var portMounts = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.CounterClockwise).ToList();
|
||||
Assert.NotEmpty(forward);
|
||||
Assert.NotEmpty(starboardMounts);
|
||||
Assert.NotEmpty(portMounts);
|
||||
foreach (var mount in starboardMounts)
|
||||
{
|
||||
Assert.True(Bears(mount, player, starboard), "a starboard mount cannot bear on the starboard hull");
|
||||
Assert.False(Bears(mount, player, port) || Bears(mount, player, bow) || Bears(mount, player, stern), "a starboard mount bears off its flank");
|
||||
}
|
||||
foreach (var mount in portMounts)
|
||||
{
|
||||
Assert.True(Bears(mount, player, port), "a port mount cannot bear on the port hull");
|
||||
Assert.False(Bears(mount, player, starboard) || Bears(mount, player, bow) || Bears(mount, player, stern), "a port mount bears off its flank");
|
||||
}
|
||||
foreach (var mount in forward)
|
||||
{
|
||||
Assert.True(Bears(mount, player, bow), "a forward mount cannot bear on the bow hull");
|
||||
Assert.False(Bears(mount, player, port) || Bears(mount, player, starboard), "a forward mount bears on a flank");
|
||||
}
|
||||
|
||||
int GroupOf(EquippedItem mount) => Array.FindIndex(player.WeaponGroups, group => group.items.Contains(mount));
|
||||
var starboardGroups = starboardMounts.Select(GroupOf).ToList();
|
||||
var portGroups = portMounts.Select(GroupOf).ToList();
|
||||
Assert.DoesNotContain(-1, starboardGroups.Concat(portGroups));
|
||||
Assert.Empty(starboardGroups.Intersect(portGroups));
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ public static class Program
|
|||
case "field-kinds": return FieldKindsCatalog(args.Contains("apply"));
|
||||
case "pd-gear": return PdGearCatalog(args.Contains("apply"));
|
||||
case "mine-launcher": return MineLauncherCatalog(args.Contains("apply"));
|
||||
case "smoke-hull": return SmokeHullCatalog(args.Contains("apply"));
|
||||
case "roles-backfill": return RolesBackfill(args.Contains("apply"));
|
||||
case "consumables-first-set": return ConsumablesFirstSetCatalog(args.Contains("apply"));
|
||||
case "pirates-faction":
|
||||
|
|
@ -49,7 +50,7 @@ public static class Program
|
|||
return PiratesFaction(args.Contains("apply"));
|
||||
case "verse-grammar": return VerseGrammarCommand.Run(args.Contains("apply"));
|
||||
default:
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], pirates-faction [apply], verse-grammar [apply]");
|
||||
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], pirates-faction [apply], verse-grammar [apply]");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1601,6 +1602,73 @@ public static class Program
|
|||
return 0;
|
||||
}
|
||||
|
||||
// aetheria-release cut scenarios-smoke (rulings weapon-feel-to-smoke-cut-test-hull, test-hull-both-flanks): the
|
||||
// Smoke Test Hull, a copy of the Longinus hull that no product sells, so no generated ship, station or shop offers
|
||||
// it and only a scenario fits it. It adds what no shipped ship hull has, where the Longinus runs out of cells: two
|
||||
// rows of cells past the bow carrying a 2x2 Energy hardpoint (ChargeBlast SG) and a 2x2 Ballistic hardpoint
|
||||
// (Autocannon), and a 2x2 side mount on each flank, a Clockwise Ballistic one on the starboard and a
|
||||
// CounterClockwise Energy one on the port, which need two columns of margin on each side: every Longinus cell and
|
||||
// hardpoint moves right by two. Their gear is unchanged. Its prefab is the
|
||||
// Longinus's, which has no barrel for the new hardpoints: they fire from the hull centre. Dry run unless passed
|
||||
// "apply".
|
||||
public static int SmokeHullCatalog(bool apply, string root = null)
|
||||
{
|
||||
const string name = "Smoke Test Hull";
|
||||
var db = AetherDb.Open(catalogWritable: apply, root: root);
|
||||
if (db.Cache.GetAll<HullData>().Any(h => h.Name == name))
|
||||
{
|
||||
Console.WriteLine($"\"{name}\" is already authored; nothing to do.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var longinus = db.Cache.GetAll<HullData>().Single(h => h.Name == "Longinus");
|
||||
var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(HullData).Assembly);
|
||||
var hull = MessagePackSerializer.Deserialize<HullData>(MessagePackSerializer.Serialize(longinus, options), options);
|
||||
hull.Name = name;
|
||||
hull.Description = "The Longinus with two forward 2x2 hardpoints and a side mount on each flank, for scenarios. No faction builds it.";
|
||||
|
||||
// Two cells of margin on each side (every Longinus cell and hardpoint moves right by Margin), two rows of cells past
|
||||
// the bow over the width of the tail's neighbours, and a 2x2 block of cells on each flank at the wide section.
|
||||
const int Margin = 2;
|
||||
var width = longinus.Shape.Width + 2 * Margin;
|
||||
var bow = longinus.Shape.Height;
|
||||
var grown = new Shape(width, bow + 2);
|
||||
for (var x = 0; x < longinus.Shape.Width; x++)
|
||||
for (var y = 0; y < bow; y++) grown.Cells[x + Margin, y] = longinus.Shape.Cells[x, y];
|
||||
for (var x = Margin + 1; x <= Margin + 4; x++)
|
||||
for (var y = bow; y < grown.Height; y++) grown.Cells[x, y] = true;
|
||||
foreach (var x in new[] { 0, 1, width - 2, width - 1 })
|
||||
for (var y = 4; y <= 5; y++) grown.Cells[x, y] = true;
|
||||
hull.Shape = grown;
|
||||
foreach (var existing in hull.Hardpoints) existing.Position = CultMath.math.int2(existing.Position.x + Margin, existing.Position.y);
|
||||
|
||||
static Shape Cells(int side)
|
||||
{
|
||||
var shape = new Shape(side, side);
|
||||
for (var x = 0; x < side; x++)
|
||||
for (var y = 0; y < side; y++) shape.Cells[x, y] = true;
|
||||
return shape;
|
||||
}
|
||||
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(Margin + 1, bow), Shape = Cells(2), Rotation = ItemRotation.None, Armor = 20f });
|
||||
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(Margin + 3, bow), Shape = Cells(2), Rotation = ItemRotation.None, Armor = 20f });
|
||||
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(width - 2, 4), Shape = Cells(2), Rotation = ItemRotation.Clockwise, Armor = 20f });
|
||||
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(0, 4), Shape = Cells(2), Rotation = ItemRotation.CounterClockwise, Armor = 20f });
|
||||
|
||||
Console.WriteLine($"Adds \"{name}\": a Longinus copy grown from {longinus.Shape.Width}x{longinus.Shape.Height} to {grown.Width}x{grown.Height}, no product.");
|
||||
foreach (var hardpoint in hull.Hardpoints.Skip(longinus.Hardpoints.Count))
|
||||
Console.WriteLine($" {hardpoint.Type,-9} {hardpoint.Shape.Width}x{hardpoint.Shape.Height} at {hardpoint.Position.x},{hardpoint.Position.y} rotation {hardpoint.Rotation}");
|
||||
if (!apply)
|
||||
{
|
||||
Console.WriteLine("\nDry run. Pass \"apply\" to author it.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
CultRecordRefs.Validate(hull);
|
||||
db.Cache.Commit(batch => batch.Upsert(typeof(HullData), hull));
|
||||
Console.WriteLine($"\nAuthored \"{name}\" in Aetheria.cc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// sim-mines-presenter: the Mine Launcher item, so a ship can be fitted with the sim's mine layer. Its numbers
|
||||
// are the 2021 record's (GameData/Legacy/AetherDB.2021-03-05.msgpack, recovered in 8c71a637: mass 75, price
|
||||
// 125000, damage 50, range 2000, energy 15, heat 500, visibility 10, magazine 6, reload 5, spread 5, velocity
|
||||
|
|
|
|||
Loading…
Reference in a new issue