From 6decf2a9d23abfc9bd0157296b4489331444fbd8 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 14 May 2026 10:26:56 +0200 Subject: [PATCH] Align frontend pulse contract and tasks --- TASKS.md | 78 +++++++++---------- .../Controls/ForecastList.cs | 2 +- .../Controls/GridViewport.cs | 6 ++ .../Data/GameSession.cs | 19 ++--- .../Data/Levels/black_start.json | 4 +- .../Data/Levels/coolant_restart.json | 4 +- .../Data/Levels/fuel_bleed.json | 4 +- .../Screens/LevelScreen.cs | 10 +-- .../SimulationEngine.cs | 12 --- src/ReactorMaintenance.Win2D/MainWindow.xaml | 4 +- .../MainWindow.xaml.cs | 34 ++++++-- .../LevelEditorTests.cs | 25 ++++++ .../SimulationEngineTests.cs | 17 +++- 13 files changed, 129 insertions(+), 90 deletions(-) diff --git a/TASKS.md b/TASKS.md index 993800b..e43981f 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,112 +1,110 @@ -# Reactor Maintenance Tasks +# Reactor Maintenance Tasks This backlog tracks what must change so the implementation matches `docs/design.md`, `docs/UX.md`, and `docs/CAMPAIGN.md`. ## Audit Snapshot -- Current simulation tests pass: `33/33` in `tests/ReactorMaintenance.Simulation.Tests`. -- Existing tests cover older behavior. They are useful regression scaffolding, but they do not prove the latest design rules. -- The simulation has a working `SimulationEngine`, network propagation, consumers, structural integrity, leaks, reactor readiness, forecasts, serialization, editor helpers, and Godot session bridge. -- Godot currently has a usable UX scaffold and grid renderer, but it still exposes older interaction concepts such as a player-facing `EndTurn` action and always-available underground layer controls. -- Existing campaign data is the older three-level placeholder set: `water_restart.json`, `fuel_bleed.json`, and `black_start.json`. These levels and manifest entries must be replaced by the tutorial plus six-group campaign from `docs/CAMPAIGN.md`. +- Current simulation tests pass: `54/54` in `tests/ReactorMaintenance.Simulation.Tests`. +- Godot has a usable UX scaffold and grid renderer; the full pulse playback, terminal-gated layer controls, and campaign content pass remain in later tasks. +- Existing campaign data is the older placeholder set. These levels and manifest entries must be replaced by the tutorial plus six-group campaign from `docs/CAMPAIGN.md`. - Unrelated Godot metadata or generated `.uid` files are not part of this backlog unless a later implementation task intentionally touches them. ## P0 Simulation Contract -- [ ] Rename the public environment tick model from turn-based `AdvanceTurn`/`EndTurn` semantics to `Pulse` semantics. +- [x] Rename the public environment tick model from turn-based `AdvanceTurn`/`EndTurn` semantics to `Pulse` semantics. - One accepted `LengthyAction` triggers exactly one `Pulse`. - One `Pulse` contains a fixed balance-defined number of deterministic `Step`s. - The fixed step count must not vary by action type, forecast result, or danger level. - Keep isolated step or pulse advancement available only for tests, editor tooling, or debug use. -- [ ] Remove player-facing wait/fast-forward behavior from normal gameplay. +- [x] Remove player-facing wait/fast-forward behavior from normal gameplay. - `MoveRobot`, selection, inspection, and terminal layer viewing remain `QuickAction`s. - `InteractProp`, `InteractLeak`, `ApplyHeatShield`, and accepted powered-prop no-op interactions are `LengthyAction`s. - `ActivateReactor` wins when `ReactorReadiness` is true and must not require a separate wait. -- [ ] Add `IsolationValveProp` as a first-class prop. +- [x] Add `IsolationValveProp` as a first-class prop. - Store carrier binding and `Open`/`Closed` state. - Block intentional underground propagation across the authored branch boundary. - Preserve downstream starvation/readiness consequences for consumers and reactor feed. - Add editor placement, validation, serialization, and rendering support. -- [ ] Add `SprinklerControlProp` and wall-mounted `SprinklerValve`. +- [x] Add `SprinklerControlProp` and wall-mounted `SprinklerValve`. - A `SprinklerControlProp` links to exactly one `SprinklerValve`. - The valve is wall-mounted, not directly interactive, and has exactly one outlet/access floor face. - Discharge creates `Water` only while linked control is `Enabled` and the water branch is fed. - Discharge applies deterministic local water pressure debt. - Add editor placement/linking, validation, serialization, and rendering support. -- [ ] Rework surface water into `Water`. +- [x] Rework surface water into `Water`. - Rename code-facing concepts where practical; otherwise make names and UI text consistently mean water, not a generic hazard. - `water` pipe failures should inject `Water`, not a damaging liquid. - `Water` alone must not cause `UnsafeEntryLoss`. -- [ ] Update leak injection. +- [x] Update leak injection. - Leaks inject only when the underground leak cell has positive amount and positive pressure/voltage after propagation. - Isolating a leak stops fresh injection without repairing the underlying fault. - Repair restores structural integrity and stops future injection but does not clean existing surface values. -- [ ] Implement the approved surface interaction order. +- [x] Implement the approved surface interaction order. - Resolve leak/sprinkler injection per `Step`. - Resolve water mitigation before ignition and electrical spread. - Implement `Dilute`, `Quench`, value-based `Evaporate`, wet-electric `Conduct`, and `Ignite`. - Preserve deterministic same-cell and adjacent-cell delta accumulation. - Closed powered doors and remedy blocks must gate only the interactions they explicitly block. -- [ ] Implement value-based evaporation. +- [x] Implement value-based evaporation. - Add balance values for ambient evaporation, heat-driven evaporation, and evaporation cooling. - Hot cells should evaporate `Water` faster than cold cells. - Evaporation happens during useful action pulses; there is no campaign wait command. -- [ ] Implement `Unsafe` as derived movement safety. +- [x] Implement `Unsafe` as derived movement safety. - `Unsafe` is recalculated after authored setup and after each `Pulse`. - `Unsafe` is caused by unsafe `Heat`, unsafe `LeakedElectricity`, or the wet-electric unsafe rule. - `LeakedFuel` alone and `Water` alone are not `Unsafe`. - `UnsafeEntryLoss` happens only when `MoveRobot` enters an `Unsafe` destination without applicable protection. - A `Pulse` must not kill a stationary robot just because the current cell becomes `Unsafe`. -- [ ] Implement powered prop behavior. +- [x] Implement powered prop behavior. - `DoorProp` and `AllSeeingEyeTerminal` require positive local electricity amount and voltage for their interactions to take effect. - Interacting with an unpowered `PoweredProp` is accepted as a `LengthyAction`, changes no prop state, reveals no terminal information, and still triggers one `Pulse`. - Powered doors keep their last physical open/closed state when power is lost. -- [ ] Gate all-seeing-eye information. +- [x] Gate all-seeing-eye information. - Underground topology, numeric underground values, and `Forecast` output are visible only while the robot is at an active and powered `AllSeeingEyeTerminal`. - Terminal access is local and does not persist after the robot leaves. - Forecasts are systemic simulations over copied state, never authored level prose. -- [ ] Update `ReactorReadiness` checks as the invariant source of victory. +- [x] Update `ReactorReadiness` checks as the invariant source of victory. - Every network present beneath `ReactorControlProp` must have positive amount and intensity. - Required per-carrier consumer counts must be `Enabled` and `Producing`. - Missing readiness blocks `Ready` but does not directly cause `Lost`. ## P0 Simulation Tests -- [ ] Update existing tests so their names and assertions use `Pulse`, `Step`, `Water`, `Unsafe`, and `ReactorReadiness` terminology. -- [ ] Add tests for fixed pulse length. +- [x] Update existing tests so their names and assertions use `Pulse`, `Step`, `Water`, `Unsafe`, and `ReactorReadiness` terminology. +- [x] Add tests for fixed pulse length. - Every accepted `LengthyAction` advances one `Pulse`. - Each pulse resolves the configured number of `Step`s. - `MoveRobot` and inspection-like calls do not resolve a pulse. -- [ ] Add tests for no normal wait/fast-forward dependency. +- [x] Add tests for no normal wait/fast-forward dependency. - Player-facing session/action APIs should not require `EndTurn` to reach readiness after a lengthy action. - Debug/test-only pulse advancement, if retained, must be clearly named and excluded from campaign UI. -- [ ] Add tests for `IsolationValveProp`. +- [x] Add tests for `IsolationValveProp`. - Open valve allows branch propagation. - Closed valve isolates damaged branches and can starve downstream consumers/reactor feed. - Toggling a valve triggers exactly one `Pulse`. -- [ ] Add tests for `SprinklerControlProp` and `SprinklerValve`. +- [x] Add tests for `SprinklerControlProp` and `SprinklerValve`. - Valve discharge requires a linked enabled control and fed water branch. - Direct valve interaction is invalid or unavailable. - Discharge creates `Water` at the authored outlet and applies pressure debt. - Disabling the linked control or isolating the sprinkler branch stops fresh discharge. -- [ ] Add tests for updated leak injection. +- [x] Add tests for updated leak injection. - Fed fuel, water, and electricity leaks inject to the correct access face. - Isolated leaks stop new surface injection. - Repairs restore structural integrity without cleaning existing surface values. -- [ ] Add tests for surface interactions. +- [x] Add tests for surface interactions. - `Water` dilutes `LeakedFuel`. - `Water` quenches `Heat`. - Evaporation removes water and cools heat according to balance values. - Wet cells conduct electricity faster than dry cells. - Fuel plus electricity or heat can ignite and create heat while consuming fuel. - Closed doors block designed surface propagation paths. -- [ ] Add tests for `Unsafe`. +- [x] Add tests for `Unsafe`. - Moving into unsafe heat loses without active heat protection. - Moving into unsafe electricity loses. - Moving into wet-electric unsafe cells loses. - Moving into fuel-only or sprinkler-water-only cells does not lose. - A pulse that makes the robot's current cell unsafe does not immediately lose. -- [ ] Add tests for powered props. +- [x] Add tests for powered props. - Powered door toggles and blocks/unblocks surface propagation. - Unpowered door interaction changes no door state but still triggers a pulse. - Powered terminal interaction enables local visibility and forecasts. @@ -122,19 +120,19 @@ This backlog tracks what must change so the implementation matches `docs/design. ## P1 Editor, Schema, And Level Data -- [ ] Bump the level schema version when adding new prop/link/outlet state. -- [ ] Update serialization round trips for new fields. +- [x] Bump the level schema version when adding new prop/link/outlet state. +- [x] Update serialization round trips for new fields. - `IsolationValveProp` carrier and open/closed state. - `SprinklerControlProp` enabled/disabled state and linked valve id or position. - `SprinklerValve` wall position, outlet/access face, linked control, and water connection. - Powered terminal active state if it becomes serialized runtime state. -- [ ] Update `LevelEditor` tools. +- [x] Update `LevelEditor` tools. - Add isolation valve placement. - Add sprinkler control placement. - Add wall-mounted sprinkler valve placement and outlet/access cycling. - Keep electricity leak wall access cycling. - Prevent invalid prop placement on walls except designed wall-mounted sprinkler valves. -- [ ] Update `LevelValidator`. +- [x] Update `LevelValidator`. - Validate powered doors have valid geometry and local electricity. - Validate terminal power requirements where needed. - Validate wall-mounted sprinkler valve geometry, outlet/access face, water connection, and exactly one linked control. @@ -145,16 +143,16 @@ This backlog tracks what must change so the implementation matches `docs/design. - Update `default_campaign_manifest.json` to the final order. - Remove or demote old placeholder levels so they are not presented as campaign content. - Add stable ids and short flavor text for every authored level. -- [ ] Add test/build helpers for level construction. +- [x] Add test/build helpers for level construction. - Prefer shared builders for linear networks, forks, leaks, wall electricity faces, doors, controls, consumers, and reactors. - Avoid duplicating low-level array setup across tests. ## P1 Godot UX Integration -- [ ] Remove player-facing `End Turn` from the normal action bar. +- [x] Remove player-facing `End Turn` from the normal action bar. - Replace it with contextual `LengthyAction` commands and `ActivateReactor` when ready. - Keep any debug pulse control behind an explicit development-only path. -- [ ] Update `GameSession` API names and events to match pulse semantics. +- [x] Update `GameSession` API names and events to match pulse semantics. - Use `PulseAdvanced` or equivalent instead of `TurnAdvanced`. - Ensure accepted no-op powered-prop interactions still notify pulse playback. - Ensure refused invalid actions do not mutate state or trigger pulse playback. @@ -186,9 +184,9 @@ This backlog tracks what must change so the implementation matches `docs/design. ## Verification Rules -- [ ] After each implementation iteration, run the focused simulation tests that cover the changed system. -- [ ] Run full `dotnet test` before committing simulation or serializer changes. -- [ ] For code changes on Windows, run the repo-required CRLF normalization and cleanup steps after implementation. -- [ ] For documentation-only iterations, no cleanup pass is required. -- [ ] Keep documentation aligned whenever code changes terminology or behavior. -- [ ] Commit each completed iteration with a brief summary. +- [x] After each implementation iteration, run the focused simulation tests that cover the changed system. +- [x] Run full `dotnet test` before committing simulation or serializer changes. +- [x] For code changes on Windows, run the repo-required CRLF normalization and cleanup steps after implementation. +- [x] For documentation-only iterations, no cleanup pass is required. +- [x] Keep documentation aligned whenever code changes terminology or behavior. +- [x] Commit each completed iteration with a brief summary. diff --git a/src/ReactorMaintenance.Godot/Controls/ForecastList.cs b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs index b490a7f..fbcf851 100644 --- a/src/ReactorMaintenance.Godot/Controls/ForecastList.cs +++ b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs @@ -32,7 +32,7 @@ public partial class ForecastList : PanelContainer { var pos = forecast.Position; var posStr = pos != null ? $" [{pos.X},{pos.Y}]" : ""; - return $"Turn +{forecast.Turns}: {forecast.Message}{posStr}"; + return $"Pulse +{forecast.Turns}: {forecast.Message}{posStr}"; } private static void ApplyForecastColor(Label label, EForecastKind kind) diff --git a/src/ReactorMaintenance.Godot/Controls/GridViewport.cs b/src/ReactorMaintenance.Godot/Controls/GridViewport.cs index 29fe8c3..2eb0ab0 100644 --- a/src/ReactorMaintenance.Godot/Controls/GridViewport.cs +++ b/src/ReactorMaintenance.Godot/Controls/GridViewport.cs @@ -514,6 +514,9 @@ public partial class GridViewport : Control EPropType.Consumer => new(0.36f, 0.48f, 0.67f), EPropType.Junction => new(0.56f, 0.44f, 0.70f), EPropType.AllSeeingEyeTerminal => new(0.33f, 0.59f, 0.61f), + EPropType.IsolationValve => CarrierColor(prop.Carrier), + EPropType.SprinklerControl => new(0.21f, 0.59f, 0.73f), + EPropType.SprinklerValve => new(0.15f, 0.44f, 0.59f), EPropType.RemedySupply => new(0.30f, 0.57f, 0.34f), EPropType.ReactorControl => new(0.69f, 0.28f, 0.29f), _ => Colors.Gray @@ -527,6 +530,9 @@ public partial class GridViewport : Control EPropType.Consumer => "CON", EPropType.Junction => $"J {prop.JunctionMode}", EPropType.AllSeeingEyeTerminal => "EYE", + EPropType.IsolationValve => prop.IsOpen ? "V OPEN" : "V CLOSED", + EPropType.SprinklerControl => prop.IsEnabled ? "SPR ON" : "SPR OFF", + EPropType.SprinklerValve => "SPR", EPropType.RemedySupply => RemedyShort(prop.RemedyType), EPropType.ReactorControl => "REACT", _ => string.Empty diff --git a/src/ReactorMaintenance.Godot/Data/GameSession.cs b/src/ReactorMaintenance.Godot/Data/GameSession.cs index 23d6d5c..458cf7c 100644 --- a/src/ReactorMaintenance.Godot/Data/GameSession.cs +++ b/src/ReactorMaintenance.Godot/Data/GameSession.cs @@ -6,7 +6,7 @@ public sealed class GameSession { public event StateChangedHandler? LevelStateChanged; public event StateChangedHandler? RobotMoved; - public event StateChangedHandler? TurnAdvanced; + public event StateChangedHandler? PulseAdvanced; public event StateChangedHandler? LevelWon; public event StateChangedHandler? LevelLost; @@ -31,20 +31,13 @@ public sealed class GameSession return true; } - public LevelState EndTurn() - { - LevelState = m_Engine.EndTurn(LevelState); - OnTurnAdvanced(); - return LevelState; - } - public bool InteractProp() { if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won) return false; LevelState = m_Engine.InteractProp(LevelState); - OnTurnAdvanced(); + OnPulseAdvanced(); return true; } @@ -54,7 +47,7 @@ public sealed class GameSession return false; LevelState = m_Engine.InteractLeak(LevelState, carrier, useRemedy); - OnTurnAdvanced(); + OnPulseAdvanced(); return true; } @@ -64,7 +57,7 @@ public sealed class GameSession return false; LevelState = m_Engine.ApplyHeatShield(LevelState); - OnTurnAdvanced(); + OnPulseAdvanced(); return true; } @@ -89,10 +82,10 @@ public sealed class GameSession LevelStateChanged?.Invoke(this); } - private void OnTurnAdvanced() + private void OnPulseAdvanced() { CheckOutcome(); - TurnAdvanced?.Invoke(this); + PulseAdvanced?.Invoke(this); } private void CheckOutcome() diff --git a/src/ReactorMaintenance.Godot/Data/Levels/black_start.json b/src/ReactorMaintenance.Godot/Data/Levels/black_start.json index ef7cdd1..77a1c4f 100644 --- a/src/ReactorMaintenance.Godot/Data/Levels/black_start.json +++ b/src/ReactorMaintenance.Godot/Data/Levels/black_start.json @@ -1,5 +1,5 @@ { - "Version": 3, + "Version": 4, "Level": { "Name": "Black Start", "Width": 10, @@ -3971,4 +3971,4 @@ }, "Forecasts": [] } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json b/src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json index 3683b27..a55fd73 100644 --- a/src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json +++ b/src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json @@ -1,5 +1,5 @@ { - "Version": 3, + "Version": 4, "Level": { "Name": "Coolant Restart", "Width": 8, @@ -2795,4 +2795,4 @@ }, "Forecasts": [] } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json b/src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json index 4f4c0e0..f0e90ab 100644 --- a/src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json +++ b/src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json @@ -1,5 +1,5 @@ { - "Version": 3, + "Version": 4, "Level": { "Name": "Fuel Bleed", "Width": 8, @@ -2808,4 +2808,4 @@ }, "Forecasts": [] } -} \ No newline at end of file +} diff --git a/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs index 104286f..e3b74af 100644 --- a/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs +++ b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs @@ -56,7 +56,7 @@ public partial class LevelScreen : ScreenBase UpdateUI(); m_Session.LevelStateChanged += OnLevelStateChanged; m_Session.RobotMoved += OnRobotMoved; - m_Session.TurnAdvanced += OnTurnAdvanced; + m_Session.PulseAdvanced += OnPulseAdvanced; } private GridViewport CreateGridViewport() @@ -131,7 +131,6 @@ public partial class LevelScreen : ScreenBase actions.AddChild(CreateButton("Move", OnMoveAction, "Move robot to adjacent floor cell")); actions.AddChild(CreateButton("Interact", OnInteractAction, "Interact with prop at robot position")); actions.AddChild(CreateButton("Repair", OnRepairAction, "Repair leak at robot position")); - actions.AddChild(CreateButton("End Turn", OnEndTurnAction)); actions.AddChild(CreateButton("Main Menu", () => m_App?.ShowMainMenu())); return actions; } @@ -168,11 +167,6 @@ public partial class LevelScreen : ScreenBase } } - private void OnEndTurnAction() - { - m_Session?.EndTurn(); - } - private void UpdateUI() { if (m_Session is null || m_Inspector is null || m_ForecastList is null || m_InventoryStrip is null) @@ -228,7 +222,7 @@ public partial class LevelScreen : ScreenBase UpdateUI(); } - private void OnTurnAdvanced(GameSession sender) + private void OnPulseAdvanced(GameSession sender) { UpdateUI(); if (sender.LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won) diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index 31f407a..ec8c9d5 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -37,18 +37,6 @@ public sealed class SimulationEngine return ResolveStep(level); } - [Obsolete("Use AdvancePulseForDebug. Player-facing wait/turn advancement is not part of normal gameplay.")] - public LevelState EndTurn(LevelState level) - { - return AdvancePulseForDebug(level); - } - - [Obsolete("Use AdvancePulseForDebug. Player-facing wait/turn advancement is not part of normal gameplay.")] - public LevelState AdvanceTurn(LevelState level) - { - return AdvancePulseForDebug(level); - } - public IReadOnlyList Forecast(LevelState level) { if (!level.HasActivePoweredTerminalAccess()) diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml index 48750f4..ad5257d 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml @@ -16,7 +16,7 @@ - + @@ -85,7 +85,7 @@ - + diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index 913af90..35719c4 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -217,9 +217,9 @@ public sealed partial class MainWindow } } - private void EndTurn_Click(object sender, RoutedEventArgs e) + private void DebugPulse_Click(object sender, RoutedEventArgs e) { - RunSimulationStep(); + RunDebugPulse(); } private void PlayPause_Click(object sender, RoutedEventArgs e) @@ -232,7 +232,7 @@ public sealed partial class MainWindow private void SimulationTimer_Tick(object? sender, object e) { - RunSimulationStep(); + RunDebugPulse(); } private void StartSimulationTimer() @@ -254,9 +254,9 @@ public sealed partial class MainWindow PlayPauseButton.Label = isPlaying ? "Pause" : "Play"; } - private void RunSimulationStep() + private void RunDebugPulse() { - m_Level = m_Simulation.EndTurn(m_Level); + m_Level = m_Simulation.AdvancePulseForDebug(m_Level); RefreshInspector(); LevelCanvas.Invalidate(); } @@ -878,7 +878,7 @@ public sealed partial class MainWindow private void RefreshInspector() { LevelNameText.Text = m_Level.Name; - TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture); + TurnText.Text = m_Level.Global.Pulse.ToString(CultureInfo.InvariantCulture); StatusText.Text = string.IsNullOrWhiteSpace(m_EditorFeedback) ? $"{m_Level.Global.LevelState}: {m_Level.Global.Status}" : $"{m_Level.Global.LevelState}: {m_EditorFeedback}"; @@ -1152,6 +1152,9 @@ public sealed partial class MainWindow EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178), EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55), EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156), + EPropType.IsolationValve => CarrierColor(prop.Carrier), + EPropType.SprinklerControl => ColorHelper.FromArgb(255, 54, 150, 186), + EPropType.SprinklerValve => ColorHelper.FromArgb(255, 38, 112, 150), EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86), EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73), _ => Colors.Gray @@ -1178,6 +1181,9 @@ public sealed partial class MainWindow EEditorTool.Junction => EPropType.Junction, EEditorTool.Door => EPropType.Door, EEditorTool.AllSeeingEyeTerminal => EPropType.AllSeeingEyeTerminal, + EEditorTool.IsolationValve => EPropType.IsolationValve, + EEditorTool.SprinklerControl => EPropType.SprinklerControl, + EEditorTool.SprinklerValve => EPropType.SprinklerValve, EEditorTool.RemedySupply => EPropType.RemedySupply, EEditorTool.ReactorControl => EPropType.ReactorControl, _ => EPropType.None @@ -1192,6 +1198,9 @@ public sealed partial class MainWindow EPropType.Junction => "prop-junction", EPropType.Door => "prop-door", EPropType.AllSeeingEyeTerminal => "prop-eye-terminal", + EPropType.IsolationValve => "prop-isolation-valve", + EPropType.SprinklerControl => "prop-sprinkler-control", + EPropType.SprinklerValve => "prop-sprinkler-valve", EPropType.RemedySupply => "prop-remedy", EPropType.ReactorControl => "prop-reactor", _ => "prop" @@ -1231,6 +1240,9 @@ public sealed partial class MainWindow EPropType.Junction => $"J {prop.JunctionMode}", EPropType.Door => "DOOR", EPropType.AllSeeingEyeTerminal => "EYE", + EPropType.IsolationValve => prop.IsOpen ? "V OPEN" : "V CLOSED", + EPropType.SprinklerControl => prop.IsEnabled ? "SPR ON" : "SPR OFF", + EPropType.SprinklerValve => "SPR", EPropType.RemedySupply => RemedyShort(prop.RemedyType), EPropType.ReactorControl => "REACT", _ => string.Empty @@ -1300,10 +1312,15 @@ public sealed partial class MainWindow CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"), CarrierTool(EEditorTool.Flow, ECarrierType.Water, "Water Source"), CarrierTool(EEditorTool.Flow, ECarrierType.Electricity, "Electric Source"), + CarrierTool(EEditorTool.IsolationValve, ECarrierType.Fuel, "Fuel Valve"), + CarrierTool(EEditorTool.IsolationValve, ECarrierType.Water, "Water Valve"), + CarrierTool(EEditorTool.IsolationValve, ECarrierType.Electricity, "Electric Valve"), Tool(EEditorTool.Consumer, "Consumer"), Tool(EEditorTool.Junction, "Junction"), Tool(EEditorTool.Door, "Door"), Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"), + Tool(EEditorTool.SprinklerControl, "Sprinkler Control"), + Tool(EEditorTool.SprinklerValve, "Sprinkler Valve"), RemedyTool(ERemedyType.FuelNeutralizer, "Fuel Remedy"), RemedyTool(ERemedyType.WaterNeutralizer, "Water Remedy"), RemedyTool(ERemedyType.ElectricityNeutralizer, "Electric Remedy"), @@ -1333,6 +1350,9 @@ public sealed partial class MainWindow or EEditorTool.Junction or EEditorTool.Door or EEditorTool.AllSeeingEyeTerminal + or EEditorTool.IsolationValve + or EEditorTool.SprinklerControl + or EEditorTool.SprinklerValve or EEditorTool.RemedySupply or EEditorTool.ReactorControl or EEditorTool.SurfaceHazard @@ -1341,7 +1361,7 @@ public sealed partial class MainWindow } var activeCarrier = LayerCarrier(m_ActiveLayer); - return command.Carrier == activeCarrier && command.Tool is EEditorTool.Underground or EEditorTool.Flow or EEditorTool.Leak; + return command.Carrier == activeCarrier && command.Tool is EEditorTool.Underground or EEditorTool.Flow or EEditorTool.Leak or EEditorTool.IsolationValve; } private void RefreshForecasts() diff --git a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs index d0761c5..875760e 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs @@ -70,6 +70,31 @@ public sealed class LevelEditorTests Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).ElectricityServiceState); } + [Fact] + public void IsolationValveToolPlacesCarrierBoundValve() + { + var level = LevelState.Create("Valve editor", 6, 6); + + var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.IsolationValve, Carrier = ECarrierType.Fuel }); + + Assert.Equal(EPropType.IsolationValve, next.GetProp(new(2, 2)).Type); + Assert.Equal(ECarrierType.Fuel, next.GetProp(new(2, 2)).Carrier); + } + + [Fact] + public void SprinklerToolsPlaceFloorControlAndWallValve() + { + var level = LevelState.Create("Sprinkler editor", 6, 6); + level = level.SetTerrain(new(2, 2), ECellTerrain.Wall); + + var control = LevelEditor.Apply(level, new(1, 2), new() { Tool = EEditorTool.SprinklerControl }); + var valve = LevelEditor.Apply(control, new(2, 2), new() { Tool = EEditorTool.SprinklerValve }); + + Assert.Equal(EPropType.SprinklerControl, valve.GetProp(new(1, 2)).Type); + Assert.Equal(EPropType.SprinklerValve, valve.GetProp(new(2, 2)).Type); + Assert.Equal(new(2, 1), valve.GetProp(new(2, 2)).OutletPosition); + } + [Fact] public void ElectricityLeakUsesAuthoredWallAccessFace() { diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index bbf58ab..095660d 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -67,7 +67,7 @@ public sealed class SimulationEngineTests var next = m_Engine.MoveRobot(level, new(2, 1)); Assert.Equal(new(2, 1), next.Robot.Position); - Assert.Equal(0, next.Global.Turn); + Assert.Equal(0, next.Global.Pulse); } [Fact] @@ -434,6 +434,21 @@ public sealed class SimulationEngineTests Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition); } + [Fact] + public void LevelSerializationRoundTripsValveAndSprinklerFields() + { + var level = SprinklerLevel(EPropSwitchState.Disabled); + level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact }); + level = level.SetProp(new(3, 3), new() { Type = EPropType.IsolationValve, Carrier = ECarrierType.Fuel, SwitchState = EPropSwitchState.Disabled }); + + var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level)); + + Assert.Equal(EPropType.IsolationValve, loaded.GetProp(new(3, 3)).Type); + Assert.Equal(EPropSwitchState.Disabled, loaded.GetProp(new(3, 3)).SwitchState); + Assert.Equal(new(2, 1), loaded.GetProp(new(3, 2)).LinkedPosition); + Assert.Equal(new(2, 2), loaded.GetProp(new(2, 1)).OutletPosition); + } + [Fact] public void LevelSerializationRejectsOldSchema() {