From 79caa8b32d9037e2a1d674d30e73850d20548780 Mon Sep 17 00:00:00 2001 From: soul Date: Fri, 23 Oct 2026 04:32:46 +0200 Subject: [PATCH 1/2] Zhestokost boss: the Nakovalnya Boss Loadout, committed through loadout-commit --boss-of; RunGoal tests reach it Co-Authored-By: Claude Sonnet 5.5 --- GameData/Aetheria.cc | 4 +- tests/Aetheria.Shared.Tests/RunGoalTests.cs | 54 +++++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/GameData/Aetheria.cc b/GameData/Aetheria.cc index 3adacaa37..96983ad12 100644 --- a/GameData/Aetheria.cc +++ b/GameData/Aetheria.cc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06724d46e8fe2bcf3f03515a4923337cbe30e19dff11b962b81287a56cfe4e17 -size 192474 +oid sha256:8df83e71feb44a18e4419f3b75beac5915a85def2829a93550f72622b2c92a77 +size 195808 diff --git a/tests/Aetheria.Shared.Tests/RunGoalTests.cs b/tests/Aetheria.Shared.Tests/RunGoalTests.cs index 139fb45ad..67f1c4211 100644 --- a/tests/Aetheria.Shared.Tests/RunGoalTests.cs +++ b/tests/Aetheria.Shared.Tests/RunGoalTests.cs @@ -234,13 +234,58 @@ public sealed partial class RunStartTests } } - // No generated fallback (rulings boss-gear-is-a-loadout, demo-boss-hull-invented): the shipped catalog authors no boss Loadout, - // so Zhestokost has no boss. The refusal happens before anything is drawn. + // The demo as shipped (follow-up zhestokost-boss-loadout): Zhestokost's Faction.BossLoadout names the committed Nakovalnya + // Loadout, and a demo run reaches a built Nakovalnya carrying exactly that Loadout's cells, nothing drawn from the market. + // The gate stays shut while it lives and opens when it dies. + [Fact] + public void The_shipped_demo_boss_is_Zhestokosts_Nakovalnya_loadout() + { + var galaxy = DemoGalaxy(1); + var antagonist = Zhestokost(galaxy); + var loadout = Loadouts.BossOf(_cache, antagonist, out var refusal); + Assert.Equal(BossRefusal.None, refusal); + Assert.Equal("Nakovalnya Boss", loadout.Name); + Assert.Equal("Nakovalnya", _cache.Get(loadout.Hull).Name); + Assert.Equal(26, loadout.Slots.Count); + + var hull = _cache.Get(loadout.Hull); + Assert.DoesNotContain(hull, LoadoutGenerator.SoldHulls(_cache)); + + foreach (var seed in new uint[] { 7, 8, 9 }) + { + var log = new List(); + var items = LoggingItems(log); + var pack = BossZonePack(galaxy, items, galaxy.Exit, seed: seed); + Assert.True(pack.BossSpawned, string.Join("; ", log)); + var boss = Assert.Single(pack.Entities, entity => entity.Boss); + Assert.Same(antagonist, _cache.Get(boss.Faction)); + Assert.Equal("Nakovalnya", HullName(items, boss)); + // A design-only slot takes the brand its faction can get (ruling loadout-product-optional-context-chooses), so the + // cell is compared by position, rotation and design; a slot that pins a product must be built with that product. + static string Placed(string cell) => string.Join(",", cell.Split(',').Take(4)); + Assert.Equal(LoadoutCells(loadout).Select(Placed), Cells(items, boss).Select(Placed)); + foreach (var pinned in LoadoutCells(loadout).Where(cell => !cell.EndsWith(",-"))) + Assert.Contains(pinned, Cells(items, boss)); + // No other ship in the zone flies the boss hull: the market never draws it. + Assert.DoesNotContain(pack.Entities, entity => !entity.Boss && entity is ShipPack && HullName(items, entity) == "Nakovalnya"); + } + + var exit = ZoneOf(galaxy, galaxy.Exit); + var nakovalnya = Assert.Single(exit.Entities, entity => entity.IsBoss); + Assert.True(RunGoal.BossAlive(exit)); + Assert.False(RunGoal.ExitOpen(exit)); + nakovalnya.HeatstrokeDeath.OnNext(Unit.Default); + Assert.True(RunGoal.ExitOpen(exit)); + } + + // No generated fallback (rulings boss-gear-is-a-loadout, demo-boss-hull-invented): a faction that sets no boss Loadout has no + // boss. The refusal happens before anything is drawn. [Fact] public void Boss_without_a_boss_loadout_is_refused() { var galaxy = DemoGalaxy(1); var antagonist = Zhestokost(galaxy); + antagonist.BossLoadout = default; Assert.Null(Loadouts.BossOf(_cache, antagonist, out var refusal)); Assert.Equal(BossRefusal.Unset, refusal); @@ -333,9 +378,10 @@ public sealed partial class RunStartTests [Fact] public void Main_galaxy_bosses_are_factions_with_a_boss_loadout() { + var factions = _cache.GetAll().OrderBy(faction => faction.Name, StringComparer.Ordinal).ToArray(); + foreach (var faction in factions) faction.BossLoadout = default; Assert.Empty(RunStart.Generate(new MainGalaxy(), Inputs(() => 1)).BossZones); - var factions = _cache.GetAll().OrderBy(faction => faction.Name, StringComparer.Ordinal).ToArray(); factions[0].BossLoadout = new CultRecordRef(Loadouts.KeyOf("No Such Loadout")); Assert.Empty(RunStart.Generate(new MainGalaxy(), Inputs(() => 1)).BossZones); @@ -431,7 +477,7 @@ public sealed partial class RunStartTests public void Exit_open_only_in_the_exit_zone() { var galaxy = RunStart.Generate(new MainGalaxy(), Inputs(() => 1)); - // The shipped catalog sets no boss Loadout, so the main galaxy places no boss: give a faction one and a boss zone that is + // Give a faction a boss Loadout and a boss zone that is // not the exit, as the generator would for a faction with a boss Loadout. This Loadout pins its hull's brand, so any faction // builds it whatever it can get. var faction = galaxy.Factions.First(); From 084c77fb12c3d990234e3485ae943199d7f00745 Mon Sep 17 00:00:00 2001 From: soul Date: Fri, 23 Oct 2026 04:38:27 +0200 Subject: [PATCH 2/2] Boss loadout tests: shipped catalog now holds the demo boss; tests that wrote their own loadouts count only those Co-Authored-By: Claude Sonnet 5.5 --- .../LoadoutCommitCommandTests.cs | 12 ++++++++---- tests/Aetheria.Shared.Tests/RunGoalTests.cs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/LoadoutCommitCommandTests.cs b/tests/Aetheria.Shared.Tests/LoadoutCommitCommandTests.cs index 3ccf1720a..da0efcca4 100644 --- a/tests/Aetheria.Shared.Tests/LoadoutCommitCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/LoadoutCommitCommandTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System; using System.IO; using System.Linq; @@ -37,6 +38,9 @@ public sealed class LoadoutCommitCommandTests : IDisposable return cache; } + // The loadouts these tests wrote: the shipped catalog already holds the demo boss's, which no command here touches. + private static IEnumerable OwnLoadouts(CultCache cache) => cache.GetAll().Where(loadout => loadout.Name != "Nakovalnya Boss"); + private static string Hash(string path) => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); // A hull product and a weapon slot of the shipped catalog that fit together, found the production way: a generated fit, @@ -111,7 +115,7 @@ public sealed class LoadoutCommitCommandTests : IDisposable Assert.StartsWith("Test boss: committed, hull ", output); using var cache = Open(); - var loadout = Assert.Single(cache.GetAll()); + var loadout = Assert.Single(OwnLoadouts(cache)); Assert.Equal(Loadouts.KeyOf("Test boss"), cache.RefOf(loadout).Key); var product = cache.GetAll().Single(p => p.Name == fit.hullProduct); Assert.Equal(cache.RefOf(product).Key, loadout.HullProduct.Key); @@ -136,7 +140,7 @@ public sealed class LoadoutCommitCommandTests : IDisposable Assert.Equal(0, Run(out _, "Test boss", fit.hullProduct, fit.openSlot, "--replace")); using var cache = Open(); - var loadout = Assert.Single(cache.GetAll()); + var loadout = Assert.Single(OwnLoadouts(cache)); Assert.False(Assert.Single(loadout.Slots).Product.IsSet()); Assert.True(loadout.WeaponGroups == null || loadout.WeaponGroups.Length == 0); } @@ -196,7 +200,7 @@ public sealed class LoadoutCommitCommandTests : IDisposable Assert.DoesNotContain(canary, output); Assert.Equal(before, Hash(CatalogPath)); using var after = Open(); - Assert.Empty(after.GetAll()); + Assert.Empty(OwnLoadouts(after)); } // Ruling catalog-is-a-package-stack: a hull product only a package under Mods holds resolves, because names resolve @@ -213,7 +217,7 @@ public sealed class LoadoutCommitCommandTests : IDisposable Assert.StartsWith("Package boss: committed, hull Loadout Commit Skiff (Loadout Commit Skiff Mk I), 0 slots", output); using var after = Open(); - var loadout = Assert.Single(after.GetAll()); + var loadout = Assert.Single(OwnLoadouts(after)); Assert.Equal(ShipModCatalog.HullKey(PackageId), loadout.Hull.Key); Assert.StartsWith("mod-product:" + PackageId + ":", loadout.HullProduct.Key.Value); Assert.Null(after.Get(loadout.Hull)); diff --git a/tests/Aetheria.Shared.Tests/RunGoalTests.cs b/tests/Aetheria.Shared.Tests/RunGoalTests.cs index 67f1c4211..cfdbdb158 100644 --- a/tests/Aetheria.Shared.Tests/RunGoalTests.cs +++ b/tests/Aetheria.Shared.Tests/RunGoalTests.cs @@ -624,7 +624,7 @@ public sealed partial class RunStartTests var galaxy = DemoGalaxy(1); var hull = SoldShipHulls().OrderBy(h => h.Name, StringComparer.Ordinal).First(); StoreLoadout(new Loadout { Name = "Aaa Decoy", Hull = _cache.RefOf(hull) }); - if (dangling) Zhestokost(galaxy).BossLoadout = new CultRecordRef(Loadouts.KeyOf("CANARY-7f3a")); + Zhestokost(galaxy).BossLoadout = dangling ? new CultRecordRef(Loadouts.KeyOf("CANARY-7f3a")) : default; var log = new List(); var items = LoggingItems(log); var pack = BossZonePack(galaxy, items, galaxy.Exit);