diff --git a/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs b/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
index e26567fd..14fa6f67 100644
--- a/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
+++ b/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
@@ -298,7 +298,7 @@ public void PowerFormatters_NameBalanceConsequenceAndBuildingDraw()
{
Assert.AreEqual("Strom 30/20", CommandCardPresenter.FormatPowerBalance(30, 20));
Assert.AreEqual(
- "Strom 20/40 · LOW POWER: Produktion ½",
+ "Strom 20/40 · LOW POWER: Produktion ½ · Türme aus",
CommandCardPresenter.FormatPowerBalance(20, 40));
Assert.IsTrue(SimDefinitions.TryGetBuilding(
diff --git a/Assets/Tests/EditMode/Gameplay/ResourceBarPresenterTests.cs b/Assets/Tests/EditMode/Gameplay/ResourceBarPresenterTests.cs
index 50c073cb..cf7cff58 100644
--- a/Assets/Tests/EditMode/Gameplay/ResourceBarPresenterTests.cs
+++ b/Assets/Tests/EditMode/Gameplay/ResourceBarPresenterTests.cs
@@ -186,7 +186,7 @@ public void BuildModel_PowerDeficit_WarnsCriticalAndNamesTheConsequences()
Assert.IsTrue(model.IsLowPower);
Assert.AreEqual("Strom 60/80", model.PowerText);
- Assert.AreEqual("Strommangel — Produktion ½ · Reparatur ½ · Radar aus", model.WarningText);
+ Assert.AreEqual("Strommangel — Produktion ½ · Reparatur ½ · Radar aus · Türme aus", model.WarningText);
Assert.IsTrue(model.IsCritical);
}
@@ -196,7 +196,7 @@ public void BuildModel_FullStoreAndDeficit_JoinsBothWarningsStorageFirst()
ResourceBarModel model = ResourceBarPresenter.BuildModel(3000, 3000, 60, 80);
Assert.AreEqual(
- "Lager voll — Einnahmen verfallen | Strommangel — Produktion ½ · Reparatur ½ · Radar aus",
+ "Lager voll — Einnahmen verfallen | Strommangel — Produktion ½ · Reparatur ½ · Radar aus · Türme aus",
model.WarningText);
Assert.IsTrue(model.IsCritical, "the deficit is an active penalty — the join escalates to red");
}
diff --git a/Assets/Tests/EditMode/Simulation/CombatSystemTests.cs b/Assets/Tests/EditMode/Simulation/CombatSystemTests.cs
index 4e0557bb..7d03f95c 100644
--- a/Assets/Tests/EditMode/Simulation/CombatSystemTests.cs
+++ b/Assets/Tests/EditMode/Simulation/CombatSystemTests.cs
@@ -244,6 +244,102 @@ public void AutoAcquire_DefensePlatform_FiresOnItsOwn()
Assert.That(HealthOf(host, attacker), Is.LessThan(200), "the platform actually fires");
}
+ // ------------------------------------------------------------------
+ // #67, D-122: defence platforms go offline in power deficit
+ // ------------------------------------------------------------------
+
+ [Test]
+ public void LowPower_DefensePlatform_StaysSilent_AndResumesWhenPowerReturns()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId platform = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(platform).Role = UnitRole.DefensePlatform; // armed: 20 dmg, range 10, cd 10
+ EntityId target = SpawnAt(host, 1, 13, 10, maxHealth: 1000);
+
+ // The economy system is not registered in this host, so nothing
+ // recomputes the balance: setting it directly is authoritative.
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 30;
+ eco.PowerRequired = 40; // deficit
+ Assert.That(host.Economy.GetPlayerEconomy(0).IsLowPower, Is.True);
+
+ host.Step(30);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget.IsValid, Is.False,
+ "offline: no auto-acquisition while the owner is in power deficit");
+ Assert.That(HealthOf(host, target), Is.EqualTo(1000), "offline: no shot");
+ Assert.That(host.Entities.GetUnitRef(platform).WeaponCooldownTicks, Is.EqualTo(0),
+ "the cooldown keeps running down while offline");
+
+ host.Economy.GetPlayerEconomy(0).PowerProvided = 100; // balance restored
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(target),
+ "power back: the platform acquires again in the very next tick");
+ Assert.That(HealthOf(host, target), Is.EqualTo(1000 - 20),
+ "power back: it fires at once, no warm-up (Economy.md: restores all systems immediately)");
+ }
+
+ [Test]
+ public void LowPower_DoesNotSilenceUnits_OnlyBuildings()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId unit = SpawnAt(host, 0, 10, 10, maxHealth: 100); // generic armed unit role
+ EntityId target = SpawnAt(host, 1, 13, 10, maxHealth: 1000);
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 0;
+ eco.PowerRequired = 50;
+
+ host.Step(3);
+ Assert.That(host.Entities.GetUnitRef(unit).AttackTarget, Is.EqualTo(target),
+ "a mobile unit is not a power consumer and still acquires");
+ Assert.That(HealthOf(host, target), Is.LessThan(1000),
+ "units keep firing in a power deficit; only building roles fall");
+ }
+
+ [Test]
+ public void LowPower_OnlyTheDeficitOwnersPlatformFalls()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId mine = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(mine).Role = UnitRole.DefensePlatform;
+ EntityId theirs = SpawnAt(host, 1, 13, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(theirs).Role = UnitRole.DefensePlatform;
+ host.Economy.GetPlayerEconomy(0).PowerRequired = 10; // slot 0 in deficit, slot 1 balanced
+
+ host.Step(3);
+ Assert.That(HealthOf(host, mine), Is.LessThan(600),
+ "the balanced owner's platform shoots the deficit owner's platform");
+ Assert.That(HealthOf(host, theirs), Is.EqualTo(600),
+ "the deficit owner's platform is offline");
+ }
+
+ [Test]
+ public void LowPower_OutageDropsTheHeldTarget_SoRecoveryReacquiresTheNearest()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId platform = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(platform).Role = UnitRole.DefensePlatform;
+ EntityId first = SpawnAt(host, 1, 13, 10, maxHealth: 5000);
+ host.Step(2);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(first),
+ "precondition: the platform holds the first hostile");
+
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 0;
+ eco.PowerRequired = 10; // outage
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget.IsValid, Is.False,
+ "an outage drops the held target (failure behaviour F, D-122)");
+
+ Teleport(host, first, 40, 10); // the old target walks off
+ EntityId second = SpawnAt(host, 1, 12, 10, maxHealth: 5000);
+ host.Economy.GetPlayerEconomy(0).PowerRequired = 0; // recovery
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(second),
+ "recovery acquires the nearest hostile instead of resuming a stale lock");
+ host.Step(12);
+ Assert.That(HealthOf(host, second), Is.LessThan(5000));
+ }
+
[Test]
public void ActiveDefensePlatformSite_NeitherAcquiresNorExecutesExplicitAttack()
{
diff --git a/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs b/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
index 0342d6c7..fdf4532e 100644
--- a/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
+++ b/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
@@ -362,13 +362,15 @@ public static BuildingPlacementBlocker EvaluateBuildingPlacementBlocker(
///
/// Compact live grid balance for the build bar. Low power must name
- /// its gameplay consequence where the player makes build decisions.
+ /// its gameplay consequence where the player makes build decisions —
+ /// including that the defence platforms go offline (#67, D-122), so a
+ /// deficit caused by one's own build-out is not a silent trap.
///
public static string FormatPowerBalance(int powerProvided, int powerRequired)
{
string balance = $"Strom {powerProvided}/{powerRequired}";
return powerRequired > powerProvided
- ? balance + " · LOW POWER: Produktion ½"
+ ? balance + " · LOW POWER: Produktion ½ · Türme aus"
: balance;
}
diff --git a/Assets/_Project/Scripts/Gameplay/UI/ResourceBarPresenter.cs b/Assets/_Project/Scripts/Gameplay/UI/ResourceBarPresenter.cs
index 84aedef4..5c81dced 100644
--- a/Assets/_Project/Scripts/Gameplay/UI/ResourceBarPresenter.cs
+++ b/Assets/_Project/Scripts/Gameplay/UI/ResourceBarPresenter.cs
@@ -99,8 +99,8 @@ public static class ResourceBarPresenter
/// Warning while the balance sits ABOVE the ceiling: the excess decays 25%/s (D-024/D-106), so the warning names the action (red).
public const string StorageOverflowWarning = "Überschuss verfällt — Lager bauen!";
- /// Warning under low power, naming the three live consequences so the rule stops being invisible (16.6 C4, Sprint-16 production factor).
- public const string LowPowerWarning = "Strommangel — Produktion ½ · Reparatur ½ · Radar aus";
+ /// Warning under low power, naming the four live consequences so the rule stops being invisible (16.6 C4, Sprint-16 production factor; "Türme aus" since #67, D-122).
+ public const string LowPowerWarning = "Strommangel — Produktion ½ · Reparatur ½ · Radar aus · Türme aus";
///
/// The storage situation, with the two edges the account can sit on:
diff --git a/Assets/_Project/Scripts/Simulation/Combat/CombatSystem.cs b/Assets/_Project/Scripts/Simulation/Combat/CombatSystem.cs
index 1c2604b4..b9c1c354 100644
--- a/Assets/_Project/Scripts/Simulation/Combat/CombatSystem.cs
+++ b/Assets/_Project/Scripts/Simulation/Combat/CombatSystem.cs
@@ -1,6 +1,7 @@
using System;
using Nova.Core;
using Nova.Simulation.Construction;
+using Nova.Simulation.Economy;
using Nova.Simulation.State;
using Nova.Simulation.Vision;
@@ -97,6 +98,24 @@ namespace Nova.Simulation.Combat
/// self-computed sight exists on this path.
///
///
+ /// LOW-POWER OFFLINE RULE (#67, D-122; docs/gamedesign/Economy.md): a
+ /// BUILDING-role entity (
+ /// — in MS-1 that is the DefensePlatform, the only armed building) whose
+ /// owner is in power deficit ()
+ /// is OFFLINE: it neither acquires a target (phase 2) nor holds one — its
+ /// attack order is DROPPED (phase 3) so that, once power returns, the next
+ /// acquisition re-picks the nearest hostile instead of resuming an old
+ /// lock. Units are never affected. The weapon cooldown keeps ticking down
+ /// in phase 1 on purpose: a platform whose power returns fires again
+ /// immediately ("restores all systems at once", Economy.md). The balance
+ /// is read from the economy state as it stands at the START of the combat
+ /// phase — the Economy system is registered (and therefore runs) before
+ /// Combat, so a power plant destroyed in tick N takes the platforms
+ /// offline from tick N+1 on: one tick of latency, fully deterministic.
+ /// The power balance is derived state, so the rule adds nothing to
+ /// snapshots or state layout.
+ ///
+ ///
/// Per-unit-type weapons from docs/gamedesign/Weapons.md ARE wired now —
/// the flat 15-damage-for-everyone placeholder is gone. What remains
/// provisional: same-team attack orders are not filtered in this slice
@@ -129,27 +148,30 @@ public sealed class CombatSystem : ISimSystem
private readonly EntityManager _entityManager;
private readonly FogOfWarSystem _fogOfWar;
- private readonly ISlotFactionLookup _factions;
+ private readonly EconomySystem _economy;
private readonly ConstructionSystem _construction;
public string Name => "CombatSystem";
///
- /// Combat needs the slot factions because weapon profiles are
- /// faction-resolved (faction identity slice): the same role fights
- /// with different numbers depending on the owner slot's faction.
- /// The lookup is the economy state — the single home of the faction
- /// assignment — injected at construction like the FoW reference.
+ /// Combat needs the economy for two reads: the slot factions, because
+ /// weapon profiles are faction-resolved (faction identity slice — the
+ /// same role fights with different numbers depending on the owner
+ /// slot's faction), and the owner's power balance, because defence
+ /// platforms go offline in power deficit (#67, D-122). The economy
+ /// state is the single home of both — injected at construction like
+ /// the FoW reference (same pattern as Fog of War and Construction)
+ /// and registered BEFORE combat so the balance is current.
///
public CombatSystem(
EntityManager entityManager,
FogOfWarSystem fogOfWar,
- ISlotFactionLookup factions,
+ EconomySystem economy,
ConstructionSystem construction)
{
_entityManager = entityManager ?? throw new ArgumentNullException(nameof(entityManager));
_fogOfWar = fogOfWar ?? throw new ArgumentNullException(nameof(fogOfWar));
- _factions = factions ?? throw new ArgumentNullException(nameof(factions));
+ _economy = economy ?? throw new ArgumentNullException(nameof(economy));
_construction = construction ?? throw new ArgumentNullException(nameof(construction));
}
@@ -191,8 +213,9 @@ public void ExecuteTick(Tick tick)
if (!attacker.IsActive || attacker.AttackTarget.IsValid
|| _construction.IsActiveSite(attacker.Id)) continue;
- WeaponProfile weapon = WeaponProfiles.Get(_factions.GetSlotFaction(attacker.PlayerId), attacker.Role);
+ WeaponProfile weapon = WeaponProfiles.Get(_economy.GetSlotFaction(attacker.PlayerId), attacker.Role);
if (!weapon.IsArmed) continue;
+ if (IsOffline(in attacker)) continue; // #67, D-122: no acquisition in power deficit
if (attacker.PlayerId >= _fogOfWar.TeamCount) continue;
TeamView view = _fogOfWar.GetTeamView(attacker.PlayerId);
@@ -260,9 +283,19 @@ public void ExecuteTick(Tick tick)
// range and cadence. An unarmed role (base damage 0) holds
// its order exactly like an out-of-range one — it simply has
// nothing to fire, so it never starts a cooldown either.
- WeaponProfile weapon = WeaponProfiles.Get(_factions.GetSlotFaction(attacker.PlayerId), attacker.Role);
+ WeaponProfile weapon = WeaponProfiles.Get(_economy.GetSlotFaction(attacker.PlayerId), attacker.Role);
if (!weapon.IsArmed) continue;
+ // #67, D-122: an offline platform neither fires nor holds its
+ // target. Dropping the order (instead of keeping it) means the
+ // recovery re-acquires the nearest hostile in phase 2. The
+ // cooldown is untouched — phase 1 keeps counting it down.
+ if (IsOffline(in attacker))
+ {
+ attacker.AttackTarget = EntityId.Invalid;
+ continue;
+ }
+
// Legality: range AND committed visibility. A living target
// that fails either check is held, never dropped.
if (!IsInRange(in attacker, in target, weapon.AttackRange)) continue;
@@ -277,7 +310,7 @@ public void ExecuteTick(Tick tick)
int damage = DamageMatrix.Resolve(
weapon.AttackDamage,
weapon.DamageType,
- WeaponProfiles.GetArmorClass(_factions.GetSlotFaction(target.PlayerId), target.Role));
+ WeaponProfiles.GetArmorClass(_economy.GetSlotFaction(target.PlayerId), target.Role));
target.CurrentHealth -= damage;
attacker.WeaponCooldownTicks = weapon.AttackCooldownTicks;
@@ -293,6 +326,18 @@ public void Shutdown()
{
}
+ ///
+ /// Low-power offline rule (#67, D-122): true for a building-role
+ /// attacker whose owner slot is currently in power deficit. Units are
+ /// never offline. Reads the balance the Economy system left at the
+ /// start of this tick (one tick of latency, deterministic).
+ ///
+ private bool IsOffline(in UnitState attacker)
+ {
+ return Definitions.SimDefinitions.IsBuildingRole(attacker.Role)
+ && _economy.GetPlayerEconomy(attacker.PlayerId).IsLowPower;
+ }
+
///
/// Exact range test (boundary inclusive): center distance squared
/// <= (range + target radius)^2, computed in widened Q32.32 long
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3af38f47..9a3cf62c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -258,6 +258,18 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
sie belegt keine Verbesserung.
### Geändert
+- **Verteidigungsplattformen fallen bei Strommangel aus (#67, D-122).** Ein
+ Besitzer im Defizit (`PlayerEconomyState.IsLowPower`) verliert die Feuerkraft
+ seiner Gebäuderollen: `CombatSystem` erfasst keine Ziele, feuert nicht und
+ verwirft ein gehaltenes Ziel; der Cooldown läuft weiter, bei Rückkehr des
+ Stroms feuert die Plattform sofort wieder. Einheiten sind nie betroffen; die
+ Bilanz gilt vom Tickanfang (ein Tick Latenz). Dritter Konstruktorparameter von
+ `CombatSystem` ist jetzt das `EconomySystem` (alle Aufrufer übergaben es
+ schon). Ressourcenleiste und Baubar nennen „Türme aus“; Buildings.md,
+ Economy.md und DemoRunbook.md nachgezogen. **Keine Baseline und kein Pin
+ bewegt sich, `RulesHash64` ebenfalls nicht** – ein weiterer Beleg für die
+ Hash-Lücke aus #108 (Reihenfolge zur Regelrevision V4 offen, siehe D-122).
+ Die Unity-EditMode-Tests (Spiegel und UI-Texte) sind nicht gelaufen.
- **Die lebende Dokumentation heißt jetzt Hashkrieg (#14, Stufe 3).** 54 Dateien,
84 Stellen — die Marke in der Prosa unter `docs/**` und in den Wurzeldateien.
Zwei Regeln haben den Umfang bestimmt. Erstens **E-3: nur die Marke.** Die
diff --git a/docs/gamedesign/Buildings.md b/docs/gamedesign/Buildings.md
index 67d51b96..78437780 100644
--- a/docs/gamedesign/Buildings.md
+++ b/docs/gamedesign/Buildings.md
@@ -1,6 +1,6 @@
# Gebäude – alle Fraktionen
-**Version:** 0.8.1 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
+**Version:** 0.9.0 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
## Zweck
@@ -71,7 +71,7 @@ Bei Widerspruch übersteuert dieser Abschnitt den nachfolgenden Vollspielentwurf
- **Führende Quelle für Gebäudewerte (Review F-03, Grundsatzregel D-047):** Dieses Dokument ist die alleinige Quelle für Gebäudekosten, Energiewerte (Erzeugung/Verbrauch) und Bauzeiten. [./Economy.md](./Economy.md) verweist hierher und enthält nur Systemlogik (Einkommensraten, Low-Power-Regel, Lager-Kapazität); doppelte Zahlenpflege ist unzulässig.
- **12 Typen pro Fraktion, identische Rollen, eigene Namen und Werte-Deltas** (D-008, D-011). Die Rolle ist fraktionsübergreifend balancierbar; Kosten/TP/Bauzeit tragen die Fraktionsidentität (Allianz teuer/präzise, Legion günstig/massig, Evolvierte regenerativ/wachsend).
-- **Energiebilanz:** Kraftwerke produzieren, fast alles andere verbraucht. Im ausgelieferten MS-1 halbiert ein Defizit Produktion, Bau und Reparatur; Radar und Minimap fallen aus. Forschung, Superwaffen-Ladung und stromabhängige Verteidigungsplattformen bleiben Ziele des nachfolgenden Vollspielentwurfs und sind im aktuellen Combat-Pfad nicht als Low-Power-Effekt implementiert.
+- **Energiebilanz:** Kraftwerke produzieren, fast alles andere verbraucht. Im ausgelieferten MS-1 halbiert ein Defizit Produktion, Bau und Reparatur; Radar und Minimap fallen aus, und die Verteidigungsplattformen des betroffenen Besitzers schalten sich ab (#67, D-122: kein Feuer, kein gehaltenes Ziel; Einheiten sind nie betroffen). Forschung und Superwaffen-Ladung bleiben Ziele des nachfolgenden Vollspielentwurfs und sind nicht als Low-Power-Effekt implementiert.
- **Lagerkapazität (D-024/D-096/D-106):** Genau eine HQ-Basiskapazität von 2.000 AE je Konto, sobald mindestens ein fertiges HQ lebt; +2.000 AE je fertigem Lager. Neue Einzahlungen werden hart gedeckelt. Vorhandener Überhang verliert unabhängig von seiner Ursache einmal pro Sekunde 25 % des aktuellen Überhangs (Abrundung, mindestens 1 AE) bis zur Grenze.
- **Eroberung (D-022):** Feindliche Gebäude sind eroberbar – eine Capture-Einheit kanalisiert 5 s am Ziel (Abbruch bei Schaden) und wird bei Erfolg verbraucht. Capture-Einheiten: Engineer (Allianz), Saboteur (Legion), Tunnelgräber (Evolvierte). Regelwerk und Details in [./Infantry.md](./Infantry.md) und [./NeutralUnits.md](./NeutralUnits.md); gilt ebenso für neutrale Geschütztürme (D-016).
- **Trefferpunkt-Klassen:** Leicht (400–800 TP), Mittel (900–1.500 TP), Schwer (1.600–2.500 TP). Klasse statt Pseudo-Präzision; exakte Werte entstehen im Balancing-Pass.
@@ -192,7 +192,7 @@ Voraussetzungsschlüssel: HQ = Hauptquartier, KW = Kraftwerk, Raff = Raffinerie,
| Legion | Geschützstellung | 300 AE | 10 s | −8 | Mittel |
| Evolvierte | Rankenturm | 350 AE | 11 s (Reifung) | −8 | Mittel |
-- Rolle: stationäre Verteidigung; Basis ist ein unbewaffnetes Podest, Bewaffnung über Module (§3). „Offline bei Low Power“ bleibt Vollspielziel; der aktuelle MS-1-Combat-Pfad schaltet die Plattform bei Strommangel noch nicht ab.
+- Rolle: stationäre Verteidigung; Basis ist ein unbewaffnetes Podest, Bewaffnung über Module (§3). **Offline bei Low Power (#67, D-122):** Solange der Besitzer im Strommangel ist (Verbrauch > Erzeugung), erfasst die Plattform keine Ziele, feuert nicht und verwirft ein gehaltenes Ziel; der Waffen-Cooldown läuft weiter, sodass sie bei Rückkehr des Stroms sofort wieder feuert und das nächstgelegene Ziel neu erfasst. Die Bilanz gilt vom Tickanfang (ein Tick Latenz). Eine offline Plattform verbraucht weiter Strom; laufende Baustellen reservieren dagegen keinen (Platzierungsprüfung liest nur die fertige Bilanz), sodass mehrere kurz nacheinander gesetzte Stromverbraucher das Defizit auslösen können, wenn sie fertig werden.
- **Aggressions-Modi (D-031.6):** Die Plattform nutzt dieselben drei Aggressions-Modi wie Einheiten – **Halten / Abwehren (Standard) / Freies Feuer** –, umschaltbar über die Befehlskachel der Auswahl; Logik identisch zu [../vision/CoreGameplay.md](../vision/CoreGameplay.md) (Abschnitt Aggressions-Modi). Der Modus gilt unverändert für alle Modul-Stufen (MG/Flak/Rakete).
- Voraussetzung: Kaserne (MG-Modul), Radar (Flak-Modul), FL (Raketen-Modul).
@@ -328,3 +328,4 @@ Entschieden und entfernt im Korrekturlauf Sprint 4: HQ-Grundenergie (+30 führen
| 0.7.0 | 2026-08-10 | Die neun fraktionsgleichen All-of-Platzierungsvoraussetzungen samt Trennung von Plattformbasis und Modulen gemäß D-103 festgeschrieben | Agent (unter Delegation) / Dennis Westermann |
| 0.8.0 | 2026-08-10 | D-104-MS-1-Override ergänzt: einheitliche 3×3-Footprints, footprintbasierte Einfluss-, Gelände-, Gebäude- und Feldabstände sowie kumulative 30-%-Reparaturkosten mit 10/5 TP pro Tick; nicht implementierte Vollspielziele klar abgegrenzt | Agent (unter Delegation) / Dennis Westermann |
| 0.8.1 | 2026-08-10 | Paket 16.10 nachgezogen: der Bauknopf erklärt All-of-Voraussetzungen, AE, Energie und Baustellenlimit; Energie bleibt informativ und blockiert den Platzierungsmodus nicht | Codex / Dennis Westermann |
+| 0.9.0 | 2026-10-17 | #67/D-122 (Entwurf): Verteidigungsplattformen fallen bei Strommangel aus (Grundprinzip „Energiebilanz“ und Abschnitt Verteidigungsplattform berichtigt; Vollspielziel nur noch für Forschung und Superwaffen) | Agent (Issue-Lauf 2026-10-17, unter Delegation) |
diff --git a/docs/gamedesign/Economy.md b/docs/gamedesign/Economy.md
index 23f2443d..3c08ce0d 100644
--- a/docs/gamedesign/Economy.md
+++ b/docs/gamedesign/Economy.md
@@ -1,6 +1,6 @@
# Wirtschaftssystem (Economy)
-**Version:** 0.6.0 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
+**Version:** 0.7.0 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
## Zweck
@@ -70,6 +70,8 @@ Harvester sind unbewaffnet (Allianz/Legion) und haben mittlere Panzerung; Evolvi
Reihenfolge ist fest: Radar und Verteidigung fallen **immer** zuerst, Produktion wird pauschal halbiert – keine spielerseitige Priorisierung im MVP (UI-/Balancing-Aufwand, siehe Offene Punkte). Rückkehr zu positivem Saldo stellt alle Systeme sofort wieder her.
+**MS-1-Stand der Low-Power-Regel (#67, D-122):** Umgesetzt sind Radar offline, Produktions-, Bau- und Reparaturgeschwindigkeit −50 % sowie **Verteidigungsplattformen offline**. Eine Plattform im Besitz eines Spielers mit Defizit erfasst keine Ziele, feuert nicht und verwirft ihr gehaltenes Ziel; ihr Cooldown läuft weiter, bei Rückkehr des Stroms feuert sie sofort wieder und erfasst das nächstgelegene Ziel neu. Einheiten sind nie betroffen. Die Bilanz wird zu Beginn des Combat-Schritts gelesen (ein Tick Latenz). Forschung (−50 %, D-030) und der Superwaffen-Countdown bleiben Vollspielziel. **Spielerfalle:** laufende Baustellen reservieren keinen Strom; wer mehrere Stromverbraucher (z. B. Plattformen) knapp nacheinander setzt, kann das Defizit mit deren Fertigstellung selbst auslösen und damit die gesamte Verteidigung abschalten. Die UI nennt die Folgen daher an Ressourcenleiste und Baubar („Türme aus“).
+
## Einkommensraten-Ziele (AE/min, Richtwerte)
| Expansionsstufe | Setup | Ziel-Einkommen |
@@ -166,3 +168,4 @@ Leitplanke: Gesamt-AE-Fluss pro Spieler über ein typisches 25-min-Match ≈ 25.
| 0.4.0 | 2026-07-21 | F-03 vollständig geschlossen: Harvester-Kosten in der Fraktions-Wirtschaftsmodifier-Tabelle durch Verweis auf die führende Quelle [Vehicles.md](./Vehicles.md) ersetzt (D-047) – keine dritte Zahl mehr neben Vehicles.md (700/550/620 AE) | Lead Gameplay Designer |
| 0.5.0 | 2026-08-10 | MS-1-Start auf 3.000 AE (D-077) nachgezogen und D-106 präzisiert: eine HQ-Basis je Konto, +2.000 je fertigem Lager, harte Einzahlungskappung sowie zustandsloser 25-%-Abbau des aktuellen Überhangs pro Sekunde | Codex / Dennis Westermann |
| 0.6.0 | 2026-08-10 | D-104: Reparaturkosten für MS-1 auf den implementierten kumulativen Startwert von 30 % vereinheitlicht; ganzzahlige `S(h)`-Abrechnung und Low-Power-Rate präzisiert | Project Owner / Agent (unter Delegation) |
+| 0.7.0 | 2026-10-17 | #67/D-122 (Entwurf): MS-1-Stand der Low-Power-Regel ergänzt – Verteidigungsplattformen offline (Ziel verwerfen, Cooldown läuft weiter, ein Tick Latenz), Spielerfalle durch nicht reservierte Baustellen benannt | Agent (Issue-Lauf 2026-10-17, unter Delegation) |
diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md
index c00a15ca..019e65e3 100644
--- a/docs/production/DecisionLog.md
+++ b/docs/production/DecisionLog.md
@@ -3662,6 +3662,119 @@ berührt.
Ansturm-Gegner ab. Wer ohne Menü startet (AutoStart, Harness), spielt diesen,
nicht r8.
+### D-122 | Entwurf | Verteidigungsplattformen fallen bei Strommangel aus (#67)
+
+**Status:** Entwurf aus dem Issue-Lauf vom 2026-10-17 (#67); verbindlich mit dem
+Merge des PR. Die Endnummer vergibt der Integrator beim Merge.
+
+**Kontext:** Die Low-Power-Regel in [Economy.md](../gamedesign/Economy.md)
+verlangt „Verteidigungsplattformen offline“, und Strang C hatte „Radar und
+Verteidigung fallen immer zuerst“ vorgesehen. Umgesetzt waren seit Paket 16.6
+(PR #78) nur Radar, Produktion, Bau und Reparatur. Ob eine Plattform feuert,
+entschied allein `WeaponProfile.IsArmed` in `CombatSystem`; `Simulation/Combat/`
+kannte keinen Strombegriff (null Treffer für „power“), und das Issue #67 hielt
+die Lücke fest: Ein Angriff auf das gegnerische Kraftwerk ließ die Produktion
+einbrechen, die Türme schossen weiter. `PlayerEconomyState.IsLowPower`
+existiert und wird vom `EconomySystem` jeden Tick neu berechnet; Economy läuft
+vor Combat. Alle Konstruktionsstellen von `CombatSystem` übergeben bereits ein
+konkretes `EconomySystem`. Bewaffnet ist im MS-1 nur die `DefensePlatform`.
+Zwei Teilfragen waren offen: **wie** Combat den Strom erreicht und **was**
+mit dem gehaltenen Ziel beim Ausfall geschieht.
+
+**Alternativen:**
+
+1. **Dritter Konstruktorparameter wird `EconomySystem` (statt
+ `ISlotFactionLookup`).** Combat liest `GetPlayerEconomy(owner).IsLowPower`
+ direkt. Bewertung: kleinster Eingriff (eine Quelldatei, kein Aufrufer ändert
+ sich), gleiches Muster wie `FogOfWarSystem` und `ConstructionSystem`. Die
+ schmale Schnittstelle verliert hier an Reinheit, ein zweiter Implementierer
+ existiert aber nicht.
+2. **Neue schmale Schnittstelle `ISlotPowerLookup` als fünfter Parameter.**
+ Entkoppelt Combat von der Wirtschaft, kostet aber 20 mechanische
+ Aufrufstellen in Spiel, Harness und beiden Testspuren, eine neue Datei samt
+ `.meta` und größere Konfliktfläche, ohne dass es einen zweiten
+ Implementierer gäbe. Mehraufwand ohne Nutzen.
+3. **`ISlotFactionLookup` um `IsSlotLowPower` erweitern.** Keine
+ Aufruferänderung, aber der Name lügt, und `EntityManager.TryValidateState`
+ hängt an derselben Schnittstelle (`State/` ist teilweise „niemand ohne
+ D-ID“). Unscharfe Verantwortung.
+4. **Die Wirtschaft erzwingt die Abschaltung von außen** (Cooldown oder
+ Zielfeld der Türme manipulieren). Schreibt fremden Zustand, verwischt die
+ Systemgrenze und ist im Verhalten schwer zu begründen. Verworfen.
+
+Teilentscheidung **Ausfallverhalten:** (E) Ziel behalten und nach Rückkehr des
+Stroms am alten Ziel weiterfeuern, oder (F) Ziel beim Ausfall verwerfen und nach
+der Rückkehr neu erfassen. Bewertung: E ist ein Zeilenunterschied weniger, hält
+aber den bekannten Mangel fest, dass ein Turm ein Ziel behält, das die
+Reichweite verlassen hat, und nie neu erfasst. F entschärft das nach jedem
+Ausfall nebenbei und liefert ein klares Bild: ein Neustart der Zielwahl.
+
+Teilentscheidung **Regel-Hash:** `RulesHash64` hasht nur Konstanten und die
+Revisionsnummer; die Regel bewegt das Verhalten, aber keine hashbare Konstante.
+Bewertung: eine eigene Regelrevision jetzt anzuheben berührt `Replays/`
+(„niemand ohne D-ID“) und kollidiert mit der Revision V4, die der Art-Strang
+bereits für das Bewegungsgefühl baut. Ohne Hashbewegung auszuliefern entspricht
+der Praxis bei D-108 und #55 (Lücke, dokumentiert in #108); die Low-Power-Regeln
+aus 16.6 haben dagegen die Revision angehoben.
+
+**Entscheidung:** Alternative 1 mit Ausfallverhalten F.
+
+- `CombatSystem` bekommt als dritten Konstruktorparameter das `EconomySystem`
+ (Feld und Parameter heißen jetzt `_economy`/`economy`).
+- `IsOffline(attacker)` ist wahr, wenn die Rolle des Angreifers eine
+ Gebäuderolle ist (`SimDefinitions.IsBuildingRole`) **und** der Besitzer
+ `IsLowPower` ist. Einheiten sind nie betroffen.
+- Phase 2 (Auto-Erfassung) überspringt offline Angreifer. Phase 3 (Engagement)
+ setzt deren `AttackTarget` auf ungültig und überspringt sie: kein Schuss,
+ kein gehaltenes Ziel.
+- Der Cooldown in Phase 1 läuft weiter. Kehrt der Strom zurück, feuert die
+ Plattform sofort wieder („stellt alle Systeme sofort wieder her“,
+ Economy.md) und erfasst das nächstgelegene Ziel neu.
+- Die Bilanz gilt vom Tickanfang (Economy vor Combat): ein im selben Tick
+ zerstörtes Kraftwerk wirkt ab dem Folgetick, ein Tick Latenz,
+ deterministisch.
+- **Regel-Hash:** Die Regel wird jetzt **nicht** mit eigener Revision
+ gebunden; `RulesHash64` bewegt sich nicht. Landet dieser PR vor dem
+ V4-PR des Art-Strangs auf `main`, deckt V4 die Regel implizit ab; sonst ist
+ sie in #108 als vierter Fall nachzutragen (Inhaber stimmt die Reihenfolge ab).
+- UI: Ressourcenleiste („… Radar aus · Türme aus“) und Baubar
+ („LOW POWER: Produktion ½ · Türme aus“) nennen die Folge, weil die UI die
+ Low-Power-Regel erklären muss ([TargetAudience.md](../vision/TargetAudience.md)).
+
+**Begründung:** Die Designentscheidung liegt in Economy.md bereits vor; offen war
+nur der Stromeingang. Alternative 1 ist der kleinste Eingriff mit etabliertem
+Muster und lässt jede Aufrufstelle unverändert. F kostet dieselbe Zeile wie E,
+vermeidet aber ein Weiterfeuern auf ein womöglich längst unpassendes Ziel und
+mildert den Mangel des nie neu erfassten Ziels. Cooldown-Weiterlauf und
+Tickanfangs-Bilanz halten die Regel einfach und deterministisch; der Strom ist
+abgeleiteter Zustand, daher ändern sich weder Snapshot-Format noch
+State-Layout.
+
+**Konsequenzen:**
+
+- **Hash-Lücke:** Ein Build mit und einer ohne diese Regel melden dieselbe
+ `RulesHash64`. Abgefedert durch den Build-Commit-Abgleich der Lobby und die
+ Desync-Erkennung, nicht durch den Fingerprint. Nach dem Merge ist ein neuer
+ Testbuild für alle Plattformen fällig.
+- **Offline verbraucht weiter Strom:** `RecomputePower` zählt die Plattform mit;
+ das Defizit kann sich dadurch halten. Verkaufen oder ein neues Kraftwerk
+ sind die Gegenmaßnahmen.
+- **Spielerfalle:** Laufende Baustellen reservieren keinen Strom
+ (`EconomySystem.RecomputePower` überspringt sie, die Platzierungsprüfung liest
+ nur die fertige Bilanz). Wer mehrere Stromverbraucher knapp nacheinander setzt,
+ kann das Defizit mit deren Fertigstellung selbst auslösen und schaltet dann die
+ gesamte Verteidigung ab. Hier nur durch den Warntext gemildert; die
+ Überzeichnung selbst ist ein eigener Nebenbefund (Q-040).
+- **Künftige Ausnahmen:** Eroberte neutrale Türme sollen laut
+ [NeutralUnits.md](../gamedesign/NeutralUnits.md) keine Energie brauchen. Die
+ Regel erfasste sie, sobald solche Rollen existieren; die Ausnahme ist dann
+ nachzurüsten.
+- **Ungelöst bleibt** der Mangel, dass ein Turm ein Ziel außer Reichweite hält
+ und nie neu erfasst (nur nach Stromausfällen durch F abgemildert; verwandt mit
+ #51).
+- Forschung und Superwaffen-Countdown bleiben Vollspielziel
+ ([Economy.md](../gamedesign/Economy.md)).
+
## Offene Punkte
- Alle Sprint-4-Review-Befunde (105, davon 9 kritisch): 7 entscheidungsbedürftige kritische Befunde sind durch D-043–D-052 entschieden.
diff --git a/docs/production/DemoRunbook.md b/docs/production/DemoRunbook.md
index 443092b5..a95d61e6 100644
--- a/docs/production/DemoRunbook.md
+++ b/docs/production/DemoRunbook.md
@@ -1,6 +1,6 @@
# Demo-Runbook – erste spielbare Runde (Glutrinne-Graybox)
-**Version:** 0.6.1 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16
+**Version:** 0.6.2 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16
## Zweck
@@ -140,7 +140,8 @@ steht davor und dauert so lange, wie man ihn zeigen will.
Harvester bestellen; den Kreislauf am Feld beobachten (ernten → abliefern →
Credits steigen).
5. **Ausbau (3:00):** Kaserne (Shift+B); danach ist ein Kraftwerk (B) fällig
- (LOW POWER halbiert die Produktionsgeschwindigkeit). Infanterie (Shift+Q)
+ (LOW POWER halbiert die Produktionsgeschwindigkeit und schaltet
+ Verteidigungsplattformen ab). Infanterie (Shift+Q)
zur Verteidigung — **die Legion baut in dieser Zeit ihre eigene Basis auf.**
6. **Gegenwehr (6:00):** Die erste gegnerische Angriffswelle trifft ein.
Infanterie per Box auswählen, mit A auf einen Angreifer klicken; Schaden
@@ -159,7 +160,10 @@ steht davor und dauert so lange, wie man ihn zeigen will.
Spieler (kein Konter, kein Rückzug). Ihre Peer-Session ist nicht
snapshot-serialisiert.
- **Zielerfassung bleibt schlicht:** Bewaffnete Einheiten und fertige
- Verteidigungsplattformen erfassen Ziele automatisch und erwidern Feuer. Ein
+ Verteidigungsplattformen erfassen Ziele automatisch und erwidern Feuer —
+ die Plattformen jedoch nur bei gedecktem Strom: im Strommangel (Statusleiste
+ „LOW POWER“, Warnung „Türme aus“) schweigen sie, bis die Bilanz wieder
+ stimmt (#67, D-122). Ein
A-Befehl ohne Gegner unter dem Cursor bleibt jedoch ein schlichtes Move;
Attack-Move und Zielerfassung bei der Ankunft sind nicht implementiert.
- `Stop` löscht Bewegung und das aktuelle Angriffsziel. Die automatische
@@ -280,3 +284,4 @@ der Editor in der Konsole, wenn das Schreiben fehlschlägt.
| 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 |
| 0.6.1 | 2026-08-10 | D-097/Paket 16.10 nachgezogen: Stop löscht das aktuelle Angriffsziel; automatische Neuerfassung im Folgetick und das weiterhin fehlende Halte-Feuer ehrlich abgegrenzt; die benachbarte überholte Behauptung fehlender Auto-Zielerfassung auf den aktuellen Combat-Stand korrigiert | Codex / Dennis Westermann |
+| 0.6.2 | 2026-10-17 | #67/D-122 (Entwurf): §5 Zielerfassung berichtigt – Verteidigungsplattformen feuern nur bei gedecktem Strom | Agent (Issue-Lauf 2026-10-17, unter Delegation) |
diff --git a/reports/v8.6.0/issue-lauf-2026-10-17/03-issue-67-defence-low-power.md b/reports/v8.6.0/issue-lauf-2026-10-17/03-issue-67-defence-low-power.md
new file mode 100644
index 00000000..821bd497
--- /dev/null
+++ b/reports/v8.6.0/issue-lauf-2026-10-17/03-issue-67-defence-low-power.md
@@ -0,0 +1,79 @@
+---
+issue: 67
+paket: P-67
+branch: feat/67-defence-offline-low-power
+agent: builder (sonnet)
+datum: 2026-10-17
+status: DONE
+---
+# Verteidigungsplattformen fallen bei Strommangel aus (#67)
+
+## Aufgabe
+`CombatSystem` kannte keinen Strombegriff: Türme feuerten weiter, obwohl ihr Besitzer im Strommangel war (Economy.md verlangt „Verteidigungsplattformen offline“). Umgesetzt ist Variante A mit Ausfallverhalten F (Triage-Empfehlung): Gebäuderollen im Strommangel erfassen nichts, feuern nicht und verwerfen ihr Ziel; Einheiten sind nie betroffen; der Cooldown läuft weiter.
+
+## Was geändert wurde — Files Changed
+- `Assets/_Project/Scripts/Simulation/Combat/CombatSystem.cs` — dritter Konstruktorparameter von `ISlotFactionLookup` auf `EconomySystem` (Feld/Parameter `_economy`/`economy`), `IsOffline` (Gebäuderolle UND `IsLowPower` des Besitzers), Gate in Phase 2 (kein Erfassen) und Phase 3 (Ziel verwerfen, kein Schuss), Phase 1 unverändert (Cooldown läuft weiter); Klassen- und Konstruktorkommentar nachgezogen.
+- `tools/Nova.SimRunner.Tests/CombatSystemTests.cs` — vier neue Tests (siehe unten).
+- `Assets/Tests/EditMode/Simulation/CombatSystemTests.cs` — dieselben vier Tests im EditMode-Spiegel (Datei war bis auf Namespace/Klassenkopf identisch).
+- `Assets/_Project/Scripts/Gameplay/UI/ResourceBarPresenter.cs` — `LowPowerWarning` um „· Türme aus“ ergänzt (TargetAudience.md:64: UI muss die Regel erklären).
+- `Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs` — `FormatPowerBalance` hängt bei Defizit „· Türme aus“ an (Baubar = Ort der Bauentscheidung, Spielerfalle durch nicht reservierte Baustellen).
+- `Assets/Tests/EditMode/Gameplay/ResourceBarPresenterTests.cs` — zwei erwartete Zeichenketten angepasst.
+- `Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs` — erwartete Zeichenkette angepasst.
+- `docs/gamedesign/Buildings.md` — Grundprinzip „Energiebilanz“ und Abschnitt Verteidigungsplattform berichtigt (Plattform fällt aus); Version 0.9.0 + Verlaufszeile.
+- `docs/gamedesign/Economy.md` — MS-1-Stand der Low-Power-Regel (Plattformen offline, Zielverwurf, Cooldown, ein Tick Latenz, Spielerfalle); Version 0.7.0 + Verlaufszeile.
+- `docs/production/DemoRunbook.md` — §4 Schritt 5 und §5 „Zielerfassung“ berichtigt (Plattformen nur bei gedecktem Strom); Version 0.6.2 + Verlaufszeile.
+- `docs/production/DecisionLog.md` — Entwurf D-122 vor „## Offene Punkte“ (Kopfversion/Verlauf nicht angefasst).
+- `CHANGELOG.md` — ein Eintrag unter `[Unreleased]` / „Geändert“ (#67).
+- `reports/v8.6.0/issue-lauf-2026-10-17/03-issue-67-defence-low-power.md` — dieser Bericht (gitignored).
+
+Nicht angefasst: die vier Guard-Dateien, `docs/README.md`, `ScopeLedger.md`, alle Aufrufer von `CombatSystem` (alle 20 Konstruktionsstellen — 16x `new CombatSystem(` plus 4x `new Nova.Simulation.Combat.CombatSystem(` in Assets/ und tools/ — übergeben bereits ein konkretes `EconomySystem`; mit `git grep` geprüft, auch `Determinism10000Scenario.cs` und `Program.cs`).
+
+## Tests und Prüfungen
+Neue Tests (beide Lanes): `LowPower_DefensePlatform_StaysSilent_AndResumesWhenPowerReturns` (30 Ticks Stille, Cooldown läuft auf 0, nach Rückkehr feuert sie im ersten Tick sofort: 1000 -> 980), `LowPower_DoesNotSilenceUnits_OnlyBuildings` (Schutztest, auch ohne Gate grün), `LowPower_OnlyTheDeficitOwnersPlatformFalls`, `LowPower_OutageDropsTheHeldTarget_SoRecoveryReacquiresTheNearest`.
+
+Gelaufen (im Worktree, SDK aus dem Hauptbaum):
+- `dotnet test tools/Nova.SimRunner.Tests/Nova.SimRunner.Tests.csproj -c Release` -> 782/782 bestanden (Basis 778 + 4 neue). Kein Pin bewegt sich (CanonicalAiOutcomeTests, MatchFingerprintTests, Determinism10000 laufen im Satz grün; keine Pin-Datei angefasst).
+- Negativkontrolle: `IsOffline` temporär auf `false` gezwungen, Filter `CombatSystemTests` -> 3 der 4 neuen Tests rot (Rest 23 grün, der Schutztest erwartungsgemäß weiter grün); Gate danach wiederhergestellt, Vollauf wie oben grün.
+- `python3 .github/scripts/check_docs.py` -> OK: 203 Markdown-Dateien und 5 Quality-JSONs geprüft (nur die vorbestehenden Hinweise zu fehlenden Statuszeilen in docs/production/hashkrieg).
+- `python3 .github/scripts/check_baseline_guard.py --self-test` -> bestanden; `--changed-files --labels ` -> „passed: the PR changes only one side of the separation“.
+
+NICHT gelaufen:
+- Unity-Kompilierung und alle Unity-EditMode-Tests: der Spiegel `Assets/Tests/EditMode/Simulation/CombatSystemTests.cs`, `ResourceBarPresenterTests`, `CommandCardPresenterTests`. Die geänderten Presenter-Dateien (`Gameplay/UI/*.cs`) werden von der dotnet-Spur nicht kompiliert. Die Änderungen sind Zeichenketten bzw. ein Typtausch, der in der dotnet-Spur mit denselben Quellen kompiliert; ungeprüft bleibt nur die Unity-Seite.
+- Gespielte Runde (Kraftwerk zerstören, Türme schweigen, Neubau, Türme feuern sofort).
+- Es gibt keine Offline-Anzeige je Plattform in der Präsentation (CoreGameplay.md:127 wünscht durchgestrichene Icons) — nicht Teil dieses Pakets.
+
+## Entscheidung (D-122-Entwurf)
+Variante A (`EconomySystem` als dritter Parameter, kleinster Eingriff, Präzedenz FoW/Construction) gegen B (neue Schnittstelle, 20 Aufrufstellen), C (`ISlotFactionLookup` erweitern, Name lügt, `State/` heikel) und D (Wirtschaft manipuliert Turmzustand). Ausfallverhalten F (Ziel verwerfen) statt E. Regel-Hash: keine eigene Revision, `RulesHash64` bewegt sich nicht; Reihenfolge zu V4 des Art-Strangs offen. Eintrag: `docs/production/DecisionLog.md`, „### D-122 | Entwurf | …“.
+
+## Nebenbefunde (nichts davon umgesetzt)
+1. **Nicht reservierte Baustellen** (`EconomySystem.RecomputePower` überspringt Sites, `ConstructionSystem.cs` Platzierungsprüfung liest nur die fertige Bilanz): Stromverbraucher, die knapp nacheinander gesetzt werden, überzeichnen den Strom über mehrere Ticks, nicht nur im selben Tick (Q-040 k1 beschreibt nur den Same-Tick-Fall). Mit #67 schaltet das nach Fertigstellung die gesamte Verteidigung ab. Neues Issue oder Q-040-Erweiterung — nur mit Freigabe veröffentlichen.
+2. **Turm hält ein Ziel außer Reichweite und erfasst nie neu** (Phase 2 überspringt Angreifer mit gültigem Ziel, Phase 3 hält Ziele außer Reichweite). F mildert das nur nach Stromausfällen. Als Kommentar an #51 (Punkt „Kein Nachsetzen“) anhängen — nur mit Freigabe.
+3. Eine offline Plattform verbraucht weiter Strom (`RecomputePower` zählt sie); das Defizit kann sich so halten.
+4. NeutralUnits.md:92: eroberte neutrale Türme sollen keine Energie brauchen; die Regel würde sie erfassen, sobald solche Rollen existieren.
+5. Vertragsflächen-Tabelle in `13-15_Parallelbetrieb.md` (Maintainer-Terrain): neue sechste Fläche CombatSystem (Einheitenstrang) liest `EconomySystem.GetPlayerEconomy().IsLowPower` (Netzstrang). Nachtrag liegt beim Inhaber.
+6. Hash-Lücke wie in #108: Build mit und ohne Regel melden dieselbe `RulesHash64`; abgefedert nur durch den Build-Commit-Abgleich der Lobby. Nach dem Merge ist ein neuer Testbuild fällig; der Merge sollte nicht unbedacht im selben Fenster wie der V4-PR des Art-Strangs landen.
+7. Die Triage nannte „D-030“ als Quelle der Regel; D-030 betrifft nur die Forschung (Economy.md zitiert es trotzdem). Im Entwurf nicht als Quelle angeführt.
+8. Der Berichtsordner `reports/v8.6.0/issue-lauf-2026-10-17/` erscheint in `git status` als untracked (nicht ignoriert); der Integrator fügt ihn wie vorgesehen hinzu.
+9. Beim Negativtest habe ich kurz eine Sicherung unter `/tmp` angelegt und sofort wieder gelöscht (nicht im Repo).
+
+## Offene Fragen an den Inhaber
+- Reihenfolge zum V4-PR des Art-Strangs: Landet #67 vor V4, deckt V4 die Regel implizit; sonst als vierter Fall in #108 nachtragen.
+- Wird die Endnummer D-122 beim Merge bestätigt (D-115/D-116 sind im Art-Branch vergeben)?
+- Sollen die beiden Nebenbefunde (Baustellen-Überzeichnung, stehendes Ziel) als Issue/Kommentar veröffentlicht werden?
+- Ergänzung der Vertragsflächen-Tabelle (Nebenbefund 5).
+
+## Vorschlag für Commit-Titel und PR-Beschreibung
+Commit-Titel: `feat(combat): take defence platforms offline at low power`
+
+PR-Beschreibung:
+- **Was:** Gebäuderollen (MS-1: Verteidigungsplattform) im Strommangel des Besitzers erfassen nichts, feuern nicht und verwerfen ihr Ziel; Cooldown läuft weiter, bei Rückkehr des Stroms sofort wieder Feuer; Einheiten nie betroffen. `CombatSystem` bekommt das `EconomySystem` als dritten Parameter. HUD-Warnungen (Ressourcenleiste, Baubar) nennen „Türme aus“.
+- **Warum:** Economy.md verlangt „Verteidigungsplattformen offline“; ohne sie bleibt der Angriff auf das Kraftwerk halb wirksam. Fixes #67, Entscheidung D-122 (Entwurf).
+- **Bereiche:** `Simulation/Combat/`, `Gameplay/UI/` (zwei Presenter), Tests beider Spuren, Buildings.md, Economy.md, DemoRunbook.md, DecisionLog, CHANGELOG.
+- **Changelog:** Eintrag „Verteidigungsplattformen fallen bei Strommangel aus (#67, D-122)“ unter `[Unreleased]` / Geändert.
+- **Tests:** `dotnet test` 782/782 (778 + 4 neue), Negativkontrolle (Gate aus -> 3 von 4 neuen Tests rot), `check_docs.py` OK, `check_baseline_guard.py --self-test` OK. Kein Pin, keine Baseline bewegt, `RulesHash64` unverändert (Hash-Lücke wie #108, siehe D-122).
+- **Unity-Spuren:** nicht gelaufen — EditMode-Spiegel `CombatSystemTests` (4 neue Tests), `ResourceBarPresenterTests`, `CommandCardPresenterTests`; Unity-Kompilierung der geänderten Presenter.
+- **Spielabnahme:** nicht gespielt. Grund: Lauf headless im Issue-Lauf. Ersatznachweis: vier Sim-Tests mit Negativkontrolle. Restrisiko: Balance (Kraftwerksausfall schaltet nun die gesamte Verteidigung ab), Spielerfalle durch nicht reservierte Baustellen, HUD-Textbreite unter Unity ungeprüft (die Leiste misst die Breite zur Laufzeit).
+
+## Unity-Spuren nachgefahren (Integrator, 2026-10-17)
+
+Nach dem Commit mit `tools/unity-tests/run_unity_tests.py` (Branch #110) auf diesem Worktree, Unity 6000.5.4f1, warme Library: **EditMode 667/667, PlayMode 15/15**, keine Fehlschläge, keine Compile-Fehler, Arbeitsbaum danach sauber. Die Zeile „Unity-Spuren: nicht gelaufen“ oben ist damit überholt; der PR-Text nennt die gemessenen Zahlen.
diff --git a/reports/v8.6.0/issue-lauf-2026-10-17/03b-review-1-issue-67.md b/reports/v8.6.0/issue-lauf-2026-10-17/03b-review-1-issue-67.md
new file mode 100644
index 00000000..81dae85a
--- /dev/null
+++ b/reports/v8.6.0/issue-lauf-2026-10-17/03b-review-1-issue-67.md
@@ -0,0 +1,165 @@
+---
+issue: 67
+paket: P-67
+branch: feat/67-defence-offline-low-power
+reviewer: opus
+runde: 1
+datum: 2026-10-17
+verdict: APPROVED
+---
+# Review Runde 1 – P-67: Verteidigungsplattformen fallen bei Strommangel aus
+
+Unabhängiges Read-only-Review nach AGENTS.md §3. Ich habe keine Projektdatei
+geändert; geschrieben habe ich nur diesen Bericht. Mutationsläufe fanden in einer
+Wegwerf-Kopie im Scratchpad statt (`scratchpad/build-67/rv1`), nicht im Worktree.
+
+## Was geprüft wurde
+
+- `git status --short` und `git diff` des Worktrees vollständig (12 geänderte
+ Dateien, untrackt nur der Berichtsordner), Basis `a6858d1`.
+- Abgleich gegen Issue #67, die Analyse samt Gegenprüfung (`triage/issue-67.json`)
+ und die Paketspezifikation (`triage/paket-P-67.json`).
+- Schreibumfang: alle 12 geänderten Dateien stehen im Schreibumfang des Pakets.
+ Die vier Guard-Dateien (D-091) sind unberührt, ebenso `docs/README.md`,
+ `ScopeLedger.md`, alle Pin- und Baseline-Dateien.
+- Alle Konstruktionsstellen von `CombatSystem` per `git grep "CombatSystem("`
+ in `Assets/` und `tools/`: 20 Stellen, jede übergibt eine Variable vom Typ
+ `EconomySystem` (auch die nur in Unity kompilierten: `SkirmishAiTests`,
+ `CanonicalMatchSetupTests`, `EconomyIntegrationTests`,
+ `ProductionConstructionIntegrationTests`, `CombatSystemTests`,
+ `WeaponValuesTests` im EditMode-Ordner, `MatchRunner`). Die Variablen namens
+ `factions` sind `var factions = new EconomySystem(...)`. Nichts bleibt
+ unkompilierbar zurück.
+- Registrierungsreihenfolge Economy vor Combat in `MatchRunner.cs:290/296`,
+ `Program.cs:379/385`, `Determinism10000Scenario.cs:791/797`,
+ `LockstepNetworkTests.cs:339/345` und `:413/419`. `RecomputePower` läuft
+ unbedingt am Anfang jedes Economy-Ticks (`EconomySystem.cs:477`), die Bilanz
+ ist also abgeleitet und restore-sicher.
+- Determinismus: keine float/double, keine Hash-Container, keine Zeit- oder
+ Zufallsquelle. `IsOffline` liest nur Rolle und Bilanz; der neue Ausnahmepfad
+ von `GetPlayerEconomy` (Slot ≥ 8) ist durch den vorher aufgerufenen
+ `GetSlotFaction` schon abgedeckt.
+- EditMode-Spiegel `Assets/Tests/EditMode/Simulation/CombatSystemTests.cs`:
+ vollständiger `diff` gegen die dotnet-Spur zeigt nur Namespace, Mirror-Kommentar
+ und `sealed` – die vier neuen Tests sind identisch.
+- UI: `ResourceBarPresenter.LowPowerWarning`, `CommandCardPresenter.FormatPowerBalance`
+ und die drei angepassten Erwartungen; weitere Vorkommen der Zeichenketten per
+ `git grep` gesucht – keine weitere Stelle, die den alten Text erwartet.
+- Doku: Kopfversionen und Verlaufszeilen von Buildings.md (0.8.1 → 0.9.0),
+ Economy.md (0.6.0 → 0.7.0), DemoRunbook.md (0.6.1 → 0.6.2) vorhanden;
+ inhaltlich gegen den Code geprüft.
+- DecisionLog: D-122 steht direkt nach D-114 vor „## Offene Punkte“, Kopfzeile
+ `### D-122 | Entwurf | …`, Status, Kontext, vier bewertete Alternativen plus
+ zwei Teilentscheidungen (Ausfallverhalten E/F, Regel-Hash), Entscheidung,
+ Begründung, Konsequenzen. Kopfversion und Verlaufstabelle unberührt.
+- CHANGELOG: ein Absatz unter `## [Unreleased]` → `### Geändert` (Zeile 260 ff.),
+ mit „(#67, D-122)“.
+
+## Nachgerechnete Ergebnisse (selbst gemessen)
+
+| Prüfung | Befehl | Ergebnis |
+|---|---|---|
+| Simulationstests | `dotnet test tools/Nova.SimRunner.Tests/Nova.SimRunner.Tests.csproj -c Release` | **782/782 bestanden**, 0 Fehler (deckt sich mit dem Builder) |
+| Doku-Links | `python3 .github/scripts/check_docs.py` | **OK: 204 Markdown-Dateien und 5 Quality-JSONs** (Builder: 203 – die Differenz ist sein eigener Bericht, den der Scanner über `ls-files --others` mitzählt; vor dessen Anlage waren es 203) |
+| Baseline-Guard | `python3 .github/scripts/check_baseline_guard.py --self-test` | **bestanden**, inkl. Negativkontrolle |
+| Guard-Dateien | `git diff --name-only` gefiltert | keine der vier berührt |
+| Negativkontrolle Gate aus | Kopie, `IsOffline` → `false`, Filter `CombatSystemTests` | **3 von 26 rot** (StaysSilent, OnlyTheDeficitOwners, OutageDrops), 23 grün – bestätigt die Builder-Angabe |
+| Mutation E (Ziel behalten) | Kopie, Phase 3 ohne `AttackTarget = Invalid` | **1 rot** (`LowPower_OutageDropsTheHeldTarget_…`) – F ist festgenagelt |
+| Mutation Cooldown eingefroren | Kopie, Phase 1 zählt offline Gebäude nicht herunter | **26/26 grün – Mutation überlebt** (siehe Befund M-1) |
+
+Pins: Der Vollauf ist grün, und keine Pin-Datei (`CanonicalAiOutcomeTests`,
+`MatchFingerprintTests`, Determinism10000) ist geändert; damit bewegt sich kein
+Pin. `RulesHash64` bleibt unverändert (keine Konstante berührt).
+
+Nicht gelaufen (auch nicht beim Reviewer): Unity-Kompilierung, EditMode-Tests
+(Spiegel `CombatSystemTests`, `ResourceBarPresenterTests`,
+`CommandCardPresenterTests`), gespielte Runde. Der Builder weist alles das
+korrekt als nicht gelaufen aus.
+
+## Befunde
+
+### Hoch
+
+Keine.
+
+### Mittel
+
+**M-1 – Der Weiterlauf des Cooldowns ist behauptet, aber nicht getestet.**
+`tools/Nova.SimRunner.Tests/CombatSystemTests.cs:270-271` (und Spiegel
+`Assets/Tests/EditMode/Simulation/CombatSystemTests.cs:270-271`) prüft
+`WeaponCooldownTicks == 0` mit der Meldung „the cooldown keeps running down
+while offline“. Die Plattform hat in diesem Test aber nie gefeuert; der Cooldown
+war von Anfang an 0, die Prüfung ist leer. Meine Mutation (Phase 1 zählt den
+Cooldown offline nicht herunter) überlebt alle 26 Combat-Tests. Auch der
+Builder-Bericht beschreibt das als „Cooldown läuft auf 0“ – technisch wahr,
+belegt aber nicht, was die Paketspezifikation, D-122 und Economy.md als Regel
+festschreiben („nach Rückkehr des Stroms sofort wieder Feuer“). Kein
+Korrektheitsfehler im Code (Phase 1 ist unverändert und richtig), daher nicht
+blockierend.
+*Vorschlag:* den Test so bauen, dass die Plattform erst bei gedecktem Strom
+feuert (Cooldown 10), dann ausfällt, 10 Ticks offline läuft, und bei Rückkehr im
+ersten Tick sofort trifft; alternativ `WeaponCooldownTicks = 10` direkt setzen,
+10 Ticks im Defizit steppen, `== 0` prüfen. In beiden Spuren identisch.
+
+### Niedrig
+
+**N-1 – Bericht widerspricht sich zu „gitignored“.**
+`reports/v8.6.0/issue-lauf-2026-10-17/03-issue-67-defence-low-power.md:27` nennt
+den Bericht „gitignored“, Nebenbefund 8 desselben Berichts sagt richtig „nicht
+ignoriert“. Ursache (vorbestehend, nicht Teil des Pakets): `.gitignore:26`
+lautet `reports/ # lokale …` – Git kennt keine Inline-Kommentare, das
+Muster enthält also den Kommentartext und greift nie. Nebenbei zählt dadurch
+`check_docs.py` die Berichte mit (tote Links in Berichten würden die CI rot
+machen, sobald sie committet sind). *Vorschlag:* Zeile 27 im Bericht
+korrigieren; den `.gitignore`-Fehler als Nebenbefund an den Inhaber.
+
+**N-2 – Historische Sprintdoku bleibt stehen.**
+`docs/production/hashkrieg/16_Sprint_Wirtschaft.md:218` („Die Verteidigung fällt
+hier nicht mit“) ist als Sprintplan historisch korrekt und nicht im
+Schreibumfang. Kein Handlungsbedarf, allenfalls ein Verweis auf #67 beim
+nächsten Anfassen.
+
+**N-3 – CHANGELOG-Absatz ist lang.**
+Der Eintrag hat zwölf Zeilen, die Vorgabe erlaubt „eine Zeile oder einen kurzen
+Absatz“. Inhaltlich richtig und ehrlich (Hash-Lücke, nicht gelaufene
+EditMode-Tests benannt); Kategorie „Geändert“ passt, weil sich bestehendes
+Combat-Verhalten ändert. Kürzen optional.
+
+**N-4 – Offline-Zustand ist nicht je Plattform sichtbar.**
+Die Warnung „Türme aus“ steht an Ressourcenleiste und Baubar; eine Anzeige an
+der Plattform selbst (CoreGameplay.md:127: durchgestrichene Icons) fehlt. Der
+Builder nennt das selbst als nicht Teil des Pakets. Für die Spielabnahme
+relevant, nicht für den Merge.
+
+## Fachliche Bewertung
+
+- **Korrektheit:** Das Gate sitzt in Phase 2 (nach `IsArmed`, vor der
+ Kandidatensuche) und in Phase 3 (nach Ziel-Validierung und `IsArmed`, vor
+ Reichweite/Sicht/Cooldown). Offline-Plattformen erfassen nicht, verwerfen ihr
+ Ziel und feuern nicht; ein explizit gegebenes Ziel wird ebenfalls verworfen,
+ was zu F passt. Einheiten sind über `IsBuildingRole` sicher ausgenommen; im
+ MS-1 ist die DefensePlatform die einzige bewaffnete Gebäuderolle. Ein Tick
+ Latenz bei im selben Tick zerstörtem Kraftwerk ist korrekt beschrieben.
+- **Tests:** Drei der vier neuen Tests nageln das Gate fest (Negativkontrolle),
+ der vierte ist als Schutztest ehrlich deklariert; F gegen E ist festgenagelt.
+ Lücke nur beim Cooldown (M-1).
+- **Doku:** Buildings.md, Economy.md und DemoRunbook.md stimmen mit dem Code
+ überein; die Spielerfalle durch nicht reservierte Baustellen ist benannt.
+ D-030 wird korrekt nicht als Quelle der Plattformregel angeführt.
+- **D-122:** vollständig, Präzedenzlage zum Regel-Hash ehrlich in beide
+ Richtungen dargestellt (D-108/#55 ohne Hashbewegung, 16.6 mit Revision).
+- **Bericht:** vollständig, Zahlen stimmen mit meiner Messung überein,
+ Nicht-Gelaufenes korrekt ausgewiesen, Spielabnahme als „nicht gespielt“ mit
+ Grund, Ersatznachweis und Restrisiko. Bis auf N-1 und die geschönte
+ Cooldown-Formulierung (M-1) ehrlich.
+
+## Empfehlung für den Inhaber
+
+**APPROVED.** Mergebar nach grüner Pflicht-CI. Vor dem Commit empfehle ich M-1
+nachzuziehen (ein Test in zwei Spuren, wenige Zeilen), weil die
+Cooldown-Zusage sonst nur im Kommentar existiert. Weiterhin offen und nur vom
+Inhaber zu entscheiden: Reihenfolge zum V4-PR des Art-Strangs (Hash-Lücke),
+Endnummer der D-ID, Veröffentlichung der zwei Nebenbefunde (Baustellen-
+Überzeichnung, stehendes Ziel an #51), Nachtrag der sechsten Vertragsfläche in
+`13-15_Parallelbetrieb.md`. Unity-EditMode-Lauf und eine gespielte Runde stehen
+aus; der PR muss die Spielabnahme ausdrücklich als „nicht gespielt“ führen.
diff --git a/tools/Nova.SimRunner.Tests/CombatSystemTests.cs b/tools/Nova.SimRunner.Tests/CombatSystemTests.cs
index 1fd0fb6d..05f13d27 100644
--- a/tools/Nova.SimRunner.Tests/CombatSystemTests.cs
+++ b/tools/Nova.SimRunner.Tests/CombatSystemTests.cs
@@ -244,6 +244,102 @@ public void AutoAcquire_DefensePlatform_FiresOnItsOwn()
Assert.That(HealthOf(host, attacker), Is.LessThan(200), "the platform actually fires");
}
+ // ------------------------------------------------------------------
+ // #67, D-122: defence platforms go offline in power deficit
+ // ------------------------------------------------------------------
+
+ [Test]
+ public void LowPower_DefensePlatform_StaysSilent_AndResumesWhenPowerReturns()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId platform = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(platform).Role = UnitRole.DefensePlatform; // armed: 20 dmg, range 10, cd 10
+ EntityId target = SpawnAt(host, 1, 13, 10, maxHealth: 1000);
+
+ // The economy system is not registered in this host, so nothing
+ // recomputes the balance: setting it directly is authoritative.
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 30;
+ eco.PowerRequired = 40; // deficit
+ Assert.That(host.Economy.GetPlayerEconomy(0).IsLowPower, Is.True);
+
+ host.Step(30);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget.IsValid, Is.False,
+ "offline: no auto-acquisition while the owner is in power deficit");
+ Assert.That(HealthOf(host, target), Is.EqualTo(1000), "offline: no shot");
+ Assert.That(host.Entities.GetUnitRef(platform).WeaponCooldownTicks, Is.EqualTo(0),
+ "the cooldown keeps running down while offline");
+
+ host.Economy.GetPlayerEconomy(0).PowerProvided = 100; // balance restored
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(target),
+ "power back: the platform acquires again in the very next tick");
+ Assert.That(HealthOf(host, target), Is.EqualTo(1000 - 20),
+ "power back: it fires at once, no warm-up (Economy.md: restores all systems immediately)");
+ }
+
+ [Test]
+ public void LowPower_DoesNotSilenceUnits_OnlyBuildings()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId unit = SpawnAt(host, 0, 10, 10, maxHealth: 100); // generic armed unit role
+ EntityId target = SpawnAt(host, 1, 13, 10, maxHealth: 1000);
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 0;
+ eco.PowerRequired = 50;
+
+ host.Step(3);
+ Assert.That(host.Entities.GetUnitRef(unit).AttackTarget, Is.EqualTo(target),
+ "a mobile unit is not a power consumer and still acquires");
+ Assert.That(HealthOf(host, target), Is.LessThan(1000),
+ "units keep firing in a power deficit; only building roles fall");
+ }
+
+ [Test]
+ public void LowPower_OnlyTheDeficitOwnersPlatformFalls()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId mine = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(mine).Role = UnitRole.DefensePlatform;
+ EntityId theirs = SpawnAt(host, 1, 13, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(theirs).Role = UnitRole.DefensePlatform;
+ host.Economy.GetPlayerEconomy(0).PowerRequired = 10; // slot 0 in deficit, slot 1 balanced
+
+ host.Step(3);
+ Assert.That(HealthOf(host, mine), Is.LessThan(600),
+ "the balanced owner's platform shoots the deficit owner's platform");
+ Assert.That(HealthOf(host, theirs), Is.EqualTo(600),
+ "the deficit owner's platform is offline");
+ }
+
+ [Test]
+ public void LowPower_OutageDropsTheHeldTarget_SoRecoveryReacquiresTheNearest()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId platform = SpawnAt(host, 0, 10, 10, maxHealth: 600);
+ host.Entities.GetUnitRef(platform).Role = UnitRole.DefensePlatform;
+ EntityId first = SpawnAt(host, 1, 13, 10, maxHealth: 5000);
+ host.Step(2);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(first),
+ "precondition: the platform holds the first hostile");
+
+ ref PlayerEconomyState eco = ref host.Economy.GetPlayerEconomy(0);
+ eco.PowerProvided = 0;
+ eco.PowerRequired = 10; // outage
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget.IsValid, Is.False,
+ "an outage drops the held target (failure behaviour F, D-122)");
+
+ Teleport(host, first, 40, 10); // the old target walks off
+ EntityId second = SpawnAt(host, 1, 12, 10, maxHealth: 5000);
+ host.Economy.GetPlayerEconomy(0).PowerRequired = 0; // recovery
+ host.Step(1);
+ Assert.That(host.Entities.GetUnitRef(platform).AttackTarget, Is.EqualTo(second),
+ "recovery acquires the nearest hostile instead of resuming a stale lock");
+ host.Step(12);
+ Assert.That(HealthOf(host, second), Is.LessThan(5000));
+ }
+
[Test]
public void ActiveDefensePlatformSite_NeitherAcquiresNorExecutesExplicitAttack()
{