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