diff --git a/Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs b/Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs index 259fca8..40f38eb 100644 --- a/Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs +++ b/Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs @@ -391,6 +391,36 @@ public void ValidatePlacement_InfluenceUsesOwnLivingCompletedFootprints_AtDistan "an active site supplies spacing, never construction influence"); } + [Test] + public void ValidatePlacement_EveryCompletedBuildingExtendsTheZone_ItsSiteDoesNot() + { + // Corrected D-108: the anchor list is open. A completed Barracks — + // a role the old HQ/Storage/Power list excluded — pushes the zone + // outward by its own radius, so the probe at footprint distance 6 + // is placeable even though no HQ/Storage/Power anchor reaches it. + var completed = new Fixture(configure: e => e.TryAddField(1, new GridPos2D(60, 60), 9000)); + Assert.That(completed.Construction.PlaceCompletedBuilding(0, 3, 0, 0).IsValid, Is.True, + "far-away HQ prerequisite; its own radius never reaches the probe area"); + Assert.That(completed.Construction.PlaceCompletedBuilding(0, 7, 30, 30).IsValid, Is.True, + "completed Barracks outside every old-list anchor radius"); + Assert.That(completed.Construction.ValidatePlacement(0, 5, 38, 30), Is.EqualTo(CommandResultCode.Applied), + "footprint distance 6 to the completed Barracks is inside influence (corrected D-108)"); + + // The SAME building as an active site does not: create the site + // next to a bootstrap anchor, remove the anchor, and the probe at + // the identical distance must fall out of the zone again. + var siteOnly = new Fixture(configure: e => e.TryAddField(1, new GridPos2D(60, 60), 9000)); + Assert.That(siteOnly.Construction.PlaceCompletedBuilding(0, 3, 0, 0).IsValid, Is.True, "HQ prerequisite"); + Assert.That(siteOnly.Construction.PlaceCompletedBuilding(0, 5, 0, 4).IsValid, Is.True, "Power prerequisite"); + EntityId bootstrap = siteOnly.Construction.PlaceCompletedBuilding(0, 3, 24, 30); + Assert.That(bootstrap.IsValid, Is.True, "bootstrap anchor so the Barracks site validates at all"); + siteOnly.Step(1); // commit the balance (the Barracks power draw is evaluated) + Assert.That(siteOnly.Construction.TryPlaceBuilding(0, 7, 30, 30), Is.True, "create the active Barracks site"); + Assert.That(siteOnly.Entities.DespawnUnit(bootstrap), Is.True, "remove the bootstrap anchor"); + Assert.That(siteOnly.Construction.ValidatePlacement(0, 5, 38, 30), Is.EqualTo(CommandResultCode.RejectedInvalidTarget), + "the Barracks SITE supplies spacing, never construction influence"); + } + [Test] public void ValidatePlacement_RequiresOneEmptyRingAroundBuildingsAndSites() { diff --git a/Assets/_Project/Scripts/Simulation/Construction/ConstructionSystem.cs b/Assets/_Project/Scripts/Simulation/Construction/ConstructionSystem.cs index 5bc54dc..044eaf5 100644 --- a/Assets/_Project/Scripts/Simulation/Construction/ConstructionSystem.cs +++ b/Assets/_Project/Scripts/Simulation/Construction/ConstructionSystem.cs @@ -176,12 +176,12 @@ public sealed class ConstructionSystem : IStatefulSimSystem /// /// Maximum footprint-aware Chebyshev distance from an own construction - /// anchor (D-104). Construction anchors are exactly the own, living and - /// COMPLETED HQ, Storage and Power buildings — every other role does NOT - /// extend the build zone (see IsInsideBuildInfluence). The corrected - /// D-108 opens this anchor list to every own completed building; that - /// rule change is a separate PR (RulesHash64 moves) and is NOT yet - /// reflected here. + /// anchor (D-104). Under the corrected D-108 every own, living and + /// COMPLETED building is an anchor and pushes the build zone outward + /// by its own radius — construction sites never do (see + /// IsInsideBuildInfluence). Deliberately accepted (D-108): a chain of + /// cheap buildings can push the zone across the map; expansion is + /// meant to be coupled to the economy. /// public const int BuildInfluenceRadiusCells = 8; @@ -1103,16 +1103,27 @@ private bool FootprintIsWalkable(int originX, int originY) return true; } + /// + /// Corrected D-108 anchor rule: EVERY own, living and completed + /// building extends the build zone by its own + /// — no role list. A + /// construction site is NOT an anchor even though it already carries + /// its definition role (16.3, #44), so the finished-only rule is + /// checked explicitly through , the same + /// register the economy's power and capacity scans use. Deliberately + /// accepted (D-108): a chain of cheap buildings can push the build + /// zone across the map — that coupling of expansion to the economy + /// is the decided behavior, not a defect. + /// private bool IsInsideBuildInfluence(byte playerSlot, int originX, int originY) { for (int i = 0; i < MaxBuildings; i++) { ref readonly PlacementState placement = ref _buildings[i]; if (!placement.IsActive) continue; - if (!SimDefinitions.TryGetBuilding(placement.BuildingDefId, out SimBuildingDefinition def)) continue; - if (def.Role != UnitRole.HQ && def.Role != UnitRole.Storage && def.Role != UnitRole.Power) continue; EntityId id = UnitCommandStateView.ToEntityId(placement.RawEntityId); + if (IsActiveSite(id)) continue; // finished only: a site never extends the zone if (!_entityManager.TryGetUnit(id, out UnitState unit) || unit.PlayerId != playerSlot) continue; if (FootprintDistance(originX, originY, placement.OriginX, placement.OriginY) <= BuildInfluenceRadiusCells) { diff --git a/tools/Nova.SimRunner.Tests/ConstructionSystemTests.cs b/tools/Nova.SimRunner.Tests/ConstructionSystemTests.cs index bdba8b6..326f231 100644 --- a/tools/Nova.SimRunner.Tests/ConstructionSystemTests.cs +++ b/tools/Nova.SimRunner.Tests/ConstructionSystemTests.cs @@ -391,6 +391,36 @@ public void ValidatePlacement_InfluenceUsesOwnLivingCompletedFootprints_AtDistan "an active site supplies spacing, never construction influence"); } + [Test] + public void ValidatePlacement_EveryCompletedBuildingExtendsTheZone_ItsSiteDoesNot() + { + // Corrected D-108: the anchor list is open. A completed Barracks — + // a role the old HQ/Storage/Power list excluded — pushes the zone + // outward by its own radius, so the probe at footprint distance 6 + // is placeable even though no HQ/Storage/Power anchor reaches it. + var completed = new Fixture(configure: e => e.TryAddField(1, new GridPos2D(60, 60), 9000)); + Assert.That(completed.Construction.PlaceCompletedBuilding(0, 3, 0, 0).IsValid, Is.True, + "far-away HQ prerequisite; its own radius never reaches the probe area"); + Assert.That(completed.Construction.PlaceCompletedBuilding(0, 7, 30, 30).IsValid, Is.True, + "completed Barracks outside every old-list anchor radius"); + Assert.That(completed.Construction.ValidatePlacement(0, 5, 38, 30), Is.EqualTo(CommandResultCode.Applied), + "footprint distance 6 to the completed Barracks is inside influence (corrected D-108)"); + + // The SAME building as an active site does not: create the site + // next to a bootstrap anchor, remove the anchor, and the probe at + // the identical distance must fall out of the zone again. + var siteOnly = new Fixture(configure: e => e.TryAddField(1, new GridPos2D(60, 60), 9000)); + Assert.That(siteOnly.Construction.PlaceCompletedBuilding(0, 3, 0, 0).IsValid, Is.True, "HQ prerequisite"); + Assert.That(siteOnly.Construction.PlaceCompletedBuilding(0, 5, 0, 4).IsValid, Is.True, "Power prerequisite"); + EntityId bootstrap = siteOnly.Construction.PlaceCompletedBuilding(0, 3, 24, 30); + Assert.That(bootstrap.IsValid, Is.True, "bootstrap anchor so the Barracks site validates at all"); + siteOnly.Step(1); // commit the balance (the Barracks power draw is evaluated) + Assert.That(siteOnly.Construction.TryPlaceBuilding(0, 7, 30, 30), Is.True, "create the active Barracks site"); + Assert.That(siteOnly.Entities.DespawnUnit(bootstrap), Is.True, "remove the bootstrap anchor"); + Assert.That(siteOnly.Construction.ValidatePlacement(0, 5, 38, 30), Is.EqualTo(CommandResultCode.RejectedInvalidTarget), + "the Barracks SITE supplies spacing, never construction influence"); + } + [Test] public void ValidatePlacement_RequiresOneEmptyRingAroundBuildingsAndSites() {