Smoke Test Hull: 2x2 side mounts on a two-cell margin; test classes read sold hulls only

The 1x1 Ballistic side mount tripped IsFilledByAgreesWithACellOracle (Shape.FitsWithin never places a
design whose only cell is not its origin, so Takes and the oracle disagree on a 1x1 hardpoint); the
side mounts are 2x2 now, which needs two columns of margin on each side, so every Longinus cell and
hardpoint moves right by two. The two catalog-wide tests that listed every hull now list sold hulls.
This commit does not pass the suite: GameData/Aetheria.cc still holds the previous hull until the
next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 21:32:50 +02:00
commit 043a73eb0a
8 changed files with 68 additions and 57 deletions

View file

@ -24,22 +24,22 @@ public sealed class Arcs : Scenario
public override void Stage(ScenarioStage stage)
{
var fighter = stage.Fit("Smoke Test Hull",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("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),
("Point Defense Gun", int2(5, 4), Clockwise),
("Point Defense Laser", int2(0, 4), CounterClockwise));
("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),

View file

@ -18,19 +18,19 @@ public sealed class WeaponFeel : Scenario
public override void Stage(ScenarioStage stage)
{
var shooter = stage.Fit("Smoke Test Hull",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("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),
("ChargeBlast SG", int2(1, 17), None),
("Autocannon", int2(3, 17), None),
("FastBlast+-", int2(1, 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),
("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);

View file

@ -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)
{

View file

@ -439,13 +439,13 @@ 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 that no sold hull fits even loosely today (a test hull no product sells gives no gear a
// home), stated so that authoring a 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 = _cache.GetAll<HullData>().Where(hull => sold.Contains(_cache.RefOf(hull).Key)).ToArray();
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)

View file

@ -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() && item.Hardpoint.Rotation == ItemRotation.None).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)
{

View file

@ -37,7 +37,7 @@ public sealed class SmokeHullCommandTests : IDisposable
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", "Point Defense Gun", "Point Defense Laser" }
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);
}
@ -61,14 +61,17 @@ public sealed class SmokeHullCommandTests : IDisposable
var hull = Assert.Single(hulls);
Assert.Equal(0, products);
// The Longinus's own hardpoints survive, and four are added: each takes the gear the scenarios fit to it.
// 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, pdGun, pdLaser) = (fitted[0], fitted[1], fitted[2], fitted[3]);
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(pdGun));
Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.CounterClockwise && h.Takes(pdLaser));
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));
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");

View file

@ -49,9 +49,9 @@ public sealed partial class RunStartTests
Assert.Equal((Arcs.Port, Arcs.Starboard), (port.Position.xz, starboard.Position.xz));
var mounts = WeaponMounts(player);
var forward = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.None).ToList();
var starboardMounts = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.Clockwise).ToList();
var portMounts = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.CounterClockwise).ToList();
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);

View file

@ -1606,8 +1606,9 @@ public static class Program
// 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 1x1 side mount on each flank, a Clockwise Ballistic one on the starboard and a
// CounterClockwise Energy one on the port. The Longinus's own hardpoints are untouched. Its prefab is the
// (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)
@ -1626,26 +1627,32 @@ public static class Program
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 rows of cells past the bow, over the width of the tail's neighbours (x 1..4).
var grown = new Shape(longinus.Shape.Width, longinus.Shape.Height + 2);
// 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 < longinus.Shape.Height; y++) grown.Cells[x, y] = longinus.Shape.Cells[x, y];
for (var x = 1; x <= 4; x++)
for (var y = longinus.Shape.Height; y < grown.Height; y++) grown.Cells[x, y] = true;
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 width, int height)
static Shape Cells(int side)
{
var shape = new Shape(width, height);
for (var x = 0; x < width; x++)
for (var y = 0; y < height; y++) shape.Cells[x, y] = true;
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;
}
var bow = longinus.Shape.Height;
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(1, bow), Shape = Cells(2, 2), Rotation = ItemRotation.None, Armor = 20f });
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(3, bow), Shape = Cells(2, 2), Rotation = ItemRotation.None, Armor = 20f });
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(longinus.Shape.Width - 1, 4), Shape = Cells(1, 1), Rotation = ItemRotation.Clockwise, Armor = 10f });
hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(0, 4), Shape = Cells(1, 1), Rotation = ItemRotation.CounterClockwise, Armor = 10f });
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))