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