diff --git a/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs b/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs
index a1c9c23..65a4e56 100644
--- a/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs
+++ b/Assets/Tests/EditMode/Gameplay/CanonicalMatchSetupTests.cs
@@ -97,7 +97,21 @@ public sealed class CanonicalMatchSetupTests
private const ushort MapWidth = 128;
private const ushort MapHeight = 128;
private const int EntityCapacity = 1024;
- private const long FieldReserveAE = 2000000L;
+ private struct FieldLayout
+ {
+ public ushort Id;
+ public int X, Y;
+ public long ReserveAE;
+ }
+
+ private static readonly FieldLayout[] FieldLayouts =
+ {
+ new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L },
+ new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L },
+ new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L },
+ new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L },
+ new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L },
+ };
// Faction-resolved opening placement ids (SimDefinitions id rule):
// slot 0 Alliance (role value), slot 1 Legion (role value + 17).
private const ushort DefHQAlliance = 3;
@@ -170,46 +184,47 @@ private static ReferenceHost BuildReferenceHost(ulong seed)
/// Fixed opening layout of one slot, in grid cells.
private sealed class SlotLayout
{
- public ushort FieldId;
- public int FieldX, FieldY;
public int HqOriginX, HqOriginY;
public int BuilderX, BuilderY;
}
private static readonly SlotLayout Slot0Layout = new SlotLayout
{
- FieldId = 1, FieldX = 7, FieldY = 7,
HqOriginX = 4, HqOriginY = 4,
BuilderX = 13, BuilderY = 7,
};
private static readonly SlotLayout Slot1Layout = new SlotLayout
{
- FieldId = 2, FieldX = 119, FieldY = 119,
HqOriginX = 120, HqOriginY = 120,
- BuilderX = 113, BuilderY = 119,
+ BuilderX = 111, BuilderY = 117,
};
///
/// Byte-exact mirror of Determinism10000Scenario.SetupMatch (D-077):
- /// per slot one Aetherium field, a completed HQ and ONE Builder —
- /// nothing else. Spawn ORDER is load-bearing: EntityManager hands
- /// out ids from a deterministic free list, so any reordering shifts
- /// every id and therefore every hash. Units spawn through SpawnUnit's
- /// defaults (maxHealth 100 for all), exactly like the scenario —
- /// NOT through SimDefinitions.
+ /// five finite Aetherium fields in canonical id order, then per slot a
+ /// completed HQ and ONE Builder — nothing else. Entity spawn order is
+ /// load-bearing: EntityManager hands out ids from a deterministic free
+ /// list, so any reordering shifts every id and therefore every hash.
+ /// Units spawn through SpawnUnit's defaults (maxHealth 100 for all),
+ /// exactly like the scenario — NOT through SimDefinitions.
///
private static void ApplyOpeningPosition(ReferenceHost host)
{
// The slot factions are already bound: BuildReferenceHost mirrors
// BuildHost, which assigns them before Kernel.Start() (the
// SetSlotFaction guard requires it).
+ for (int i = 0; i < FieldLayouts.Length; i++)
+ {
+ FieldLayout field = FieldLayouts[i];
+ Assert.That(host.Economy.TryAddField(
+ field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE),
+ Is.True, "reference field registration");
+ }
+
for (byte slot = 0; slot < 2; slot++)
{
SlotLayout c = slot == 0 ? Slot0Layout : Slot1Layout;
-
- Assert.That(host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE),
- Is.True, "reference field registration");
Assert.That(host.Construction.PlaceCompletedBuilding(slot, slot == 0 ? DefHQAlliance : DefHQLegion, c.HqOriginX, c.HqOriginY).IsValid,
Is.True, "reference HQ placement");
@@ -376,7 +391,15 @@ public void MatchBootstrap_PlacesTheCanonicalOpeningGeometry()
bootstrap.StartGrayboxMatch();
Assert.That(bootstrap.LocalFieldCell, Is.EqualTo(new Vector2Int(7, 7)));
- Assert.That(bootstrap.EnemyFieldCell, Is.EqualTo(new Vector2Int(119, 119)));
+ Assert.That(bootstrap.EnemyFieldCell, Is.EqualTo(new Vector2Int(117, 117)));
+ Assert.That(bootstrap.AllFieldCells, Is.EqualTo(new[]
+ {
+ new Vector2Int(7, 7),
+ new Vector2Int(117, 117),
+ new Vector2Int(24, 40),
+ new Vector2Int(100, 84),
+ new Vector2Int(62, 62),
+ }));
Assert.That(bootstrap.LocalHqOrigin, Is.EqualTo(new Vector2Int(4, 4)));
Assert.That(bootstrap.EnemyHqOrigin, Is.EqualTo(new Vector2Int(120, 120)));
Assert.That(bootstrap.MapSize, Is.EqualTo(new Vector2Int(MapWidth, MapHeight)));
@@ -398,6 +421,23 @@ public void MatchBootstrap_PlacesTheCanonicalOpeningGeometry()
Is.EqualTo(3000L), "the D-077 start balance (EconomySystem.CanonicalMatchStartingCreditsAE)");
}
+ [Test]
+ public void ReferenceOpeningPosition_RegistersFiveFiniteFields()
+ {
+ ReferenceHost host = BuildReferenceHost(CanonicalSeed);
+ ApplyOpeningPosition(host);
+
+ Assert.That(host.Economy.FieldCount, Is.EqualTo(FieldLayouts.Length));
+ for (int i = 0; i < FieldLayouts.Length; i++)
+ {
+ FieldLayout expected = FieldLayouts[i];
+ Assert.That(host.Economy.TryGetField(expected.Id, out AetheriumField actual), Is.True);
+ Assert.That(actual.GridPos.X, Is.EqualTo(expected.X));
+ Assert.That(actual.GridPos.Y, Is.EqualTo(expected.Y));
+ Assert.That(actual.RemainingAE, Is.EqualTo(expected.ReserveAE));
+ }
+ }
+
[Test]
public void ReferenceOpeningPosition_IsSeedSensitive()
{
diff --git a/Assets/_Project/Data/Maps/MAP_Glutrinne.asset b/Assets/_Project/Data/Maps/MAP_Glutrinne.asset
index dc5fe34..0c117ad 100644
--- a/Assets/_Project/Data/Maps/MAP_Glutrinne.asset
+++ b/Assets/_Project/Data/Maps/MAP_Glutrinne.asset
@@ -21,4 +21,7 @@ MonoBehaviour:
- {x: 121, y: 121}
_resourceNodes:
- {x: 7, y: 7}
- - {x: 119, y: 119}
+ - {x: 117, y: 117}
+ - {x: 24, y: 40}
+ - {x: 100, y: 84}
+ - {x: 62, y: 62}
diff --git a/Assets/_Project/Editor/BootstrapSceneGenerator.cs b/Assets/_Project/Editor/BootstrapSceneGenerator.cs
index 935ab1f..dd442e6 100644
--- a/Assets/_Project/Editor/BootstrapSceneGenerator.cs
+++ b/Assets/_Project/Editor/BootstrapSceneGenerator.cs
@@ -269,8 +269,8 @@ private static void CreateMapObject(MatchRunner runner, GameObject ground)
///
/// The data-layer map asset. It records exactly the graybox-accurate
/// subset of the Glutrinne manifest layout — the two spawn points and
- /// the two aetherium fields the canonical match actually registers.
- /// The full five-field layout with both primary routes is G4 scope
+ /// the five aetherium fields the canonical match registers since
+ /// Sprint 16.7. Primary-route dressing remains G4 scope
/// (docs/production/ScopeLedger.md) and is deliberately not invented here.
///
private static void EnsureGlutrinneMapAsset()
@@ -292,8 +292,15 @@ private static void EnsureGlutrinneMapAsset()
128,
// HQ footprint centres of the canonical opening (4,4)+(3x3) and its 180° mirror.
new[] { new Vector2(5f, 5f), new Vector2(121f, 121f) },
- // The two fields MatchBootstrap registers: local (7,7), enemy (119,119).
- new[] { new Vector2(7f, 7f), new Vector2(119f, 119f) });
+ // The five fields MatchBootstrap registers, in canonical id order.
+ new[]
+ {
+ new Vector2(7f, 7f),
+ new Vector2(117f, 117f),
+ new Vector2(24f, 40f),
+ new Vector2(100f, 84f),
+ new Vector2(62f, 62f),
+ });
EditorUtility.SetDirty(map);
}
diff --git a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs
index 66ba566..0a2d431 100644
--- a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs
+++ b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs
@@ -85,8 +85,9 @@ public NetworkJoinStatus(
/// quality/content/mvp-v1.json startStatePerPlayer): per slot ONLY a
/// completed HQ, ONE Builder and 3.000 AE starting credits
/// (EconomySystem.CanonicalMatchStartingCreditsAE, plumbed in by
- /// 's default) —
- /// plus one Aetherium field per slot. The Refinery is NO longer
+ /// 's default) — plus the five
+ /// finite canonical Aetherium fields (two starts, two expansions and the
+ /// contested centre). The Refinery is NO longer
/// pre-placed: the player builds it (it has no Power-plant prerequisite
/// since D-077), and the completed Refinery — not the HQ — produces the
/// Harvesters.
@@ -94,9 +95,10 @@ public NetworkJoinStatus(
///
/// It MIRRORS Determinism10000Scenario.SetupMatch
/// (tools/Nova.SimRunner/Determinism10000Scenario.cs): identical seed,
- /// identical map size, identical entity capacity, identical per-slot
- /// layout AND identical spawn ORDER — field, HQ, Builder; slot 0 first,
- /// then slot 1.
+ /// identical map size, identical entity capacity, identical field and
+ /// per-slot layout AND identical entity spawn order — HQ, Builder; slot 0
+ /// first, then slot 1. Fields register first in canonical id order but do
+ /// not allocate entity ids.
/// The order is load-bearing: the hands out
/// entity ids from a deterministic free list, so any reordering shifts
/// every id and therefore every state hash. An EditMode test asserts that
@@ -139,8 +141,32 @@ public sealed class MatchBootstrap : MonoBehaviour
/// Canonical scenario seed (DeterminismOptions.Seed). Required for InitialStateHash parity: the PRNG words are hashed into the kernel state block.
public const ulong CanonicalSeed = 0xDE7E000000010271UL;
- /// Aetherium reserve per field, in AE (Determinism10000Scenario.FieldReserveAE).
- private const long FieldReserveAE = 2000000L;
+ /// One Aetherium field of the canonical map (16.7, C1).
+ private struct FieldLayout
+ {
+ public ushort Id;
+ public int X, Y;
+ public long ReserveAE;
+ }
+
+ ///
+ /// The five canonical fields (16.7, C1 — MVPContentManifest section 5,
+ /// mirror of Determinism10000Scenario.FieldLayouts): two start fields
+ /// and two natural expansions at 9.000 AE each, one contested centre
+ /// at 15.000. Symmetry is binding: every slot-1 coordinate is the
+ /// point mirror of slot 0 through the map centre ((x, y) ->
+ /// (124 - x, 124 - y), the same mirror that maps HQ (4,4) to HQ
+ /// (120,120)), so both starts are equally far from their expansion
+ /// (36 cells) and from the centre (58).
+ ///
+ private static readonly FieldLayout[] FieldLayouts =
+ {
+ new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L },
+ new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L },
+ new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L },
+ new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L },
+ new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L },
+ };
/// maxHealth stamped by SpawnUnit when no definition stats are applied.
private const int SpawnDefaultMaxHealth = 100;
@@ -224,9 +250,27 @@ public sealed class MatchBootstrap : MonoBehaviour
/// Aetherium field cell of the human player (7, 7).
public Vector2Int LocalFieldCell => new Vector2Int(LocalPlayerLayout.FieldX, LocalPlayerLayout.FieldY);
- /// Aetherium field cell of the opponent (119, 119).
+ /// Aetherium field cell of the opponent (117, 117 since 16.7 — the exact centre mirror).
public Vector2Int EnemyFieldCell => new Vector2Int(EnemyPlayerLayout.FieldX, EnemyPlayerLayout.FieldY);
+ ///
+ /// All five registered field cells in canonical id order: start 0/1,
+ /// expansion 0/1, contested centre. Presentation iterates this list so
+ /// marker and scatter geometry cannot silently omit a field.
+ ///
+ public Vector2Int[] AllFieldCells
+ {
+ get
+ {
+ var cells = new Vector2Int[FieldLayouts.Length];
+ for (int i = 0; i < FieldLayouts.Length; i++)
+ {
+ cells[i] = new Vector2Int(FieldLayouts[i].X, FieldLayouts[i].Y);
+ }
+ return cells;
+ }
+ }
+
/// Lower-left footprint origin of the human HQ (4, 4).
public Vector2Int LocalHqOrigin => new Vector2Int(LocalPlayerLayout.HqOriginX, LocalPlayerLayout.HqOriginY);
@@ -655,7 +699,10 @@ private void BuildOpening(MatchConfig config)
// Global slot order is load-bearing for entity ids and snapshots:
// BOTH clients build slot 0 first, then slot 1, regardless of
- // which one the relay assigned locally.
+ // which one the relay assigned locally. Field registration
+ // spawns nothing and therefore does not shift ids, but it is part
+ // of the hashed initial state and must happen in one fixed order.
+ SetupFields();
SetupSlot(LocalLayout);
SetupSlot(EnemyLayout);
}
@@ -865,17 +912,29 @@ private void OnDestroy()
}
///
- /// One slot's D-077 start state: an Aetherium field, a completed HQ
- /// and one Builder near it — nothing else. Spawn order mirrors
- /// SetupMatch exactly (field, HQ, Builder).
+ /// Registers all five canonical fields in ascending id order. This is
+ /// the exact field pass mirrored by Determinism10000Scenario and both
+ /// CanonicalMatchSetupTests lanes.
///
- private void SetupSlot(SlotLayout c)
+ private void SetupFields()
{
- if (!Runner.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE))
+ for (int i = 0; i < FieldLayouts.Length; i++)
{
- throw new InvalidOperationException($"[MatchBootstrap] field {c.FieldId} could not be registered");
+ FieldLayout field = FieldLayouts[i];
+ if (!Runner.Economy.TryAddField(field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE))
+ {
+ throw new InvalidOperationException($"[MatchBootstrap] field {field.Id} could not be registered");
+ }
}
+ }
+ ///
+ /// One slot's D-077 start state: a completed HQ and one Builder near
+ /// it — nothing else. Fields are registered globally by
+ /// ; entity spawn order remains HQ, Builder.
+ ///
+ private void SetupSlot(SlotLayout c)
+ {
FactionId faction = Runner.Economy.GetSlotFaction(c.Slot);
ushort hqDefId = SimDefinitions.ToDefinitionId(faction, UnitRole.HQ);
@@ -933,13 +992,13 @@ private sealed class SlotLayout
BuilderX = 13, BuilderY = 7,
};
- /// Opponent base, top-right: the 180-degree mirror of .
+ /// Opponent base, top-right: the exact centre mirror of .
private static readonly SlotLayout EnemyLayout = new SlotLayout
{
Slot = EnemySlot,
- FieldId = 2, FieldX = 119, FieldY = 119,
+ FieldId = 2, FieldX = 117, FieldY = 117,
HqOriginX = 120, HqOriginY = 120,
- BuilderX = 113, BuilderY = 119,
+ BuilderX = 111, BuilderY = 117,
};
}
}
diff --git a/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs b/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs
index 6800c63..16b5996 100644
--- a/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs
+++ b/Assets/_Project/Scripts/Presentation/Maps/GlutrinneBlockoutView.cs
@@ -9,16 +9,13 @@ namespace Nova.Presentation.Maps
/// what is rendered is exactly what the simulation registered: the
/// procedural desert ground of the Glutrinne biome, scattered rock
/// debris, a weathered edge band instead of a hard frame, and an
- /// aetherium crystal cluster on each of the two fields the canonical
- /// match actually registers.
+ /// aetherium crystal cluster on each of the five fields the canonical
+ /// match registers.
///
/// Pure presentation: this component reads the bootstrap's layout
/// properties and spawns primitive-only markers; it never writes into
- /// simulation state. The full five-field manifest layout with the two
- /// primary attack routes is G4 scope and deliberately NOT shown — the
- /// blockout must not promise fields the match does not have
- /// (docs/production/ScopeLedger.md, rows map.aetheriumFields /
- /// map.primaryRouteCount).
+ /// simulation state. The five-field manifest layout is visible since
+ /// Sprint 16.7; primary-route dressing remains later map-art scope.
///
///
/// KARTENBILD (D-085): everything here is generated at runtime with a
@@ -92,11 +89,14 @@ private void Start()
return;
}
+ Vector2Int[] fieldCells = _bootstrap.AllFieldCells;
TintGround();
- BuildScatterRocks();
+ BuildScatterRocks(fieldCells);
BuildWeatheredEdge(_bootstrap.MapSize);
- BuildFieldMarker(_bootstrap.LocalFieldCell, "Local");
- BuildFieldMarker(_bootstrap.EnemyFieldCell, "Enemy");
+ for (int i = 0; i < fieldCells.Length; i++)
+ {
+ BuildFieldMarker(fieldCells[i], $"Field_{i + 1}");
+ }
}
///
@@ -140,11 +140,11 @@ private void TintGround()
///
/// Rock debris: squashed-sphere boulders and pebbles, placed by a
/// fixed-seed xorshift (no UnityEngine.Random), rejected inside the
- /// exclusion zones around both start bases and both aetherium
+ /// exclusion zones around both start bases and all five aetherium
/// fields, and NEVER carrying a collider — the debris is pure
/// visual, the sim's grid pathing does not see it (and must not).
///
- private void BuildScatterRocks()
+ private void BuildScatterRocks(Vector2Int[] fieldCells)
{
Vector2Int mapSize = _bootstrap.MapSize;
var scatter = new GameObject("ScatterRocks");
@@ -157,7 +157,7 @@ private void BuildScatterRocks()
{
float x = 2f + Next01(ref rng) * (mapSize.x - 4f);
float z = 2f + Next01(ref rng) * (mapSize.y - 4f);
- if (IsExcluded(x, z)) continue;
+ if (IsExcluded(x, z, fieldCells)) continue;
float sx = 0.25f + Next01(ref rng) * 0.70f;
float sy = 0.15f + Next01(ref rng) * 0.40f;
@@ -180,16 +180,25 @@ private void BuildScatterRocks()
}
/// Inside a start-base or aetherium-field exclusion zone the scatter stays out (the D-085 brief).
- private bool IsExcluded(float x, float z)
+ private bool IsExcluded(float x, float z, Vector2Int[] fieldCells)
{
Vector2Int localHq = _bootstrap.LocalHqCenterCell;
Vector2Int enemyHq = _bootstrap.EnemyHqCenterCell;
- Vector2Int localField = _bootstrap.LocalFieldCell;
- Vector2Int enemyField = _bootstrap.EnemyFieldCell;
- return WithinRadius(x, z, localHq.x + 0.5f, localHq.y + 0.5f, BaseExclusionRadius)
- || WithinRadius(x, z, enemyHq.x + 0.5f, enemyHq.y + 0.5f, BaseExclusionRadius)
- || WithinRadius(x, z, localField.x + 0.5f, localField.y + 0.5f, FieldExclusionRadius)
- || WithinRadius(x, z, enemyField.x + 0.5f, enemyField.y + 0.5f, FieldExclusionRadius);
+ if (WithinRadius(x, z, localHq.x + 0.5f, localHq.y + 0.5f, BaseExclusionRadius)
+ || WithinRadius(x, z, enemyHq.x + 0.5f, enemyHq.y + 0.5f, BaseExclusionRadius))
+ {
+ return true;
+ }
+
+ for (int i = 0; i < fieldCells.Length; i++)
+ {
+ Vector2Int field = fieldCells[i];
+ if (WithinRadius(x, z, field.x + 0.5f, field.y + 0.5f, FieldExclusionRadius))
+ {
+ return true;
+ }
+ }
+ return false;
}
private static bool WithinRadius(float x, float z, float cx, float cz, float radius)
diff --git a/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs b/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs
index e38d655..3391cb3 100644
--- a/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs
+++ b/Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs
@@ -129,7 +129,11 @@ public sealed class EconomySystem : IStatefulSimSystem, ISlotFactionLookup
/// Format capacity for Aetherium fields (map content: 5 fields in mvp-v1).
public const int MaxFields = 64;
- /// Provisional harvest rate in AE per tick per harvester (Q-040 candidate).
+ ///
+ /// Harvest rate in AE per tick per Harvester. D-102 deliberately keeps
+ /// the provisional value at 2 for 16.7 and defers calibration until a
+ /// played timing curve provides evidence.
+ ///
public const int HarvestRateAE = 2;
///
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e883c69..d377a05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,14 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
das Tor identisch zur Kopfzahl. Die Partie bestätigt damit die Neutralität;
sie belegt keine Verbesserung.
+### Geändert
+- **16.7/C1: Fünf endliche Aetheriumfelder schaffen Knappheit (D-102)** — die
+ zwei praktisch endlosen Startfelder werden durch zwei symmetrische
+ Startfelder und zwei Expansionen mit je 9.000 AE sowie ein umkämpftes
+ Zentrum mit 15.000 AE ersetzt. Alle fünf Startaufstellungs-Spiegel und die
+ Kartenmarker folgen derselben Reihenfolge; `HarvestRateAE` bleibt bewusst
+ bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt
+
### Behoben
- **Low Power ist eine Waffe (C4, Sprint 16.6)** — bei Energiedefizit
fällt Radar zuerst: `FogOfWarSystem.GetRadarSignatures` liefert nichts mehr
diff --git a/README.md b/README.md
index 44fa2a5..00948c7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Project Nova
-**Dokumentversion:** 0.20.0 | **Status:** unveröffentlichter Entwicklungsstand, spielbar | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Stand:** 2026-08-10
+**Dokumentversion:** 0.21.0 | **Status:** unveröffentlichter Entwicklungsstand, spielbar | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Stand:** 2026-08-10
> Ein Echtzeitstrategiespiel in der Tradition von **Command & Conquer** — Basisbau,
> Ernte, Armee, Karte kontrollieren. Gebaut mit Unity und C#, offen entwickelt.
@@ -156,9 +156,10 @@ funktionierender Loop:
- **Gefechtsdichte ist noch nicht manuell abgenommen.** VFX und SFX sind
implementiert; die geplante Sicht- und Gegenhörprüfung mit 60 Einheiten
bleibt offen.
-- **Strang C ist offen.** Endliche Aetheriumfelder, wirksames Lager und Radar,
- Low-Power-Wirkung sowie die übrigen Wirtschafts- und Platzierungsregeln sind
- noch nicht umgesetzt.
+- **Strang C läuft weiter.** Fünf endliche Aetheriumfelder, Lagergrenze, Radar
+ und Low-Power-Wirkung sind umgesetzt; Mehrfachvoraussetzungen,
+ Platzierungsregeln, Reparaturkosten und ehrliches Blocker-Feedback folgen
+ noch in den nachgelagerten Paketen.
- **Kein Speichern.** Die Simulation kann ihren Zustand vollständig
serialisieren und hash-identisch fortsetzen — es fehlt nur das Schreiben auf
die Platte.
@@ -284,10 +285,9 @@ Runde*.
- **Netzwerk und Gefechtsdichte sind noch nicht vollständig manuell
abgenommen.** A8 Stufen 2–4 sowie die 60-Einheiten-Sicht-/Gegenhörprüfung
bleiben offen.
-- **Strang C ist offen.** Wirtschaftsdruck, Low Power und die ausstehenden
- Gebäude- und Platzierungsregeln fehlen noch.
-- **Lager und Radar kosten Geld und tun nichts.** Zwei von neun Gebäuden warten
- noch auf ihre Wirkung.
+- **Strang C läuft weiter.** Wirtschaftsdruck, Lager, Radar und Low Power sind
+ da; Mehrfachvoraussetzungen, Platzierungsregeln, Reparaturkosten und das
+ vollständige Entscheidungsfeedback fehlen noch.
Die vollständigen Grenzen stehen im aktuellen
[Sprintbericht](docs/production/hashkrieg/12_Sprint_Zu_Zweit.md).
@@ -434,7 +434,9 @@ nicht rückwirkend übertragen.
## Offene Punkte
-- **Die Wirtschaftsfrage aus §2** ist die wichtigste offene Entscheidung.
+- **Die verbleibenden Strang-C-Pakete aus §2** — Voraussetzungen,
+ Platzierung/Reparaturkosten und Entscheidungsfeedback — sind die wichtigste
+ offene Umsetzungsfolge.
- Der Umbenennungsbeschluss auf *Hashkrieg* ist im Bestand dieses Repositories
noch nicht vollzogen — Repo, Code und Wiki laufen weiter unter *Project Nova*.
- Q-018 (Preis) und Q-019 (Telemetrie) bleiben offen und blockieren MS-1 nicht.
@@ -447,12 +449,12 @@ Zur Bewertung, in dieser Reihenfolge:
2–4; Linux/systemd und der Live-Workflow müssen ebenfalls real laufen.
2. **Gefechtsfeedback manuell abnehmen** — ungefähr 60 feuernde Einheiten,
SFX-Regler, Klangbalance und Kamera-Listener gegenhören und ansehen.
-3. **Wirtschaftsdruck** — endliche Aetheriumfelder geben der Runde einen Bogen und
- einen Grund, um Gebiet zu kämpfen.
+3. **Strang C abschließen** — Mehrfachvoraussetzungen,
+ Platzierungs-/Reparaturregeln und ehrliches Blocker-Feedback integrieren.
4. **Attack-Move** — Truppen sollen unterwegs Gegner bekämpfen, ohne jeden
Kontakt einzeln befohlen zu bekommen.
-5. **Gebäude mit Wirkung und KI-Ausbau** — Lager und Radar warten auf ihre
- Funktion; der Gegner spielt, aber schlicht.
+5. **KI-Ausbau** — der Gegner spielt, aber weiterhin schlicht; die neuen
+ Wirtschaftsregeln müssen später auch in sein strategisches Verhalten einfließen.
Die Gate-Kette G0–G5 ruht unter Tier 2 und wird erst wieder aufgenommen, wenn das
Projekt ein Publikum hat.
@@ -465,6 +467,7 @@ Projekt ein Publikum hat.
| 0.19.0 | 2026-08-08 | D-091: Source-available Lizenz, CLA und Tier-2-Beitragsmodell ergänzt; die zwei Maintainer mit Merge-Recht und die Asset-/Markenabgrenzung klar benannt | Technical Writer |
| 0.19.1 | 2026-08-08 | CLA-Wirkung auf Beiträge mit dokumentierter Zustimmung begrenzt; keine rückwirkende Rechteübertragung unterstellt | Technical Writer |
| 0.20.0 | 2026-08-10 | D-105: alleinige Projektleitung und Merge-Autorität von `@cubetribe`, externe aktuelle Inhaberfreigabe sowie die ehrliche Zurückstellung manueller Spielabnahmen dokumentiert | Technical Writer |
+| 0.21.0 | 2026-08-10 | Strang-C-Status auf #73/#77/#78/#80 nachgezogen: Lagergrenze, Radar, Low Power und fünf endliche Aetheriumfelder sind umgesetzt; die tatsächlich verbleibenden Pakete bleiben offen | Codex / Dennis Westermann |
| 0.7.1 | 2026-07-24 | Recovery-Baseline nach Implementierungs-Audit | Executive Producer / Lead Technical Director |
| 0.8.0 | 2026-07-24 | Closed-Core MS-1, exakten Engine-Pin, G0-offenen Status und Quality-Verträge D-056–D-061 aufgenommen | Executive Producer / Technical Writer |
| 0.8.1 | 2026-07-24 | Evidence-Semantikvalidator ergänzt und Dokumentstruktur korrigiert | Technical Writer / Lead QA Engineer |
diff --git a/docs/README.md b/docs/README.md
index f09bc65..64e1334 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
# Project Nova – Entwicklungs-Wiki
-**Version:** 0.19.0 | **Status:** unveröffentlichter Arbeitsstand – Lobby (Sprint 14) client- und relay-seitig implementiert und getestet; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme offen | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Sprint:** 16
+**Version:** 0.20.0 | **Status:** unveröffentlichter Arbeitsstand – Lobby (Sprint 14) client- und relay-seitig implementiert und getestet; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme offen | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Sprint:** 16
## Zweck
@@ -183,7 +183,7 @@ gelisteten Kernverträge.
- [RiskAnalysis](production/RiskAnalysis.md)
- [GrayboxLog](production/GrayboxLog.md) – Sitzungsprotokoll der Graybox-Spur (D-067, Entwurf)
- [ScopeLedger](production/ScopeLedger.md) – Zurückstellungen der Graybox-Spur, verweist auf Manifest-Schlüsselpfade
-- [DemoRunbook](production/DemoRunbook.md) (0.1.0, Entwurf) – erste Demo-Runde: Ablauf, Steuerung, bekannte Grenzen, Asset-Ablage
+- [DemoRunbook](production/DemoRunbook.md) (0.6.0, Entwurf) – erste Demo-Runde: Ablauf, Steuerung, fünf endliche Aetheriumfelder, bekannte Grenzen und Asset-Ablage
- [StatusSnapshot 2026-08-05](production/StatusSnapshot_2026-08-05.md) (0.1.0) – datierter Projektstand vor dem Eintreffen der ersten 3D-Assets
- [Hashkrieg-Planungsmappe](production/hashkrieg/README.md) und
[Sprint 12 „Zu zweit"](production/hashkrieg/12_Sprint_Zu_Zweit.md) –
@@ -273,3 +273,4 @@ kann keine Datei einen Gate-Pass erzeugen.
| 0.17.0 | 2026-08-08 | D-091 und Sprint 13.0 indexiert: Tier-2-Beitragsmodell, Lizenz- und Merge-Schutz vorbereitet | Technical Writer |
| 0.18.0 | 2026-08-09 | Sprint-14-Lobby indexiert: LobbySupabase.md (Vertrag, Schema, Edge-Function-Referenzen, Betriebspfad) und RelayServer.md 1.1.0 (kurzlebige Lobby-Tokens) aufgenommen, D-092 bis D-094; Supabase-Anlage, Relay-Redeploy und gespielte Abnahme ausdrücklich offen | Agent (Umsetzung) |
| 0.19.0 | 2026-08-10 | D-105 indexiert: Dennis Westermann ist alleiniger Projektinhaber, Tier-Entscheider und Mergeberechtigter; Tier 2 und die externen CLA-/Review-Regeln bleiben aktiv | Technical Writer |
+| 0.20.0 | 2026-08-10 | DemoRunbook 0.6.0 indexiert: D-102/Sprint 16.7 ersetzt die alte Zwei-Feld-Demo durch fünf endliche, sichtbare Aetheriumfelder | Codex / Dennis Westermann |
diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md
index 4d9d73d..0ba7eb5 100644
--- a/docs/production/DecisionLog.md
+++ b/docs/production/DecisionLog.md
@@ -1,6 +1,6 @@
# Decision Log
-**Version:** 1.35.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16
+**Version:** 1.36.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16
## Zweck
@@ -3005,6 +3005,52 @@ Wer den Harness umbenennt, sagt es an. Keine Baseline-Datei ist berührt.
---
+### D-102 | verbindlich | Sprint 16 (fünf endliche Aetheriumfelder; Ernterate getrennt kalibrieren)
+
+**Status:** Inhaberfreigabe vom 2026-08-09.
+
+**Kontext:** Die kanonische Glutrinne registriert bislang zwei Felder mit je
+2.000.000 AE. Ein Sammler könnte daran rund 28 Stunden ununterbrochen ernten;
+Expansion und Kartenmitte haben damit keinen wirtschaftlichen Zweck. Das
+MVP-Manifest nennt fünf Felder mit 9.000 beziehungsweise 15.000 AE, während
+`EconomySystem.HarvestRateAE = 2` ausdrücklich nur provisorisch ist. Für eine
+neue Ernterate existiert weder ein verbindlicher Zielwert noch gespielte
+Messung.
+
+**Alternativen:** (a) Feldlayout und Ernterate gleichzeitig nach Gefühl
+ändern; (b) die endlichen Manifestfelder liefern und die Rate unverändert
+lassen, bis eine gespielte Kurve kalibriert werden kann; (c) Knappheit bis zur
+vollständigen Balance-Runde vertagen und die praktisch endlosen Felder
+behalten.
+
+**Entscheidung:** (b). Die kanonische Karte registriert in dieser Reihenfolge:
+
+1. Start Slot 0 `(7,7)`, 9.000 AE;
+2. Start Slot 1 `(117,117)`, 9.000 AE;
+3. Expansion Slot 0 `(24,40)`, 9.000 AE;
+4. Expansion Slot 1 `(100,84)`, 9.000 AE;
+5. Zentrum `(62,62)`, 15.000 AE.
+
+Die Paarpositionen sind durch `(x,y) → (124-x,124-y)` punktgespiegelt. Felder
+werden vor den HQ-/Builder-Entitäten in aufsteigender ID-Reihenfolge
+registriert. `HarvestRateAE` bleibt für Paket 16.7 bei **2 AE/Tick**; ihre
+Kalibrierung ist sichtbar vertagt und nicht als erledigt zu melden.
+
+**Begründung:** Die Manifestreserven beheben den belegten
+Knappheitsdefekt, ohne eine unbelegte zweite Balancevariable zu verändern.
+Symmetrie verhindert einen Startvorteil, die feste Reihenfolge hält beide
+Lockstep-Hosts und den Headless-Harness identisch.
+
+**Konsequenzen:** Alle fünf Spiegel der Startaufstellung müssen gemeinsam
+ziehen; die Präsentation markiert und schützt alle fünf Felder vor
+Steinstreuung. Der Definitions-Hash und das Zustandsformat ändern sich nicht,
+der gehashte Initialzustand aber schon. Geschützte Determinismus-Baselines
+werden nur in einem getrennten Baseline-PR bewegt, falls der Guard dies
+verlangt. Die Ernteraten-Kalibrierung braucht eine gespielte Runde mit
+Zeitkurve und bleibt bis dahin offen.
+
+---
+
### D-105 | verbindlich | Sprint 16 (alleinige Projektleitung und Merge-Autorität)
**Status:** unmittelbar wirksame Inhaberentscheidung vom 2026-08-10 (Dennis
@@ -3264,6 +3310,7 @@ Konvergenz, `long.MaxValue` und die Ablehnung des alten Rules-Stubs ab.
| Version | Datum | Änderung | Autor |
|---|---|---|---|
+| 1.36.0 | 2026-08-10 | D-102 aufgenommen: fünf symmetrische endliche Aetheriumfelder werden geliefert, die mangels belastbarer Zielkurve unveränderte Ernterate von 2 AE/Tick wird ausdrücklich getrennt kalibriert | Project Owner / Agent |
| 1.35.0 | 2026-08-10 | D-106 aufgenommen: einmalige 2.000-AE-HQ-Basis, +2.000 je fertigem Lager, sofort gedeckelte Einzahlungen, zustandsloser 25-%-Abbau des aktuellen Überhangs je Sekunde und kanonischer Rules-Hash; D-024/D-096 in Verlustausformung, HQ-Stapelung und Replay-Kompatibilität teilweise ersetzt; Fehlverweise der Lobbyfamilie D-095–D-097 auf D-092–D-094 berichtigt | Agent (unter Delegation) / Dennis Westermann |
| 1.34.0 | 2026-08-10 | D-105 aufgenommen: Dennis Westermann ist alleiniger Projektinhaber, Tier-Entscheider und Mergeberechtigter; Tier 2 bleibt mit CLA und aktueller Inhaberfreigabe aktiv, Inhaber-PRs dürfen nach grüner Pflicht-CI und unabhängigem Review selbst gemergt werden, und manuelle Spielabnahme darf ehrlich zurückgestellt, aber nicht als gelaufen behauptet werden | Project Owner / Orchestrator |
| 1.33.0 | 2026-08-09 | D-101 aufgenommen: der Ausgangspin der kanonischen KI-Partie (Entscheidungstick, Endzustand) wird vom Identitätspin getrennt und zieht in eine Maintainer-Datei; `tools/Nova.SimRunner.Tests/` bekommt erstmals eine Eigentümerzeile | Project Owner / Orchestrator |
diff --git a/docs/production/DemoRunbook.md b/docs/production/DemoRunbook.md
index 6f48149..52e3562 100644
--- a/docs/production/DemoRunbook.md
+++ b/docs/production/DemoRunbook.md
@@ -1,6 +1,6 @@
# Demo-Runbook – erste spielbare Runde (Glutrinne-Graybox)
-**Version:** 0.5.0 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 7
+**Version:** 0.6.0 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16
## Zweck
@@ -42,7 +42,7 @@ Gate-Status steht ausschließlich in [MVPRecoveryPlan.md](MVPRecoveryPlan.md).
Musiklautstärke in den Einstellungen auf 0 oder Musik ist ausgeschaltet —
siehe §7.
-## 2. Was die Demo zeigt (Spielstand GB-005, D-077 + Hauptmenü, D-083)
+## 2. Was die Demo zeigt (Spielstand Sprint 16.7, D-077/D-102 + Hauptmenü, D-083)
- **Hauptmenü mit Musik (D-083):** Key Art im Vollbild, Titel „HASHKRIEG",
vier Einträge — **Neues Spiel / Laden / Einstellungen / Beenden**. „Laden" ist
@@ -51,8 +51,10 @@ Gate-Status steht ausschließlich in [MVPRecoveryPlan.md](MVPRecoveryPlan.md).
Die Einstellungen überleben den Neustart (§7). Es ist das erste UI-Toolkit-UI
des Projekts; das übrige HUD bleibt bis auf Weiteres OnGUI-Wegwerfcode.
- **Karte „Glutrinne" (Blockout):** Wüstengetönte 128×128-Ebene, dunkler
- Kartenrand-Rahmen, Aetherium-Kristallmarker (cyan) auf den beiden Feldern,
- die das kanonische Match registriert (lokal (7,7), gegnerisch (119,119)).
+ Kartenrand-Rahmen und Aetherium-Kristallmarker (cyan) auf allen fünf
+ kanonischen Feldern: Start `(7,7)` / `(117,117)` und Expansion
+ `(24,40)` / `(100,84)` mit je 9.000 AE sowie Zentrum `(62,62)` mit
+ 15.000 AE (D-102).
- **Startaufstellung je Slot:** HQ + 1 Builder + 3.000 AE. Slot 0 (Mensch) =
Allianz, Slot 1 = Legion. Mehr gibt es nicht — der Kernloop wird gespielt,
nicht geschenkt.
@@ -179,8 +181,8 @@ steht davor und dauert so lange, wie man ihn zeigen will.
Fraktionswahl hängt an `InitialStateHash` und wäre eine Determinismus-,
keine Menü-Änderung (D-083).
- Aetherium-Felder sind endlich, aber statisch (kein Nachwachsen, keine
- Warnung); das Manifest-Layout mit 5 Feldern und 2 Angriffswegen ist G4-Scope
- – der Blockout zeigt bewusst nur die zwei real registrierten Felder.
+ Warnung). Das Fünf-Feld-Layout ist seit D-102 registriert und sichtbar;
+ die zwei ausgearbeiteten Angriffswege bleiben G4-Scope.
- Erledigt seit GB-005 (hier nur als Historie): das Vollbild-Debug-Overlay ist
standardmäßig aus (F3); die 3D-Modelle überlagern sich nicht mehr
(Laufzeit-Normierung auf den Sim-Footprint); der KI-Slot spielt; der
@@ -272,3 +274,4 @@ der Editor in der Konsole, wenn das Schreiben fehlschlägt.
| 0.3.0 | 2026-08-06 | Stand GB-005 (D-077): Start HQ + Builder + 3.000 AE, Kernloop-Ablauf neu (Raffinerie → Harvester → Kaserne), KI-Gegner aktiv, Sieg bei HQ-Zerstörung, Statusleiste + F3-Panel, Skalierungsreparatur vermerkt | Agent |
| 0.4.0 | 2026-08-06 | Hauptmenü (D-083): §1 korrigiert – Play zeigt das Menü, das Match startet über „Neues Spiel" (`AutoStart = false`), nicht mehr von selbst; §2 um Menü, Key Art und Menümusik ergänzt; §4 um einen Menüschritt vorangestellt und durchnummeriert (Zeitmarken zählen ab Matchstart); §5 um „Laden" ausgegraut, wirkungslosen SFX-Regler, gemeinsames URP-Asset über alle sechs Render-Detail-Stufen und fehlenden Rückweg ins Menü erweitert; neues §7 zu `settings.json` in `Application.persistentDataPath` (Inhalt, Zurücksetzen, Verhalten bei kaputter Datei) | Agent |
| 0.5.0 | 2026-08-06 | Bedienbares HUD (D-084): §2 um Bauleiste/Ghost-Platzierung, Command Card, Minimap, sichtbaren Nebel und Selektionsmarker ergänzt; §3 um MMB-Drag-Rotation, Space-Reset und die tastaturfreien Wege über Bauleiste/Command Card erweitert | Agent |
+| 0.6.0 | 2026-08-10 | D-102/Sprint 16.7 nachgezogen: fünf endliche Aetheriumfelder samt Markerpositionen und Reserven ersetzen die alte Zwei-Feld-Beschreibung; Angriffswege bleiben G4-Scope | Codex / Dennis Westermann |
diff --git a/docs/production/ScopeLedger.md b/docs/production/ScopeLedger.md
index 23267bd..34a6db5 100644
--- a/docs/production/ScopeLedger.md
+++ b/docs/production/ScopeLedger.md
@@ -1,6 +1,6 @@
# Scope-Ledger der Graybox-Spur
-**Version:** 0.6.0 | **Status:** laufendes Register – Graybox-Entwurf D-067 plus verbindliche 12B-Abweichungen D-090 | **Verantwortungsbereich:** Orchestrator / Technical Writer | **Sprint:** 12
+**Version:** 0.7.0 | **Status:** laufendes Register – Graybox-Entwurf D-067 plus verbindliche 12B-Abweichungen D-090 und D-102-Fünf-Feld-Stand | **Verantwortungsbereich:** Orchestrator / Technical Writer | **Sprint:** 16
## Zweck
@@ -41,7 +41,7 @@ funktionalen Anteil, das genannte Gate den vollständigen Inhalt.
|---|---|---|---|
| `startStatePerPlayer.unitRoles` | Startaufstellung des Determinismus-Szenarios portiert; zusätzlich vier Infanterieeinheiten je Slot, damit überhaupt etwas zu sehen ist | G4 | D-067 K1, K2 |
| `map.id`, `map.biome` | die Karte heißt seit GB-003 Glutrinne und zeigt einen Wüsten-Blockout (Sandtönung, Kartenrand-Rahmen, Kristallmarker – reine Präsentation über `GlutrinneBlockoutView` plus Datenasset `MAP_Glutrinne.asset`); weiterhin kein Terrain-/Biom-System und keine Hindernisse, nur die Kantenlänge stimmt | G4 (G2: technisch korrektes Testlayout) | D-067 K1, K2 |
-| `map.aetheriumFields` | zwei Felder an festen Zellen nahe den Startbasen statt der im Manifest festgelegten Feldliste; seit GB-003 als Kristallmarker sichtbar, Reserven und Verhalten unverändert | G4 (G2) | D-067 K1, K2 |
+| `map.aetheriumFields` | seit D-102 sind fünf endliche Felder in Manifestanzahl und -reserven an festen, punktgespiegelten Zellen registriert und als Kristallmarker sichtbar; Startaufstellung, Headless-Szenario, Datenasset und Tests sind synchron, aber eine gespielte Abnahme beziehungsweise ein G4-Nachweis fehlt weiterhin | G4 (G2) | D-067 K1, K2; D-102 |
| `map.primaryRouteCount` | keine Routenführung; die Ebene ist überall passierbar | G4 (G2) | D-067 K1, K2 |
| `factions[1]` | seit dieser Sitzung Simulationswirklichkeit: 34 fraktionsaufgelöste Definitionen (`SimDefinitions`, Id-Regel und Provenienz per D-075), Slot-Fraktion im Economy-Snapshotblock v2 mit `SetSlotFaction`-Guard, fraktionsaufgelöste Kosten/Bauzeiten/Energie/Waffenwerte, Graybox-Farben aus den D-072-Paletten im `UnitViewManager` und Slot-Fraktionen im Debug-HUD. Verbleibend: das `weaponProfile` der Identität (eigene Zeile) und der untätige KI-Slot (eigene Zeile). Kein Gate-Nachweis — die Zeile bleibt bis zur auflösenden Evidence | G4 | D-067 K1, K2 |
| `factions[1].identity.harvesterCargoAE` | seit dieser Sitzung im Code erfüllt: die Kapazität lebt in der Harvester-Definitionszeile (`SimUnitDefinition.CargoCapacityAE`), das `EconomySystem` klammert die Ernte fraktionsaufgelöst; die Entity-Store-Snapshotvalidierung deckelt auf das fraktionsübergreifende Maximum und bietet die pro-Entity-Fraktionsgrenze als Überladung. Kein Gate-Nachweis — die Zeile bleibt bis zur auflösenden Evidence | G4 | D-067 K1, K2 |
@@ -152,3 +152,4 @@ implementierten Gefechtsfeedback fest. D-090 ist die führende Entscheidung.
| 0.4.0 | 2026-08-05 | Sitzung GB-003: Zeilen `map.id`/`map.biome` und `map.aetheriumFields` auf den Stand des Glutrinne-Blockouts fortgeschrieben (Wüsten-Präsentation und Kristallmarker, weiterhin kein Terrain-System und nur zwei registrierte Felder; nicht entfernt – es gibt keinen auflösenden Gate-Nachweis) | Technical Writer |
| 0.5.0 | 2026-08-05 | Sitzung GB-004: Zeile `persistence.pauseRequired` fortgeschrieben (Pause an P gebunden; kein Pausenmenü – Zeile bleibt bis G2) | Technical Writer |
| 0.6.0 | 2026-08-08 | Separaten D-090-Abschnitt mit sämtlichen bekannten Abweichungen des ausgeführten Sprint-12-Strangs B ergänzt; Graybox-Hauptregister und Manifestverweise unverändert erhalten | Technical Writer / Agent (Umsetzung) |
+| 0.7.0 | 2026-08-10 | `map.aetheriumFields` auf D-102/Sprint 16.7 fortgeschrieben: fünf endliche Felder sind registriert, sichtbar und automatisiert gespiegelt; mangels gespielter Abnahme bleibt die Zeile bis zum Gate-Nachweis bestehen | Codex / Dennis Westermann |
diff --git a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md
index 27b1e3f..ed11f2c 100644
--- a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md
+++ b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md
@@ -1,6 +1,6 @@
# Sprint 16: Die Wirtschaft trägt sich selbst — kein Gebäude kostet Geld, ohne etwas zu tun
-**Version:** 1.3.0 | **Status:** in Umsetzung | **Verantwortungsbereich:** Netzstrang (Maintainer) | **Sprint:** 16 | **Vorgänger:** [12_Sprint_Zu_Zweit.md](12_Sprint_Zu_Zweit.md) Strang C | **Parallel zu:** [13B](13B_Sprint_Einheitenverhalten.md) | **Regelwerk:** [13-15_Parallelbetrieb.md](13-15_Parallelbetrieb.md) | **UX-Gate:** human | **Leitsatz:** ein Gebäude, das Strom zieht und nichts tut, ist kein Platzhalter, sondern ein Schaden
+**Version:** 1.4.0 | **Status:** in Umsetzung | **Verantwortungsbereich:** Netzstrang (Maintainer) | **Sprint:** 16 | **Vorgänger:** [12_Sprint_Zu_Zweit.md](12_Sprint_Zu_Zweit.md) Strang C | **Parallel zu:** [13B](13B_Sprint_Einheitenverhalten.md) | **Regelwerk:** [13-15_Parallelbetrieb.md](13-15_Parallelbetrieb.md) | **UX-Gate:** human | **Leitsatz:** ein Gebäude, das Strom zieht und nichts tut, ist kein Platzhalter, sondern ein Schaden
## Zweck
@@ -236,14 +236,16 @@ aktuellen Host aber vor Tick 1 mit `RulesHash64`-Mismatch abgelehnt.
|---|---|---|
| Feldreserve | 2.000.000 AE (≈ 28 h ununterbrochene Ernte eines Sammlers) | Manifestwerte **9.000 / 15.000 AE** |
| Feldanzahl | 2 (je Slot eins) | **5** — 2 Start, 2 Expansion, 1 umkämpftes Zentrum |
-| Ernterate | 2 AE/Tick, als Provisorium markiert | gegen die Zielkurve kalibriert |
+| Ernterate | 2 AE/Tick, als Provisorium markiert | **bleibt in 16.7 bei 2 AE/Tick**; gespielte Kalibrierung bewusst vertagt (D-102) |
**Symmetrie ist Pflicht.** Beide Startpositionen müssen gleich weit zu Expansion
und Zentrum liegen — sonst entscheidet die Karte das erste Mensch-gegen-Mensch-Match.
-Keiner der drei Zielwerte liegt in `SimDefinitions`: `FieldReserveAE` und die
-Feldpositionen stehen in `MatchBootstrap`, `HarvestRateAE` in `EconomySystem`.
-Der Definitions-Hash bewegt sich hier **nicht** — das tut nur 16.8.
+Feldreserven und Feldpositionen liegen nicht in `SimDefinitions`, sondern in
+der kanonischen Startaufstellung. D-102 trennt diese belegte
+Knappheitskorrektur ausdrücklich von der noch unbelegten Ernteraten-Kalibrierung;
+`HarvestRateAE` bleibt unverändert. Der Definitions-Hash bewegt sich hier
+**nicht** — das tut erst 16.8.
**Fünf synchrone Stellen** (siehe Regelwerk, „kanonische Startaufstellung"):
@@ -368,9 +370,10 @@ dann **16.6**. Jeder Abwurf mit Begründung in den
|---|---|---|
| D-096 | Lager erhält eine **abgeleitete** AE-Obergrenze (kein Zustandsfeld); Radar schaltet die Minimap frei und leitet seine Abdeckung vom Gebäude ab | Inhaber (Richtung) / Agent (Ausformung) |
| D-097 | „Stoppen" löscht den Angriffsbefehl; ein Halte-Feuer bleibt beim Einheitenstrang | Inhaber |
+| D-102 | Fünf endliche, punktgespiegelte Aetheriumfelder; `HarvestRateAE` bleibt bis zur gespielten Kalibrierung bei 2 AE/Tick | Inhaber / Agent |
| D-106 | AE-Kontobasis gilt einmalig je Slot; vorhandener Überhang zerfällt zustandslos pro Sekunde und die Regelrevision wird im Match-Fingerprint gebunden | Agent unter Inhaberdelegation |
-D-096, D-097 und D-106 sind im [DecisionLog](../DecisionLog.md) eingetragen. D-098
+D-096, D-097, D-102 und D-106 sind im [DecisionLog](../DecisionLog.md) eingetragen. D-098
(Entwurf) und D-099 stehen dort für [Sprint 17](17_Sprint_Zugangsprotokoll.md),
D-100 bleibt für dessen Paket B vorgemerkt, D-098 gehört zu
[Sprint 14](14_Sprint_Lobby.md). Keine dieser Nummern darf hier verbraucht
@@ -396,6 +399,7 @@ kein Bruch.
| Version | Datum | Änderung | Autor |
|---|---|---|---|
+| 1.4.0 | 2026-08-10 | D-102 ergänzt: fünf endliche symmetrische Felder sind Paket 16.7; die Ernterate bleibt mangels gespielter Zielkurve ausdrücklich bei 2 AE/Tick und wird getrennt kalibriert | Project Owner / Agent |
| 1.3.0 | 2026-08-10 | C4-Kompatibilitätsgrenze dokumentiert: Low-Power-Reparatur bindet Rules-Revision 2 und 10/5 HP pro Tick, ohne Zustands- oder Schema-Bump | Codex / Dennis Westermann |
| 1.2.0 | 2026-08-10 | D-106 für 16.4 festgeschrieben: einmalige HQ-Kontobasis, periodischer 25-%-Abbau des aktuellen Überhangs und Rules-Hash-Kompatibilitätsgrenze | Codex / Dennis Westermann |
| 1.1.0 | 2026-08-10 | D-105-Integrationsausnahme für 16.3 dokumentiert: aktive Sites sind keine Kampfteilnehmer oder fertigen KI-Produzenten | Codex / Dennis Westermann |
diff --git a/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs b/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs
index 337b67e..2bf06a8 100644
--- a/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs
+++ b/tools/Nova.SimRunner.Tests/CanonicalMatchSetupTests.cs
@@ -69,7 +69,21 @@ public sealed class CanonicalMatchSetupTests
private const ushort MapWidth = 128;
private const ushort MapHeight = 128;
private const int EntityCapacity = 1024;
- private const long FieldReserveAE = 2000000L;
+ private struct FieldLayout
+ {
+ public ushort Id;
+ public int X, Y;
+ public long ReserveAE;
+ }
+
+ private static readonly FieldLayout[] FieldLayouts =
+ {
+ new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L },
+ new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L },
+ new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L },
+ new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L },
+ new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L },
+ };
// Faction-resolved opening placement ids (SimDefinitions id rule):
// slot 0 Alliance (role value), slot 1 Legion (role value + 17).
private const ushort DefHQAlliance = 3;
@@ -142,46 +156,47 @@ private static ReferenceHost BuildReferenceHost(ulong seed)
/// Fixed opening layout of one slot, in grid cells.
private sealed class SlotLayout
{
- public ushort FieldId;
- public int FieldX, FieldY;
public int HqOriginX, HqOriginY;
public int BuilderX, BuilderY;
}
private static readonly SlotLayout Slot0Layout = new SlotLayout
{
- FieldId = 1, FieldX = 7, FieldY = 7,
HqOriginX = 4, HqOriginY = 4,
BuilderX = 13, BuilderY = 7,
};
private static readonly SlotLayout Slot1Layout = new SlotLayout
{
- FieldId = 2, FieldX = 119, FieldY = 119,
HqOriginX = 120, HqOriginY = 120,
- BuilderX = 113, BuilderY = 119,
+ BuilderX = 111, BuilderY = 117,
};
///
/// Byte-exact mirror of Determinism10000Scenario.SetupMatch (D-077):
- /// per slot one Aetherium field, a completed HQ and ONE Builder —
- /// nothing else. Spawn ORDER is load-bearing: EntityManager hands
- /// out ids from a deterministic free list, so any reordering shifts
- /// every id and therefore every hash. Units spawn through SpawnUnit's
- /// defaults (maxHealth 100 for all), exactly like the scenario —
- /// NOT through SimDefinitions.
+ /// five finite Aetherium fields in canonical id order, then per slot a
+ /// completed HQ and ONE Builder — nothing else. Entity spawn order is
+ /// load-bearing: EntityManager hands out ids from a deterministic free
+ /// list, so any reordering shifts every id and therefore every hash.
+ /// Units spawn through SpawnUnit's defaults (maxHealth 100 for all),
+ /// exactly like the scenario — NOT through SimDefinitions.
///
private static void ApplyOpeningPosition(ReferenceHost host)
{
// The slot factions are already bound: BuildReferenceHost mirrors
// BuildHost, which assigns them before Kernel.Start() (the
// SetSlotFaction guard requires it).
+ for (int i = 0; i < FieldLayouts.Length; i++)
+ {
+ FieldLayout field = FieldLayouts[i];
+ Assert.That(host.Economy.TryAddField(
+ field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE),
+ Is.True, "reference field registration");
+ }
+
for (byte slot = 0; slot < 2; slot++)
{
SlotLayout c = slot == 0 ? Slot0Layout : Slot1Layout;
-
- Assert.That(host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE),
- Is.True, "reference field registration");
Assert.That(host.Construction.PlaceCompletedBuilding(slot, slot == 0 ? DefHQAlliance : DefHQLegion, c.HqOriginX, c.HqOriginY).IsValid,
Is.True, "reference HQ placement");
@@ -276,6 +291,23 @@ public void ReferenceOpeningPosition_MatchesDeterminism10000ScenarioSetupMatch()
"drift here means the Unity host and the headless harness are no longer the same match.");
}
+ [Test]
+ public void ReferenceOpeningPosition_RegistersFiveFiniteFields()
+ {
+ ReferenceHost host = BuildReferenceHost(CanonicalSeed);
+ ApplyOpeningPosition(host);
+
+ Assert.That(host.Economy.FieldCount, Is.EqualTo(FieldLayouts.Length));
+ for (int i = 0; i < FieldLayouts.Length; i++)
+ {
+ FieldLayout expected = FieldLayouts[i];
+ Assert.That(host.Economy.TryGetField(expected.Id, out AetheriumField actual), Is.True);
+ Assert.That(actual.GridPos.X, Is.EqualTo(expected.X));
+ Assert.That(actual.GridPos.Y, Is.EqualTo(expected.Y));
+ Assert.That(actual.RemainingAE, Is.EqualTo(expected.ReserveAE));
+ }
+ }
+
[Test]
public void ReferenceOpeningPosition_IsSeedSensitive()
{
diff --git a/tools/Nova.SimRunner/Determinism10000Scenario.cs b/tools/Nova.SimRunner/Determinism10000Scenario.cs
index cbaf834..a8fbdf7 100644
--- a/tools/Nova.SimRunner/Determinism10000Scenario.cs
+++ b/tools/Nova.SimRunner/Determinism10000Scenario.cs
@@ -136,7 +136,8 @@ internal sealed class DeterminismComparison
/// MS-1 manifest start state of quality/content/mvp-v1.json
/// (startStatePerPlayer) per slot — a COMPLETED HQ, ONE Builder and
/// 3.000 AE (EconomySystem.CanonicalMatchStartingCreditsAE, wired by
- /// ) — plus one Aetherium field per slot. Nothing
+ /// ) — plus five finite canonical Aetherium fields.
+ /// Nothing
/// else is spawned: the script then drives the opening exactly like a
/// player, for BOTH slots — walk the Builder to the future site, place
/// the Refinery once it is affordable and the committed grid covers its
@@ -184,7 +185,30 @@ internal static class Determinism10000Scenario
private const byte HumanSlot = 0;
private const byte AiSlot = 1;
private const int EntityCapacity = 1024;
- private const long FieldReserveAE = 2000000L;
+ /// One Aetherium field of the canonical map (16.7, C1).
+ private struct FieldLayout
+ {
+ public ushort Id;
+ public int X, Y;
+ public long ReserveAE;
+ }
+
+ ///
+ /// The five canonical fields (16.7, C1 — MVPContentManifest section 5):
+ /// two start fields and two natural expansions at 9.000 AE each, one
+ /// contested centre at 15.000. Every slot-1 coordinate is the point
+ /// mirror of slot 0 through the map centre ((x, y) ->
+ /// (124 - x, 124 - y)). Registration in ascending id order is part of
+ /// the canonical initial state.
+ ///
+ private static readonly FieldLayout[] FieldLayouts =
+ {
+ new FieldLayout { Id = 1, X = 7, Y = 7, ReserveAE = 9000L },
+ new FieldLayout { Id = 2, X = 117, Y = 117, ReserveAE = 9000L },
+ new FieldLayout { Id = 3, X = 24, Y = 40, ReserveAE = 9000L },
+ new FieldLayout { Id = 4, X = 100, Y = 84, ReserveAE = 9000L },
+ new FieldLayout { Id = 5, X = 62, Y = 62, ReserveAE = 15000L },
+ };
///
/// Faction-resolved definition id of a role for the given slot
@@ -601,25 +625,26 @@ private static void IssueSlotCommands(
RefineryRallyX = 7, RefineryRallyY = 6,
};
- /// Slot 1 base layout (top-right), the 180-degree mirror of slot 0.
+ /// Slot 1 base layout (top-right), the exact centre mirror of slot 0.
private static readonly SlotLayout Slot1Layout = new SlotLayout
{
- FieldId = 2, FieldX = 119, FieldY = 119,
+ FieldId = 2, FieldX = 117, FieldY = 117,
HqOriginX = 120, HqOriginY = 120,
- BuilderSpawnX = 113, BuilderSpawnY = 119,
- RefineryOriginX = 116, RefineryOriginY = 120, RefineryBuildX = 116, RefineryBuildY = 119,
- RefineryRallyX = 119, RefineryRallyY = 120,
+ BuilderSpawnX = 111, BuilderSpawnY = 117,
+ RefineryOriginX = 114, RefineryOriginY = 118, RefineryBuildX = 114, RefineryBuildY = 117,
+ RefineryRallyX = 117, RefineryRallyY = 118,
};
///
- /// Applies the deterministic match setup to a fresh host: per slot
- /// the D-077 start state of quality/content/mvp-v1.json
+ /// Applies the deterministic match setup to a fresh host: the five
+ /// canonical fields in ascending id order, then per slot the D-077
+ /// start state of quality/content/mvp-v1.json
/// (startStatePerPlayer) — a COMPLETED HQ, ONE Builder and the 3.000
/// AE of
- /// (wired by ) — plus one Aetherium field. No
+ /// (wired by ). No
/// pre-placed Refinery, no Harvesters, no skirmish squad: the loop
- /// start is scripted, not spawned. Deterministic spawn order (field,
- /// HQ, Builder; slot 0 first) means identical entity ids on every
+ /// start is scripted, not spawned. Deterministic entity order (HQ,
+ /// Builder; slot 0 first) means identical entity ids on every
/// host and platform. The slot factions are already bound —
/// assigns them before Kernel.Start(),
/// which the guard
@@ -628,13 +653,17 @@ private static void IssueSlotCommands(
private static SlotState[] SetupMatch(Host host)
{
var slots = new[] { new SlotState(), new SlotState() };
- for (byte slot = 0; slot < 2; slot++)
+ for (int f = 0; f < FieldLayouts.Length; f++)
{
- SlotLayout c = slot == HumanSlot ? Slot0Layout : Slot1Layout;
- if (!host.Economy.TryAddField(c.FieldId, new GridPos2D(c.FieldX, c.FieldY), FieldReserveAE))
+ FieldLayout field = FieldLayouts[f];
+ if (!host.Economy.TryAddField(field.Id, new GridPos2D(field.X, field.Y), field.ReserveAE))
{
- throw new InvalidOperationException($"field {c.FieldId} could not be registered");
+ throw new InvalidOperationException($"field {field.Id} could not be registered");
}
+ }
+ for (byte slot = 0; slot < 2; slot++)
+ {
+ SlotLayout c = slot == HumanSlot ? Slot0Layout : Slot1Layout;
if (!host.Construction.PlaceCompletedBuilding(slot, DefId(host, slot, UnitRole.HQ), c.HqOriginX, c.HqOriginY).IsValid)
{
throw new InvalidOperationException("HQ placement failed");