diff --git a/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs b/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
index c23fcf1..8d82d9f 100644
--- a/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
+++ b/Assets/Tests/EditMode/Gameplay/CommandCardPresenterTests.cs
@@ -1,6 +1,7 @@
using NUnit.Framework;
using Nova.Core;
using Nova.Gameplay;
+using Nova.Simulation.Construction;
using Nova.Simulation.Definitions;
using Nova.Simulation.Production;
using Nova.Simulation.State;
@@ -10,8 +11,8 @@ namespace Nova.Gameplay.Tests
///
/// Contract tests for the role-aware command-card mapping of
/// : which buttons a selection shows
- /// (presence), the blocker evaluation order that greys them
- /// (availability — the mirror of the executor's own validation order),
+ /// (presence), the blocker evaluation priorities that grey them
+ /// (availability — documented separately for each surface),
/// the producer/content listing and the building-side repair-actor
/// convention. The legacy count-only overload is pinned by
/// SelectionManagerTests and deliberately not duplicated here.
@@ -192,7 +193,7 @@ public void GetProducibleUnits_FiltersByFaction()
}
// ----------------------------------------------------------------
- // Availability evaluation (mirrors the executor's validation order)
+ // Availability evaluation (documented per-surface priorities)
// ----------------------------------------------------------------
[Test]
@@ -239,6 +240,75 @@ public void EvaluateProductionBlocker_InsufficientCreditsAndNone()
"unlocked, funded and queue space: the executor would apply");
}
+ [Test]
+ public void EvaluateBuildingPlacementBlocker_FollowsBuildMenuPriority()
+ {
+ Assert.IsTrue(SimDefinitions.TryGetBuilding(
+ FactionId.Alliance, UnitRole.VehicleFactory, out SimBuildingDefinition factory));
+
+ Assert.AreEqual(
+ BuildingPlacementBlocker.MissingPrerequisite,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in factory, prerequisiteMet: false, credits: 0,
+ powerProvided: 0, powerRequired: 0, activeSiteCount: ConstructionSystem.MaxSites),
+ "the build menu prioritizes the actionable prerequisite chain when every blocker applies");
+ Assert.AreEqual(
+ BuildingPlacementBlocker.InsufficientCredits,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in factory, prerequisiteMet: true, credits: factory.CostAE - 1,
+ powerProvided: 0, powerRequired: 0, activeSiteCount: ConstructionSystem.MaxSites),
+ "affordability wins over power and capacity");
+ Assert.AreEqual(
+ BuildingPlacementBlocker.InsufficientPower,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in factory, prerequisiteMet: true, credits: factory.CostAE,
+ powerProvided: factory.PowerRequired - 1, powerRequired: 0,
+ activeSiteCount: ConstructionSystem.MaxSites),
+ "power wins over site capacity");
+ Assert.AreEqual(
+ BuildingPlacementBlocker.SiteCapacityReached,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in factory, prerequisiteMet: true, credits: factory.CostAE,
+ powerProvided: factory.PowerRequired, powerRequired: 0,
+ activeSiteCount: ConstructionSystem.MaxSites),
+ "free power equal to the draw is sufficient, exposing the later capacity blocker");
+ Assert.AreEqual(
+ BuildingPlacementBlocker.None,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in factory, prerequisiteMet: true, credits: factory.CostAE,
+ powerProvided: factory.PowerRequired, powerRequired: 0,
+ activeSiteCount: ConstructionSystem.MaxSites - 1));
+ }
+
+ [Test]
+ public void EvaluateBuildingPlacementBlocker_ZeroDrawNeverEnergyBlocks()
+ {
+ Assert.IsTrue(SimDefinitions.TryGetBuilding(
+ FactionId.Alliance, UnitRole.Power, out SimBuildingDefinition powerPlant));
+
+ Assert.AreEqual(
+ BuildingPlacementBlocker.None,
+ CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in powerPlant, prerequisiteMet: true, credits: powerPlant.CostAE,
+ powerProvided: 0, powerRequired: 100, activeSiteCount: 0));
+ }
+
+ [Test]
+ public void PowerFormatters_NameBalanceConsequenceAndBuildingDraw()
+ {
+ Assert.AreEqual("Strom 30/20", CommandCardPresenter.FormatPowerBalance(30, 20));
+ Assert.AreEqual(
+ "Strom 20/40 · LOW POWER: Produktion ½",
+ CommandCardPresenter.FormatPowerBalance(20, 40));
+
+ Assert.IsTrue(SimDefinitions.TryGetBuilding(
+ FactionId.Alliance, UnitRole.Power, out SimBuildingDefinition powerPlant));
+ Assert.IsTrue(SimDefinitions.TryGetBuilding(
+ FactionId.Alliance, UnitRole.Refinery, out SimBuildingDefinition refinery));
+ Assert.AreEqual("Erzeugt +100 Strom", CommandCardPresenter.FormatBuildingPower(in powerPlant));
+ Assert.AreEqual("Benötigt 20 Strom", CommandCardPresenter.FormatBuildingPower(in refinery));
+ }
+
[Test]
public void EvaluateBuildingRepairBlocker_FollowsTheSimRepairRule()
{
diff --git a/Assets/Tests/EditMode/Simulation/KernelIntegrationTests.cs b/Assets/Tests/EditMode/Simulation/KernelIntegrationTests.cs
index 45ee821..f8c8005 100644
--- a/Assets/Tests/EditMode/Simulation/KernelIntegrationTests.cs
+++ b/Assets/Tests/EditMode/Simulation/KernelIntegrationTests.cs
@@ -153,6 +153,35 @@ public void SealedMoveCommand_ChangesUnitStateAtTargetTick()
"the ordered unit must actually move after the target tick");
}
+ [Test]
+ public void SealedStopCommand_ClearsMovementAndAttackTarget_WithoutCombatSystem()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId unit = host.Entities.SpawnUnit(
+ 0, new Transform2D(SimFixed.FromFloat(10.5f), SimFixed.FromFloat(10.5f)), SimFixed.FromInt(5));
+ EntityId target = host.Entities.SpawnUnit(
+ 1, new Transform2D(SimFixed.FromFloat(20.5f), SimFixed.FromFloat(20.5f)), SimFixed.FromInt(5));
+
+ ref UnitState state = ref host.Entities.GetUnitRef(unit);
+ state.SetTarget(new GridPos2D(30, 30));
+ state.AttackTarget = target;
+
+ var stop = new StopPayload(new[] { UnitCommandStateView.ToRawEntityId(unit) });
+ Assert.AreEqual(
+ CommandIngressResult.Accepted,
+ host.Ingress.TrySubmitIntent(CommandIntent.Create(stop), out _));
+
+ host.StepTick();
+
+ Assert.AreEqual(1, host.Kernel.LastTickResults.Count);
+ Assert.AreEqual(CommandResultCode.Applied, host.Kernel.LastTickResults[0].Code);
+ ref readonly UnitState stopped = ref host.Entities.GetUnitRef(unit);
+ Assert.IsFalse(stopped.IsMoving);
+ Assert.IsFalse(stopped.TargetGridPos.IsValid);
+ Assert.IsFalse(stopped.GoalGridPos.IsValid);
+ Assert.AreEqual(EntityId.Invalid, stopped.AttackTarget);
+ }
+
[Test]
public void StateHash_ReflectsStateMutation_AndStaysStableOnRepeat()
{
diff --git a/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs b/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
index f3db7dc..335f5f5 100644
--- a/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
+++ b/Assets/_Project/Scripts/Gameplay/UI/CommandCardPresenter.cs
@@ -1,5 +1,6 @@
using System;
using Nova.Core;
+using Nova.Simulation.Construction;
using Nova.Simulation.Definitions;
using Nova.Simulation.Production;
using Nova.Simulation.State;
@@ -10,7 +11,8 @@ namespace Nova.Gameplay
/// The buttons one command card can show. Presence is role-driven
/// (); whether a present button is
/// clickable is a separate, state-driven evaluation
- /// (, )
+ /// (, ,
+ /// )
/// — a greyed button must always carry its reason.
///
[Flags]
@@ -44,6 +46,23 @@ public enum ProductionBlocker
InsufficientCredits = 3,
}
+ ///
+ /// Why the build bar reports a building as blocked. Before a target cell
+ /// exists, the HUD uses the explicit priority prerequisite, affordability,
+ /// free power and finally global construction-site capacity. This is a UI
+ /// priority, not the global CommandExecutor order (which checks credits
+ /// before domain validation). The UI derives the reason because schema v1
+ /// deliberately shares one result code between several domain cases.
+ ///
+ public enum BuildingPlacementBlocker
+ {
+ None = 0,
+ MissingPrerequisite = 1,
+ InsufficientCredits = 2,
+ InsufficientPower = 3,
+ SiteCapacityReached = 4,
+ }
+
///
/// Why a building's repair button is blocked. The sim's repair order
/// needs a live Builder as the actor and an own COMPLETED placement that
@@ -202,6 +221,55 @@ public static ProductionBlocker EvaluateProductionBlocker(
return ProductionBlocker.None;
}
+ ///
+ /// First building-placement blocker in the build bar's explicit UI
+ /// priority. Geometry is intentionally absent: the bar has no target
+ /// cell until placement mode starts. This priority is not the global
+ /// CommandExecutor order; an energy blocker is informational and must
+ /// not disable entering placement mode.
+ ///
+ public static BuildingPlacementBlocker EvaluateBuildingPlacementBlocker(
+ in SimBuildingDefinition definition, bool prerequisiteMet, long credits,
+ int powerProvided, int powerRequired, int activeSiteCount)
+ {
+ if (!prerequisiteMet) return BuildingPlacementBlocker.MissingPrerequisite;
+ if (credits < definition.CostAE) return BuildingPlacementBlocker.InsufficientCredits;
+ if (definition.PowerRequired > 0
+ && powerProvided - powerRequired < definition.PowerRequired)
+ {
+ return BuildingPlacementBlocker.InsufficientPower;
+ }
+ if (activeSiteCount >= ConstructionSystem.MaxSites)
+ {
+ return BuildingPlacementBlocker.SiteCapacityReached;
+ }
+ return BuildingPlacementBlocker.None;
+ }
+
+ ///
+ /// Compact live grid balance for the build bar. Low power must name
+ /// its gameplay consequence where the player makes build decisions.
+ ///
+ public static string FormatPowerBalance(int powerProvided, int powerRequired)
+ {
+ string balance = $"Strom {powerProvided}/{powerRequired}";
+ return powerRequired > powerProvided
+ ? balance + " · LOW POWER: Produktion ½"
+ : balance;
+ }
+
+ /// Power generation or draw shown on a selected building's command card and on build-button hover.
+ public static string FormatBuildingPower(in SimBuildingDefinition definition)
+ {
+ if (definition.PowerProvided > 0 && definition.PowerRequired > 0)
+ {
+ return $"Erzeugt +{definition.PowerProvided} · benötigt {definition.PowerRequired} Strom";
+ }
+ if (definition.PowerProvided > 0) return $"Erzeugt +{definition.PowerProvided} Strom";
+ if (definition.PowerRequired > 0) return $"Benötigt {definition.PowerRequired} Strom";
+ return "Kein Strombedarf";
+ }
+
///
/// First blocker of a building's repair button: an undamaged building
/// has nothing to repair (the executor rejects such an order as an
diff --git a/Assets/_Project/Scripts/Presentation/UI/BuildMenuHud.cs b/Assets/_Project/Scripts/Presentation/UI/BuildMenuHud.cs
index bcca97c..ca73fb4 100644
--- a/Assets/_Project/Scripts/Presentation/UI/BuildMenuHud.cs
+++ b/Assets/_Project/Scripts/Presentation/UI/BuildMenuHud.cs
@@ -26,8 +26,9 @@ namespace Nova.Presentation.UI
/// the bar, where it appears while the entry is hovered.
///
///
- /// THE STATUS LINE above the bar serves three masters in priority order:
- /// the hovered entry's blocker reason, then the D-085 builder warning
+ /// THE STATUS LINE above the bar always carries the live power balance on
+ /// the left. Its right side serves three masters in priority order: the
+ /// hovered entry's blocker or power value, then the D-085 builder warning
/// ("Kein Builder — Bau pausiert…") shown as long as any own construction
/// site has no living Builder — the visible warning instead of the silent
/// dead end —, then the onboarding hint below.
@@ -38,9 +39,11 @@ namespace Nova.Presentation.UI
/// alternative BuildingRegistrySO has no asset instances in the project
/// and is not wired at runtime, so SimDefinitions is the honest source:
/// the bar cannot drift from the executor. Availability mirrors the
- /// executor's own rule precisely — the prerequisite check is the sim's
- /// and the credit
- /// check is the balance the executor charges at placement.
+ /// prerequisite, credit and global site-capacity gates; the prerequisite
+ /// check is the sim's all-of
+ /// rule. Power is
+ /// derived and shown too, but deliberately does not disable entering
+ /// placement mode — the target-cell decision still belongs to placement.
///
///
/// Clicking an available entry calls
@@ -90,9 +93,6 @@ public sealed class BuildMenuHud : MonoBehaviour
/// Horizontal gap between two buttons (explicit rects, so the gap is exactly this, not a GUILayout margin guess).
private const float ButtonSpacing = 4f;
- /// Width of the status line above the bar (centered on the zone), same measure the onboarding hint had.
- private const float StatusLineWidth = 640f;
-
[Header("Wiring (scene generator)")]
[SerializeField] private MatchRunner _runner;
[SerializeField] private RtsDeviceInput _input;
@@ -114,6 +114,7 @@ public sealed class BuildMenuHud : MonoBehaviour
private readonly StringBuilder _builder = new StringBuilder(64);
private GUIStyle _buttonStyle;
private GUIStyle _statusStyle;
+ private GUIStyle _powerStatusStyle;
private bool _hintDismissed;
private float _hintShownAt = -1f;
@@ -121,8 +122,7 @@ public sealed class BuildMenuHud : MonoBehaviour
// the status line and the buttons.
private bool _siteLacksBuilder;
private int _siteLacksBuilderFrame = -1;
- private string _hoveredBlockerReason;
- private string _hoveredRadarHint;
+ private string _hoveredStatusText;
private string _transientNotice;
private float _transientNoticeUntil;
@@ -281,7 +281,9 @@ private void DrawBar()
if (economy == null || construction == null) return; // match not initialized yet
byte slot = _runner.Session != null ? _runner.Session.LocalSlot : (byte)0;
- long credits = economy.GetPlayerEconomy(slot).AetheriumCredits;
+ ref readonly PlayerEconomyState playerEconomy = ref economy.GetPlayerEconomy(slot);
+ long credits = playerEconomy.AetheriumCredits;
+ int activeSiteCount = construction.SiteCount;
FactionId faction = economy.GetSlotFaction(slot);
Rect zone = ComputeBarZone();
@@ -295,8 +297,7 @@ private void DrawBar()
// is exactly why they are explicit.
float scale = Mathf.Max(1f, _uiScale);
Vector2 guiMouse = HudLayout.RawMouseToGui(Input.mousePosition, scale);
- _hoveredBlockerReason = null;
- _hoveredRadarHint = null;
+ _hoveredStatusText = null;
for (int i = 0; i < BuildableRoles.Length; i++)
{
UnitRole role = BuildableRoles[i];
@@ -306,29 +307,42 @@ private void DrawBar()
buttonsRect.x + i * (buttonWidth + ButtonSpacing), buttonsRect.y, buttonWidth, _buttonHeight);
if (!rect.Contains(guiMouse)) continue;
- if (!IsAvailable(in def, slot, credits, construction))
- {
- _hoveredBlockerReason = BlockerReason(role, in def, slot, credits, construction);
- }
- else if (role == UnitRole.Radar)
+ UnitRoleMask missingPrerequisites =
+ construction.GetMissingPrerequisiteRoles(slot, def.PrerequisiteRoles);
+ bool prerequisiteMet = missingPrerequisites == UnitRoleMask.None;
+ BuildingPlacementBlocker blocker = CommandCardPresenter.EvaluateBuildingPlacementBlocker(
+ in def, prerequisiteMet, credits,
+ playerEconomy.PowerProvided, playerEconomy.PowerRequired, activeSiteCount);
+ _hoveredStatusText = BlockerReason(
+ role, in def, blocker, missingPrerequisites, credits,
+ playerEconomy.PowerProvided, playerEconomy.PowerRequired, activeSiteCount);
+ if (_hoveredStatusText == null)
{
- // 16.5 (#54, C3): the button says in plain text what it
- // unlocks — the minimap is a Radar function now, and
- // losing the building takes the map away again.
- _hoveredRadarHint = "Radar: schaltet die Minimap frei — ohne Radar keine Karte";
+ _hoveredStatusText = $"{CommandCardPresenter.BuildingDisplayName(role)}: "
+ + CommandCardPresenter.FormatBuildingPower(in def);
+ if (role == UnitRole.Radar)
+ {
+ // 16.5 (#54, C3): keep the minimap unlock explicit
+ // while 16.10 adds the building's power draw.
+ _hoveredStatusText += " · schaltet Minimap frei";
+ }
}
}
// Chrome and status line paint BEFORE the buttons — IMGUI paints
// in call order and later calls sit on top.
- DrawChromeAndStatusLine(zone, buttonsRect);
+ DrawChromeAndStatusLine(
+ zone, buttonsRect,
+ CommandCardPresenter.FormatPowerBalance(
+ playerEconomy.PowerProvided, playerEconomy.PowerRequired));
for (int i = 0; i < BuildableRoles.Length; i++)
{
UnitRole role = BuildableRoles[i];
if (!SimDefinitions.TryGetBuilding(faction, role, out SimBuildingDefinition def)) continue;
- bool available = IsAvailable(in def, slot, credits, construction);
+ bool prerequisiteMet = PrerequisiteMet(in def, slot, construction);
+ bool available = IsAvailable(in def, prerequisiteMet, credits, activeSiteCount);
var rect = new Rect(
buttonsRect.x + i * (buttonWidth + ButtonSpacing), buttonsRect.y, buttonWidth, _buttonHeight);
@@ -343,29 +357,50 @@ private void DrawBar()
}
}
- /// Entry availability, the executor's own rule: prerequisite finished (if any) and enough credits.
- private static bool IsAvailable(in SimBuildingDefinition def, byte slot, long credits, ConstructionSystem construction)
+ private static bool PrerequisiteMet(
+ in SimBuildingDefinition def, byte slot, ConstructionSystem construction)
{
- bool prerequisiteMet = construction.HasFinishedBuildings(slot, def.PrerequisiteRoles);
- return prerequisiteMet && credits >= def.CostAE;
+ return construction.HasFinishedBuildings(slot, def.PrerequisiteRoles);
+ }
+
+ ///
+ /// Whether the button may enter placement mode. Energy is
+ /// deliberately absent: its blocker is visible on hover, but the
+ /// player must still be able to inspect terrain in placement mode.
+ ///
+ private static bool IsAvailable(
+ in SimBuildingDefinition def, bool prerequisiteMet, long credits, int activeSiteCount)
+ {
+ return prerequisiteMet
+ && credits >= def.CostAE
+ && activeSiteCount < ConstructionSystem.MaxSites;
}
///
/// The chrome frame and the status line. IMGUI paints in call order,
/// so this runs BEFORE the buttons and the box stays behind them.
///
- private void DrawChromeAndStatusLine(Rect zone, Rect buttonsRect)
+ private void DrawChromeAndStatusLine(Rect zone, Rect buttonsRect, string powerBalance)
{
GUI.Box(
new Rect(buttonsRect.x - 4f, buttonsRect.y - 4f, buttonsRect.width + 8f, buttonsRect.height + 8f),
GUIContent.none, HudChrome.PanelStyle);
- string statusText = ResolveStatusLineText();
- if (statusText == null) return;
-
- var rect = new Rect(zone.center.x - StatusLineWidth * 0.5f, zone.y, StatusLineWidth, StatusLineHeight);
+ var rect = new Rect(buttonsRect.x, zone.y, buttonsRect.width, StatusLineHeight);
GUI.Box(rect, GUIContent.none, HudChrome.PanelStyle);
- GUI.Label(rect, statusText, _statusStyle);
+
+ float powerWidth = Mathf.Min(260f, rect.width * 0.36f);
+ GUI.Label(
+ new Rect(rect.x + 8f, rect.y, powerWidth - 8f, rect.height),
+ powerBalance, _powerStatusStyle);
+
+ string statusText = ResolveStatusLineText();
+ if (statusText != null)
+ {
+ GUI.Label(
+ new Rect(rect.x + powerWidth, rect.y, rect.width - powerWidth - 8f, rect.height),
+ statusText, _statusStyle);
+ }
}
///
@@ -373,12 +408,12 @@ private void DrawChromeAndStatusLine(Rect zone, Rect buttonsRect)
/// blocker reason (the player is interrogating that button right
/// now), then the D-085 builder warning while any own site lacks a
/// Builder, then the onboarding hint until it dismisses itself.
- /// Null = the line stays empty (and unpainted).
+ /// Null leaves the contextual right side empty; the power balance on
+ /// the left remains visible.
///
private string ResolveStatusLineText()
{
- if (_hoveredBlockerReason != null) return _hoveredBlockerReason;
- if (_hoveredRadarHint != null) return _hoveredRadarHint;
+ if (_hoveredStatusText != null) return _hoveredStatusText;
if (_transientNotice != null && Time.unscaledTime < _transientNoticeUntil) return _transientNotice;
if (_siteLacksBuilder) return ConstructionSiteStatus.NoBuilderWarning;
if (!_hintDismissed && _runner.IsRunning) return HintText;
@@ -424,36 +459,52 @@ private string ButtonLabel(UnitRole role, in SimBuildingDefinition def, float bu
return nameLine + "\n" + costLine;
}
- /// The hovered entry's blocker, in the executor's own check order — prerequisite first, then affordability.
+ /// The hovered entry's first blocker in the build bar's documented UI priority.
private string BlockerReason(
- UnitRole role, in SimBuildingDefinition def, byte slot, long credits, ConstructionSystem construction)
+ UnitRole role, in SimBuildingDefinition def, BuildingPlacementBlocker blocker,
+ UnitRoleMask missingPrerequisites, long credits,
+ int powerProvided, int powerRequired, int activeSiteCount)
{
- UnitRoleMask missing = construction.GetMissingPrerequisiteRoles(slot, def.PrerequisiteRoles);
- if (missing != UnitRoleMask.None)
+ string name = CommandCardPresenter.BuildingDisplayName(role);
+ string buildingPower = CommandCardPresenter.FormatBuildingPower(in def);
+ switch (blocker)
{
- _builder.Clear();
- _builder.Append(CommandCardPresenter.BuildingDisplayName(role)).Append(": benötigt ");
- bool appended = false;
- UnitRoleMask remaining = missing;
- for (int i = 0; i < PrerequisiteDisplayOrder.Length; i++)
- {
- UnitRole prerequisiteRole = PrerequisiteDisplayOrder[i];
- UnitRoleMask roleMask = (UnitRoleMask)(1u << (int)prerequisiteRole);
- if ((missing & roleMask) == UnitRoleMask.None) continue;
-
- if (appended) _builder.Append(" + ");
- _builder.Append(CommandCardPresenter.BuildingDisplayName(prerequisiteRole));
- appended = true;
- remaining &= ~roleMask;
- }
- if (remaining != UnitRoleMask.None)
- {
- if (appended) _builder.Append(" + ");
- _builder.Append("unbekannte Voraussetzung 0x").Append(((uint)remaining).ToString("X8"));
- }
- return _builder.ToString();
+ case BuildingPlacementBlocker.MissingPrerequisite:
+ _builder.Clear();
+ _builder.Append(name).Append(": benötigt ");
+ bool appended = false;
+ UnitRoleMask remaining = missingPrerequisites;
+ for (int i = 0; i < PrerequisiteDisplayOrder.Length; i++)
+ {
+ UnitRole prerequisiteRole = PrerequisiteDisplayOrder[i];
+ UnitRoleMask roleMask = (UnitRoleMask)(1u << (int)prerequisiteRole);
+ if ((missingPrerequisites & roleMask) == UnitRoleMask.None) continue;
+
+ if (appended) _builder.Append(" + ");
+ _builder.Append(CommandCardPresenter.BuildingDisplayName(prerequisiteRole));
+ appended = true;
+ remaining &= ~roleMask;
+ }
+ if (remaining != UnitRoleMask.None)
+ {
+ if (appended) _builder.Append(" + ");
+ _builder.Append("unbekannte Voraussetzung 0x")
+ .Append(((uint)remaining).ToString("X8"));
+ }
+ _builder.Append(" · ").Append(buildingPower);
+ return _builder.ToString();
+ case BuildingPlacementBlocker.InsufficientCredits:
+ return $"{name}: nicht genug Aetherium ({credits}/{def.CostAE} AE)"
+ + $" · {buildingPower}";
+ case BuildingPlacementBlocker.InsufficientPower:
+ return $"{name}: benötigt {def.PowerRequired} Strom · "
+ + $"{Mathf.Max(0, powerProvided - powerRequired)} frei";
+ case BuildingPlacementBlocker.SiteCapacityReached:
+ return $"{name}: Baustellenlimit erreicht ({activeSiteCount}/{ConstructionSystem.MaxSites})"
+ + $" · {buildingPower}";
+ default:
+ return null;
}
- return $"{CommandCardPresenter.BuildingDisplayName(role)}: nicht genug Aetherium";
}
///
@@ -507,9 +558,19 @@ private void EnsureStyles()
{
_statusStyle = new GUIStyle(GUI.skin.label)
{
- fontSize = 13,
+ fontSize = 9,
+ fontStyle = FontStyle.Bold,
+ alignment = TextAnchor.MiddleRight,
+ wordWrap = false
+ };
+ }
+ if (_powerStatusStyle == null)
+ {
+ _powerStatusStyle = new GUIStyle(GUI.skin.label)
+ {
+ fontSize = 10,
fontStyle = FontStyle.Bold,
- alignment = TextAnchor.MiddleCenter,
+ alignment = TextAnchor.MiddleLeft,
wordWrap = false
};
}
diff --git a/Assets/_Project/Scripts/Presentation/UI/CommandCardHud.cs b/Assets/_Project/Scripts/Presentation/UI/CommandCardHud.cs
index 2b8bec7..1b92529 100644
--- a/Assets/_Project/Scripts/Presentation/UI/CommandCardHud.cs
+++ b/Assets/_Project/Scripts/Presentation/UI/CommandCardHud.cs
@@ -28,7 +28,8 @@ namespace Nova.Presentation.UI
/// or when no Builder exists), plus — for producers — one production
/// button per unit the building builds (from
/// filtered by faction and producer
- /// role, the table the executor validates against), greyed WITH the
+ /// role, the table the executor validates against), plus the selected
+ /// building's power draw or generation from the same definition, greyed WITH the
/// reason (benötigt Forschungslabor / Warteschlange voll / nicht genug
/// Aetherium — the executor's own validation order, so the reason the
/// player reads is the one the executor would reject with). The queue is
@@ -114,6 +115,8 @@ private sealed class CardModel
public bool Visible;
public string Title = string.Empty;
public EntityId LeadId;
+ /// Generation or draw of a completed building; null on unit and site cards.
+ public string BuildingPowerText;
public readonly List Buttons = new List(16);
public string QueueHeader;
public readonly List QueueRows = new List(ProductionSystem.MaxQueueEntries);
@@ -130,6 +133,7 @@ public void Clear()
Visible = false;
Title = string.Empty;
LeadId = EntityId.Invalid;
+ BuildingPowerText = null;
Buttons.Clear();
QueueHeader = null;
QueueRows.Clear();
@@ -274,6 +278,10 @@ private void BuildBuildingModel(
CommandButtonType commands = _presenter.GetBuildingCommands(building.Role);
bool definitionKnown = SimDefinitions.TryGetBuilding(faction, building.Role, out SimBuildingDefinition def);
+ if (definitionKnown)
+ {
+ model.BuildingPowerText = CommandCardPresenter.FormatBuildingPower(in def);
+ }
if (commands.HasFlag(CommandButtonType.Sell))
{
@@ -481,6 +489,10 @@ private void OnGUI()
GUILayout.BeginVertical(HudChrome.PanelStyle);
GUILayout.Label(model.Title, _titleStyle, GUILayout.Height(TitleHeight));
+ if (model.BuildingPowerText != null)
+ {
+ GUILayout.Label(model.BuildingPowerText, _rowStyle, GUILayout.Height(RowHeight));
+ }
if (model.ProgressBar01 >= 0f) DrawProgressBar(model.ProgressBar01);
if (model.SiteStatusText != null)
{
@@ -581,6 +593,7 @@ private float EstimateHeight(CardModel model)
{
float height = HudChrome.PanelStyle.padding.vertical;
height += TitleHeight + _titleStyle.margin.vertical;
+ if (model.BuildingPowerText != null) height += RowHeight + _rowStyle.margin.vertical;
if (model.ProgressBar01 >= 0f) height += ProgressHeight; // GUIStyle.none: no margin
if (model.SiteStatusText != null) height += SiteStatusHeight + _siteStatusStyle.margin.vertical;
for (int i = 0; i < model.Buttons.Count; i++)
diff --git a/Assets/_Project/Scripts/Simulation/State/UnitCommandStateView.cs b/Assets/_Project/Scripts/Simulation/State/UnitCommandStateView.cs
index b42fbb3..b5cc998 100644
--- a/Assets/_Project/Scripts/Simulation/State/UnitCommandStateView.cs
+++ b/Assets/_Project/Scripts/Simulation/State/UnitCommandStateView.cs
@@ -275,8 +275,9 @@ public void Apply(in CommandRecord record)
{
ref UnitState unit = ref _entityManager.GetUnitRef(id);
unit.Stop();
- // Stop cancels every standing order, economy and
- // repair orders included; the unit keeps its cargo.
+ unit.AttackTarget = EntityId.Invalid;
+ // Stop cancels every standing order: attack,
+ // economy and repair included; the unit keeps its cargo.
unit.HarvestFieldId = 0;
unit.IsReturningCargo = false;
_constructionSystem?.ClearRepairOrder(stop.EntityIds[i]);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8299a3..cdd6d71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
## [Unreleased]
-> **Dokumentationsstand 0.20.0 (unveröffentlicht):** Dieses Rebaseline ist ein
+> **Dokumentationsstand 0.21.0 (unveröffentlicht):** Dieses Rebaseline ist ein
> Wiki-/Vertrags-Minor und kein Game-Release. Es wird kein Tag oder Release
> erzeugt; MS-0 und MS-1 bleiben offen.
@@ -73,6 +73,7 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt
### Behoben
+- **#45/#47/#48: Entscheidungspunkt und „Stoppen“ melden jetzt die Wahrheit (D-097):** Die Baubar zeigt dauerhaft die Strombilanz samt Low-Power-Folge, nennt beim Überfahren Bedarf beziehungsweise Erzeugung und leitet den ersten Blocker in der ausdrücklich festgelegten HUD-Priorität Voraussetzung, AE, freie Energie und Baustellenlimit her; diese Priorität ist nicht die globale Executor-Reihenfolge. Energie sperrt den Eintritt in den Platzierungsmodus bewusst nicht. Die Befehlskarte zeigt den Stromwert des gewählten Gebäudes, und ein angewandter Stop-Befehl räumt zusätzlich `AttackTarget` ab. Ein echtes Halte-Feuer bleibt ausserhalb dieses Pakets, weil D-087 im nächsten Combat-Tick wieder ein Ziel erfassen darf
- **Low Power ist eine Waffe (C4, Sprint 16.6)** — bei Energiedefizit
fällt Radar zuerst: `FogOfWarSystem.GetRadarSignatures` liefert nichts mehr
(Economy als Pflicht-Abhängigkeit), und die Minimap geht mit aus (dieselbe
diff --git a/README.md b/README.md
index 9c33194..aa4f59b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Project Nova
-**Dokumentversion:** 0.22.2 | **Status:** unveröffentlichter Entwicklungsstand, spielbar | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Stand:** 2026-08-10
+**Dokumentversion:** 0.23.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.
@@ -136,10 +136,10 @@ gemacht, das man starten, bedienen und gewinnen kann:
Dazu durchgehend: **Fraktionsidentität** (Allianz und Legion unterscheiden sich
in Schadensmatrix, Waffenwerten, Kosten und Harvester-Kapazität), ein
-deterministischer Simulationskern mit **706/706 bestandenen Headless-Tests** auf
+deterministischer Simulationskern mit **707/707 bestandenen Headless-Tests** auf
dem integrierten 16.9-Stand sowie **34 3D-Assets**, die über eine Drop-in-Pipeline
einfahren (§5). Auf demselben Stand sind außerdem die vollständige Unity-
-EditMode-Suite mit **587/587** Tests und die fokussierte Graybox-Proof-
+EditMode-Suite mit **591/591** Tests und die fokussierte Graybox-Proof-
PlayMode-Suite mit **3/3** Tests im gepinnten Editor grün gelaufen. Das ist
automatisierte Laufzeitevidenz, keine behauptete manuelle Spielabnahme.
@@ -156,10 +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 läuft weiter.** Fünf endliche Aetheriumfelder, Lagergrenze, Radar,
- Low-Power-Wirkung, Mehrfachvoraussetzungen sowie Platzierungs- und
- Reparaturkosten sind umgesetzt; das ehrliche Blocker- und Energiefeedback am
- Entscheidungspunkt folgt noch in Paket 16.10.
+- **Strang C ist technisch umgesetzt, aber noch nicht manuell abgenommen.**
+ Endliche Felder, Lagergrenze, Radar, Low Power, Mehrfachvoraussetzungen,
+ Platzierung, Reparaturkosten sowie Blocker-, Energie- und Stop-Feedback sind
+ verdrahtet; die menschliche Prüfung am Entscheidungspunkt bleibt offen.
- **Kein Speichern.** Die Simulation kann ihren Zustand vollständig
serialisieren und hash-identisch fortsetzen — es fehlt nur das Schreiben auf
die Platte.
@@ -285,9 +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 läuft weiter.** Wirtschaftsdruck, Lager, Radar, Low Power,
- Mehrfachvoraussetzungen, Platzierungsregeln und Reparaturkosten sind da; nur
- das vollständige Entscheidungsfeedback aus Paket 16.10 fehlt noch.
+- **Strang C ist nicht manuell abgenommen.** Die technische Umsetzung
+ einschließlich Blocker-, Energie- und Stop-Feedback ist vollständig; offen
+ bleibt der menschliche Durchlauf auf dem aktuellen Stand.
Die vollständigen Grenzen stehen im aktuellen
[Sprintbericht](docs/production/hashkrieg/12_Sprint_Zu_Zweit.md).
@@ -434,8 +434,9 @@ nicht rückwirkend übertragen.
## Offene Punkte
-- **Das verbleibende Strang-C-Paket 16.10** — korrektes Blocker-, Energie- und
- Stop-Feedback — ist die wichtigste offene Umsetzungsfolge.
+- **Die manuelle Strang-C-Abnahme** muss Blocker-, Energie- und Stop-Feedback
+ im laufenden Spiel prüfen; die technische Umsetzung von Paket 16.10 ist
+ abgeschlossen.
- 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.
@@ -448,8 +449,8 @@ 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. **Strang C abschließen** — ehrliches Blocker-, Energie- und Stop-Feedback
- aus Paket 16.10 integrieren.
+3. **Strang C manuell abnehmen** — Blocker-, Energie- und Stop-Feedback im
+ laufenden Spiel lesen und bedienen; die Implementierung ist abgeschlossen.
4. **Attack-Move** — Truppen sollen unterwegs Gegner bekämpfen, ohne jeden
Kontakt einzeln befohlen zu bekommen.
5. **KI-Ausbau** — der Gegner spielt, aber weiterhin schlicht; die neuen
@@ -470,6 +471,7 @@ Projekt ein Publikum hat.
| 0.22.0 | 2026-08-10 | Strang-C-Status auf D-103/D-104/D-107 nachgezogen: All-of-Voraussetzungen, footprintbasierte Platzierung, bezahlte Reparatur und faire Glutrinne-Startgeometrie sind umgesetzt; nur Paket 16.10 bleibt offen | Codex / Dennis Westermann |
| 0.22.1 | 2026-08-10 | Die veraltete Unity-Baseline durch die tatsächlich gelaufenen Nachweise 586/586 EditMode und 3/3 fokussierte Graybox-PlayMode-Tests ersetzt und klar von einer manuellen Spielabnahme getrennt | Codex / Dennis Westermann |
| 0.22.2 | 2026-08-10 | Nach dem Snapshot-Restore-Fix die erneut gelaufenen Nachweise auf 706/706 Headless- und 587/587 EditMode-Tests fortgeschrieben | Codex / Dennis Westermann |
+| 0.23.0 | 2026-08-10 | Paket 16.10 technisch abgeschlossen: All-of-Blocker, Strombilanz und Gebäudeleistung am Entscheidungspunkt sowie Stop-Löschung des Angriffsziels dokumentiert; 707/707 Headless- und 591/591 EditMode-Nachweise von der weiter offenen manuellen Abnahme getrennt | 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 64e1334..8a9e9e3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
# Project Nova – Entwicklungs-Wiki
-**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
+**Version:** 0.21.0 | **Status:** unveröffentlichter Arbeitsstand – Sprint 16 technisch umgesetzt; manuelle Strang-C-, Netzwerk-, Gefechts- und Lobby-Abnahmen offen | **Verantwortungsbereich:** Executive Producer / Technical Writer | **Sprint:** 16
## Zweck
@@ -233,9 +233,8 @@ kann keine Datei einen Gate-Pass erzeugen.
- Q-018 und Q-019 bleiben offen und nicht MS-1-blockierend.
- Sprint 12 A8 Stufen 2–4 (zwei Unity-Fenster, LAN, VPS) sind nicht gespielt.
- Für Strang B fehlt die manuelle Sicht-/Gegenhörabnahme mit einem dichten
- Gefecht; der automatisierte PlayMode-Gesamtlauf bleibt wegen des bestehenden
- headless `RenderTexture.Create`-Fehlers in `BarracksSpawnDiagnosisTests` bei
- 8/9, während der neue Slot-Reuse-Test grün ist.
+ Gefecht. Die 591/591 EditMode- und 3/3 fokussierten Graybox-PlayMode-Tests
+ ersetzen diese menschliche Prüfung nicht.
- Die vier Suno-Musikdatensätze benennen echte, noch fehlende Ursprungs- oder
Konvertierungsbelege und bleiben bis zu deren Lieferung `incomplete`.
@@ -243,8 +242,8 @@ kann keine Datei einen Gate-Pass erzeugen.
1. Den vorliegenden macOS-Build visuell und auditiv im dichten Gefecht prüfen.
2. A8 mit zwei Unity-Fenstern, danach im LAN und auf dem VPS spielen.
-3. Erst danach den Gesamt-Sprint als gespielt beziehungsweise DoD-fertig
- bezeichnen; Strang C bleibt ein eigener offener Simulationsumfang.
+3. Sprint 16 im laufenden Spiel abnehmen; Strang C ist technisch umgesetzt,
+ aber ohne diesen Durchlauf weder gespielt noch vollständig DoD-fertig.
## Änderungsverlauf
@@ -274,3 +273,4 @@ kann keine Datei einen Gate-Pass erzeugen.
| 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 |
+| 0.21.0 | 2026-08-10 | Sprint 16 technisch bis Paket 16.10 abgeschlossen und die 707/707 Headless-, 591/591 EditMode- und fokussierten 3/3 PlayMode-Nachweise klar von der offenen manuellen Strang-C-Abnahme getrennt | Codex / Dennis Westermann |
diff --git a/docs/gamedesign/Buildings.md b/docs/gamedesign/Buildings.md
index e8c41b5..67d51b9 100644
--- a/docs/gamedesign/Buildings.md
+++ b/docs/gamedesign/Buildings.md
@@ -1,6 +1,6 @@
# Gebäude – alle Fraktionen
-**Version:** 0.8.0 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
+**Version:** 0.8.1 | **Status:** Entwurf – MS-1-Override verbindlich | **Verantwortungsbereich:** Lead Gameplay Designer | **Sprint:** 16
## Zweck
@@ -256,8 +256,10 @@ Module werden auf dem fertigen Podest installiert (nicht als separates Gebäude)
mindestens 2 zu jedem Feld. Erschöpfte Felder zählen weiterhin.
- **Gebäudering:** Abstand mindestens 2 zu jeder aktiven Baustelle und jedem
lebenden fertigen Gebäude; damit bleibt ein voller Zellenring leer.
-- Die Platzierungsvorschau zeigt valide (grün) und invalide (rot) Zellen. Der
- exakte Sperrgrund am Bauknopf gehört zu Paket 16.10.
+- Die Platzierungsvorschau zeigt valide (grün) und invalide (rot) Zellen. Paket
+ 16.10 leitet am Bauknopf All-of-Voraussetzungen, AE, freie Energie und das
+ Baustellenlimit her; Energie bleibt ein sichtbarer Hinweis und sperrt den
+ Eintritt in den Platzierungsmodus bewusst nicht.
- **Vollspielziel:** variable 2×2- bis 4×4-Footprints, Rotation, Evolvierte-
Einfluss und weitere Terrain-/Neutralregeln werden erst außerhalb von MS-1
entschieden und implementiert.
@@ -325,3 +327,4 @@ Entschieden und entfernt im Korrekturlauf Sprint 4: HQ-Grundenergie (+30 führen
| 0.6.0 | 2026-08-10 | MS-1-Startwert (D-077) und Lagervertrag (D-106) nachgezogen: eine 2.000-AE-HQ-Basis je Konto, +2.000 je fertigem Lager und periodischer 25-%-Abbau des aktuellen Überhangs statt separatem Zerstörungsabzug | Codex / Dennis Westermann |
| 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 |
diff --git a/docs/production/DemoRunbook.md b/docs/production/DemoRunbook.md
index a2dc1d7..43dbe21 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.0 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16
+**Version:** 0.6.1 | **Status:** Entwurf – Graybox-Spur, kein Gate-Nachweis | **Verantwortungsbereich:** Producer / Technical Writer | **Sprint:** 16
## Zweck
@@ -152,16 +152,20 @@ steht davor und dauert so lange, wie man ihn zeigen will.
Details (Tick, Census, Sieg-Code) zeigen. Zurück ins Menü führt kein Weg —
die Runde endet mit dem Beenden des Spiels (§5).
-## 5. Bekannte Grenzen (ehrlich, Stand GB-005)
+## 5. Bekannte Grenzen (ehrlich, aktueller technischer Stand; manuelle Abnahme offen)
- **Die KI ist bewusst einfach:** feste Build-Order, nur Infanterie-Wellen,
kein Nachschub-Management jenseits der Grundregeln, kein Reagieren auf den
Spieler (kein Konter, kein Rückzug). Ihre Peer-Session ist nicht
snapshot-serialisiert.
-- **Keine Zielerfassung:** Einheiten erwidern kein Feuer; die
- Verteidigungsplattform kann nie schießen; A ohne Gegner unter dem Cursor
- ist ein schlichtes Move. (Die KI umgeht das mit expliziten Attack-Orders.)
-- `Stop` löscht das Angriffsziel nicht; Angriffe auf eigene Einheiten sind zulässig.
+- **Zielerfassung bleibt schlicht:** Bewaffnete Einheiten und fertige
+ Verteidigungsplattformen erfassen Ziele automatisch und erwidern Feuer. 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
+ Zielerfassung darf im nächsten Combat-Tick erneut ein Ziel setzen; ein echtes
+ Halte-Feuer ist weiterhin nicht implementiert. Angriffe auf eigene Einheiten
+ sind zulässig.
- Nach Siegentscheid tickt der Host weiter; es gibt keinen Ergebnisbildschirm
(nur die Statusleiste / F3) **und keinen Rückweg ins Hauptmenü**.
- **„Laden" im Menü ist ohne Funktion** und deshalb ausgegraut. Die
@@ -275,3 +279,4 @@ der Editor in der Konsole, wenn das Schreiben fehlschlägt.
| 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 |
+| 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 |
diff --git a/docs/production/hashkrieg/16_Sprint_Wirtschaft.md b/docs/production/hashkrieg/16_Sprint_Wirtschaft.md
index 64e7b48..91794c7 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.7.1 | **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.8.0 | **Status:** technisch umgesetzt – manuelle Spielabnahme zurückgestellt | **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
@@ -350,18 +350,19 @@ in Changelog und PR statt in einem erfundenen Artefakt.
- **Integrierter Ausgang:** Mit D-104 und der D-107-HQ-Korrektur entscheidet die
kanonische KI-Partie auf Tick 2.726 mit `0x10B83E94F86F2E55`. Die Kennung
bleibt `r7.E34435F9`: bewegt hat sich die Simulation, nicht die KI. Die
- vollständige lokale .NET-Suite ist auf diesem Integrationsstand mit 706/706
+ vollständige lokale .NET-Suite ist auf diesem Integrationsstand mit 707/707
grün.
### 16.10 · Ehrliche Rückmeldung am Entscheidungspunkt (#47, #48, #45)
-Drei kleine Eingriffe, die zusammengehören, weil sie dasselbe Regelwerk berühren:
+Drei kleine Eingriffe sind gemeinsam umgesetzt, weil sie dasselbe Regelwerk
+berühren:
- **#47 Der Blockergrund wird hergeleitet, nicht gemeldet.**
- `BuildMenuHud.BlockerReason` kennt heute zwei Ursachen (fehlende Voraussetzung,
- zu wenig Aetherium) und wird um **Energie** und **volles Baustellenkontingent**
- ergänzt. **Nicht** über einen neuen `CommandResultCode` — `CommandsV1/` ist
- eingefroren.
+ `BuildMenuHud.BlockerReason` leitet All-of-Voraussetzungen, zu wenig
+ Aetherium, **Energie** und ein **volles Baustellenkontingent** aus dem
+ aktuellen Simulationszustand her. Dafür gibt es keinen neuen
+ `CommandResultCode` — `CommandsV1/` bleibt eingefroren.
> **Energie darf nicht in `IsAvailable`.** Sonst wird der Knopf ausgegraut und
> der Platzierungsmodus ist gar nicht mehr erreichbar. *Grund anzeigen* ist
> nicht *Knopf sperren*.
@@ -371,8 +372,8 @@ Drei kleine Eingriffe, die zusammengehören, weil sie dasselbe Regelwerk berühr
Gebäude Bedarf beziehungsweise Erzeugung (`CommandCardPresenter`). Die
bestehende Anzeige in `DebugHud.DrawStatusBar` bleibt, wo sie ist.
- **#45 „Stoppen" löscht auch den Angriffsbefehl.** `UnitCommandStateView`
- räumt bei `CommandKind.Stop` zusätzlich `AttackTarget` ab. Heute löscht Stop
- Bewegung, Ernte und Reparatur — nur den Angriff nicht.
+ räumt bei `CommandKind.Stop` zusätzlich `AttackTarget` ab. Vor Paket 16.10
+ löschte Stop Bewegung, Ernte und Reparatur, aber nicht den Angriff.
> **Was das nicht leistet:** Die Einheit erfasst im nächsten Tick automatisch
> ein neues Ziel (D-087). Ein echtes „Feuer einstellen" braucht einen
> Haltezustand in `Simulation/Combat/` und gehört damit dem Einheitenstrang.
@@ -404,7 +405,9 @@ Drei kleine Eingriffe, die zusammengehören, weil sie dasselbe Regelwerk berühr
## Fertig wenn
-1. `./.dotnet/dotnet test tools/Nova.SimRunner.Tests/Nova.SimRunner.Tests.csproj -c Release --no-restore` ist lokal und **in der CI** grün — ohne
+1. `./.dotnet/dotnet test tools/Nova.SimRunner.Tests/Nova.SimRunner.Tests.csproj -c Release --no-restore`
+ ist lokal mit 707/707 grün; Unity EditMode steht bei 591/591. Zusätzlich
+ bleibt grüne Pflicht-CI auf dem aktuellen PR-Head Merge-Gate — ohne
Baseline-Neusetzung im selben PR wie eine Verhaltensänderung.
2. Ein Mensch hat eine Runde gespielt und dabei gesehen:
- der Sammler fährt nach der ersten Raffinerie von allein los,
@@ -419,9 +422,11 @@ Drei kleine Eingriffe, die zusammengehören, weil sie dasselbe Regelwerk berühr
3. Der Ablauf steht im [GrayboxLog](../GrayboxLog.md) mit Commit und Endzustand.
4. Die abgeworfenen Pakete stehen mit Begründung im [ScopeLedger](../ScopeLedger.md).
-Punkt 2 ist nicht durch Punkt 1 ersetzbar. `Gameplay/` und `Presentation/` sind
-in **keinem** CI-Testlauf enthalten; für 16.10 gibt es ausser der gespielten
-Runde keinen Nachweis.
+Punkt 2 ist nicht durch Punkt 1 ersetzbar. Die automatisierten Läufe decken den
+Unity-freien Stop-Vertrag, den Blocker-Evaluator, die Power-Formatter und die
+Kompilierung der Präsentationsschicht ab; sie belegen nicht, dass ein Mensch
+Hovertext, Stromzeile und Stop-Wirkung im laufenden Spiel gelesen und bedient
+hat. Diese Spielabnahme wurde zurückgestellt.
## Abwurfliste
@@ -471,6 +476,7 @@ Tests, kein Bruch.
| Version | Datum | Änderung | Autor |
|---|---|---|---|
+| 1.8.0 | 2026-08-10 | Paket 16.10 technisch abgeschlossen: All-of-/AE-/Energie-/Baustellenblocker, Stromanzeige am Baupunkt und Stop-Löschung des Angriffsziels umgesetzt; 707/707 Headless und 591/591 EditMode belegt, manuelle Spielabnahme ausdrücklich zurückgestellt | Codex / Dennis Westermann |
| 1.7.1 | 2026-08-10 | Snapshot-Restore ersetzt nun auch den dynamischen CostField-Footprintzustand eines abweichenden Zielhosts; gespiegelter Regressionstest und finaler 706/706-Nachweis ergänzt | Agent (unter Delegation) / Dennis Westermann |
| 1.7.0 | 2026-08-10 | D-107 und den integrierten 16.9-Vertrag ergänzt: Glutrinne-Layoutachse, HQ-Ursprung `(118,118)`, 45/45 Folge-Raffinerieplätze, dichte Snapshot-Reihenfolgen, Rules-Revision 3 und finaler r7-Ausgangspin | Agent (unter Delegation) / Dennis Westermann |
| 1.6.0 | 2026-08-10 | Paket 16.9 mit D-104 konkretisiert: footprintbasierte Einfluss-, Feld-, Gelände- und Gebäudeabstände sowie zustandslose kumulative 30-Prozent-Reparaturkosten und Ziel-Deduplikation festgeschrieben | Project Owner / Agent (unter Delegation) |
diff --git a/docs/production/hashkrieg/README.md b/docs/production/hashkrieg/README.md
index 2210607..b3a340d 100644
--- a/docs/production/hashkrieg/README.md
+++ b/docs/production/hashkrieg/README.md
@@ -1,6 +1,6 @@
# Übergang Project Nova → Hashkrieg — Planungsmappe
-**Version:** 0.7.2 | **Status:** fortgeschriebene Planungsmappe – Sprint 13 zur Hälfte gemergt (13.1, 13.7 seit `e15f5e6`), die offenen Pakete 13.2–13.5 warten auf Zugangsdaten und zwei Menschen an zwei Rechnern; **Sprint 16 ist nach dem ersten Betatest vorgezogen** und läuft seit D-095 parallel zum Einheitenstrang; Sprint 18 neu angelegt | **Verantwortungsbereich:** Project Owner / Orchestrator / Producer | **Sprint:** 16
+**Version:** 0.8.0 | **Status:** fortgeschriebene Planungsmappe – Sprint 16 technisch umgesetzt, manuelle Spielabnahme offen; Sprint 13.2–13.5 warten weiter auf Zugangsdaten und zwei Menschen an zwei Rechnern | **Verantwortungsbereich:** Project Owner / Orchestrator / Producer | **Sprint:** 16
## Zweck
@@ -50,7 +50,7 @@ ausschließlich in [../MVPRecoveryPlan.md](../MVPRecoveryPlan.md) und
| [14_Sprint_Lobby.md](14_Sprint_Lobby.md) | Sprint 14 (Maintainer); Match per Code über Supabase, Fraktionswahl, Build-Abgleich vor dem Verbinden |
| [15_Sprint_Netzstabilitaet.md](15_Sprint_Netzstabilitaet.md) | Sprint 15 (Maintainer); Reconnect, Desync-Erstbefund, adaptive Eingabeverzögerung, Dauerbetrieb des Relays |
| [16-19_Betatest_Einordnung.md](16-19_Betatest_Einordnung.md) | **Einordnung des ersten Betatest-Berichts** in die Sprintfolge — Issues #43–#58, nach Schreibhoheit geschnitten, mit den offenen Inhaberentscheidungen |
-| [16_Sprint_Wirtschaft.md](16_Sprint_Wirtschaft.md) | Sprint 16 (Netzstrang, **vorgezogen**); Strang C aus Sprint 12 und die **acht** Betatest-Befunde (#43, #44, #45, #46, #47, #48, #53, #54) im selben Schreibbereich — Knappheit, Lager, Radar, Low Power, Bauvoraussetzungen, Platzierung, Reparaturkosten |
+| [16_Sprint_Wirtschaft.md](16_Sprint_Wirtschaft.md) | Sprint 16 (Netzstrang, **technisch umgesetzt; manuelle Spielabnahme offen**); Strang C aus Sprint 12 und die **acht** Betatest-Befunde (#43, #44, #45, #46, #47, #48, #53, #54) — Knappheit, Lager, Radar, Low Power, Bauvoraussetzungen, Platzierung, Reparaturkosten und Entscheidungsfeedback |
| [17_Sprint_Zugangsprotokoll.md](17_Sprint_Zugangsprotokoll.md) | Sprint 17 (Maintainer); Zugriffsprotokoll, Sperrliste und Erstmeldung — Paket A läuft im Großauftrag als **Block 4 hinter der Lobby**, weil es die Lobby-Functions aus Sprint 14 voraussetzt; es liegt bis auf den `.partial`-Fix in `RelayServerCore.cs` ausserhalb des Repos |
| [18_Sprint_Befehl_und_Auswahl.md](18_Sprint_Befehl_und_Auswahl.md) | Sprint 18 (Netzstrang); Auswahl nach Rolle, sichtbares Angriffsziel mit Nachsetzen über zwei Intents, Formationsausrichtung — Eingabe und Darstellung, kein neuer `CommandKind` |
| [AUFTRAG_Grossblock.md](AUFTRAG_Grossblock.md) | **Der gebündelte Arbeitsauftrag an den ausführenden Agenten, Blöcke 0 bis 4** — Auswahlrahmen, Sprint 16, Sprint 18, Sprint 14, Sprint 17 Paket A. Er ist die verbindliche Reihenfolge; wo er von einer Sprintdatei abweicht, gilt er |
@@ -186,8 +186,8 @@ Masterplans grundlegend — sonst nichts.
([AUFTRAG_Grossblock.md](AUFTRAG_Grossblock.md)), fünf Blöcke am Stück:
- **Block 0 — Auswahlrahmen** (#49): eine Konstante in
`GroundMarkerVisuals.cs`, Wirkung sofort sichtbar.
- - **Block 1 — [Sprint 16](16_Sprint_Wirtschaft.md):** die Wirtschaft trägt
- sich selbst.
+ - **Block 1 — [Sprint 16](16_Sprint_Wirtschaft.md): technisch umgesetzt.**
+ Die Wirtschaft trägt sich selbst; die manuelle Spielabnahme bleibt offen.
- **Block 2 — [Sprint 18](18_Sprint_Befehl_und_Auswahl.md):** Befehl und
Auswahl werden lesbar.
- **Block 3 — [Sprint 14](14_Sprint_Lobby.md):** Match per Code über
@@ -198,11 +198,11 @@ Masterplans grundlegend — sonst nichts.
`.partial`-Leck in `Assets/_Project/Scripts/Networking/RelayServerCore.cs`.
`ResetMatch` verwirft den Aufzeichnungsstrom und merkt sich den Pfad,
löscht die Datei aber nie. Paket B wartet weiterhin auf Sprint 15.
-4. **Alles, was `SimDefinitions` anfasst, liegt vor dem VPS-Rollout.** Das sind
- in Sprint 16 die Pakete **16.7** (Feldwerte und Feldanzahl) und **16.8**
- (Bauvoraussetzungs-Bitmaske). Der Relay vergleicht den Definitions-Hash
- **serverseitig**; nach dem Rollout kostet dieselbe Zahlenänderung einen
- Serverzugang und einen Redeploy
+4. **Die `SimDefinitions`-Pakete wurden vor dem VPS-Rollout integriert.**
+ Sprint 16.7 (Feldwerte und Feldanzahl) und 16.8
+ (Bauvoraussetzungs-Bitmaske) sind abgeschlossen. Der Relay vergleicht den
+ Definitions-Hash **serverseitig**; spätere Zahlenänderungen kosten deshalb
+ Serverzugang und Redeploy
([13-15_Parallelbetrieb.md](13-15_Parallelbetrieb.md), §Definitions-Hash).
5. **Sprint 19 bleibt danach und ist kein Codeauftrag.** Die beiden Art-Befunde
#57 (Gebäude durchsichtig und hohl) und #58 (Maßstab des Radarturms) sind
@@ -219,3 +219,4 @@ Masterplans grundlegend — sonst nichts.
| 0.7.0 | 2026-08-09 | Nach der Inhaberentscheidung zum ersten Betatest: **Sprint 16 vorgezogen**, [16_Sprint_Wirtschaft.md](16_Sprint_Wirtschaft.md) und [18_Sprint_Befehl_und_Auswahl.md](18_Sprint_Befehl_und_Auswahl.md) in die Mappe aufgenommen, Regelwerk als Parallelbetrieb 13–18 mit Trennung über Dateihoheit (**D-095**) fortgeschrieben, Großauftrag mit den Blöcken 0 bis 4 eingehängt. „Nächste Schritte" neu geschrieben: Sprint 13 halb gemergt (`e15f5e6`), 13.2–13.5 nicht durch einen Agenten erledigbar, `SimDefinitions`-Pakete vor dem VPS-Rollout. Punkt 2 der Fünf-Sätze-Zusammenfassung nach **D-077** berichtigt — die Skirmish-KI ist registriert und spielt | Orchestrator |
| 0.7.1 | 2026-08-09 | Index gegen den Großauftrag und die Sprintdateien nachgezogen: Sprint 17 Paket A ist nicht „sofort vorziehbar", sondern läuft als **Block 3 hinter der Lobby**, die seit `b4e75e5` auf `main` gebaut ist, und es berührt mit dem `.partial`-Fix in `RelayServerCore.cs` genau **eine** Datei unter `Assets/`. Sprint 16 trägt **acht** Betatest-Befunde (#43, #44, #45, #46, #47, #48, #53, #54), nicht sechs. Alle Markdown-Links dieser Datei gegen das Dateisystem geprüft — kein toter Link | Orchestrator |
| 0.7.2 | 2026-08-10 | D-105 nachgezogen: alleinige Projektleitung, sichtbare Spielabnahme-Zurückstellung und den historischen Charakter des D-091-Zwei-Maintainer-Rollouts im Index kenntlich gemacht | Project Owner / Orchestrator |
+| 0.8.0 | 2026-08-10 | Sprint 16 als technisch umgesetzt markiert, Paket 16.10 und die vor dem VPS-Rollout integrierten Definitionsänderungen nachgezogen; manuelle Spielabnahme bleibt ausdrücklich offen | Codex / Dennis Westermann |
diff --git a/tools/Nova.SimRunner.Tests/KernelIntegrationTests.cs b/tools/Nova.SimRunner.Tests/KernelIntegrationTests.cs
index a129736..7fa2300 100644
--- a/tools/Nova.SimRunner.Tests/KernelIntegrationTests.cs
+++ b/tools/Nova.SimRunner.Tests/KernelIntegrationTests.cs
@@ -153,6 +153,35 @@ public void SealedMoveCommand_ChangesUnitStateAtTargetTick()
"the ordered unit must actually move after the target tick");
}
+ [Test]
+ public void SealedStopCommand_ClearsMovementAndAttackTarget_WithoutCombatSystem()
+ {
+ var host = TestHost.Create(Seed);
+ EntityId unit = host.Entities.SpawnUnit(
+ 0, new Transform2D(SimFixed.FromFloat(10.5f), SimFixed.FromFloat(10.5f)), SimFixed.FromInt(5));
+ EntityId target = host.Entities.SpawnUnit(
+ 1, new Transform2D(SimFixed.FromFloat(20.5f), SimFixed.FromFloat(20.5f)), SimFixed.FromInt(5));
+
+ ref UnitState state = ref host.Entities.GetUnitRef(unit);
+ state.SetTarget(new GridPos2D(30, 30));
+ state.AttackTarget = target;
+
+ var stop = new StopPayload(new[] { UnitCommandStateView.ToRawEntityId(unit) });
+ Assert.That(
+ host.Ingress.TrySubmitIntent(CommandIntent.Create(stop), out _),
+ Is.EqualTo(CommandIngressResult.Accepted));
+
+ host.StepTick();
+
+ Assert.That(host.Kernel.LastTickResults.Count, Is.EqualTo(1));
+ Assert.That(host.Kernel.LastTickResults[0].Code, Is.EqualTo(CommandResultCode.Applied));
+ ref readonly UnitState stopped = ref host.Entities.GetUnitRef(unit);
+ Assert.That(stopped.IsMoving, Is.False);
+ Assert.That(stopped.TargetGridPos.IsValid, Is.False);
+ Assert.That(stopped.GoalGridPos.IsValid, Is.False);
+ Assert.That(stopped.AttackTarget, Is.EqualTo(EntityId.Invalid));
+ }
+
[Test]
public void StateHash_ReflectsStateMutation_AndStaysStableOnRepeat()
{