Align frontend pulse contract and tasks

This commit is contained in:
2026-05-14 10:26:56 +02:00
parent 830c7aef14
commit 6decf2a9d2
13 changed files with 129 additions and 90 deletions

View File

@@ -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`. This backlog tracks what must change so the implementation matches `docs/design.md`, `docs/UX.md`, and `docs/CAMPAIGN.md`.
## Audit Snapshot ## Audit Snapshot
- Current simulation tests pass: `33/33` in `tests/ReactorMaintenance.Simulation.Tests`. - Current simulation tests pass: `54/54` in `tests/ReactorMaintenance.Simulation.Tests`.
- Existing tests cover older behavior. They are useful regression scaffolding, but they do not prove the latest design rules. - 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.
- The simulation has a working `SimulationEngine`, network propagation, consumers, structural integrity, leaks, reactor readiness, forecasts, serialization, editor helpers, and Godot session bridge. - 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`.
- 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`.
- Unrelated Godot metadata or generated `.uid` files are not part of this backlog unless a later implementation task intentionally touches them. - Unrelated Godot metadata or generated `.uid` files are not part of this backlog unless a later implementation task intentionally touches them.
## P0 Simulation Contract ## 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 accepted `LengthyAction` triggers exactly one `Pulse`.
- One `Pulse` contains a fixed balance-defined number of deterministic `Step`s. - 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. - 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. - 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. - `MoveRobot`, selection, inspection, and terminal layer viewing remain `QuickAction`s.
- `InteractProp`, `InteractLeak`, `ApplyHeatShield`, and accepted powered-prop no-op interactions are `LengthyAction`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. - `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. - Store carrier binding and `Open`/`Closed` state.
- Block intentional underground propagation across the authored branch boundary. - Block intentional underground propagation across the authored branch boundary.
- Preserve downstream starvation/readiness consequences for consumers and reactor feed. - Preserve downstream starvation/readiness consequences for consumers and reactor feed.
- Add editor placement, validation, serialization, and rendering support. - 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`. - A `SprinklerControlProp` links to exactly one `SprinklerValve`.
- The valve is wall-mounted, not directly interactive, and has exactly one outlet/access floor face. - 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 creates `Water` only while linked control is `Enabled` and the water branch is fed.
- Discharge applies deterministic local water pressure debt. - Discharge applies deterministic local water pressure debt.
- Add editor placement/linking, validation, serialization, and rendering support. - 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. - 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` pipe failures should inject `Water`, not a damaging liquid.
- `Water` alone must not cause `UnsafeEntryLoss`. - `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. - 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. - 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. - 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 leak/sprinkler injection per `Step`.
- Resolve water mitigation before ignition and electrical spread. - Resolve water mitigation before ignition and electrical spread.
- Implement `Dilute`, `Quench`, value-based `Evaporate`, wet-electric `Conduct`, and `Ignite`. - Implement `Dilute`, `Quench`, value-based `Evaporate`, wet-electric `Conduct`, and `Ignite`.
- Preserve deterministic same-cell and adjacent-cell delta accumulation. - Preserve deterministic same-cell and adjacent-cell delta accumulation.
- Closed powered doors and remedy blocks must gate only the interactions they explicitly block. - 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. - Add balance values for ambient evaporation, heat-driven evaporation, and evaporation cooling.
- Hot cells should evaporate `Water` faster than cold cells. - Hot cells should evaporate `Water` faster than cold cells.
- Evaporation happens during useful action pulses; there is no campaign wait command. - 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 recalculated after authored setup and after each `Pulse`.
- `Unsafe` is caused by unsafe `Heat`, unsafe `LeakedElectricity`, or the wet-electric unsafe rule. - `Unsafe` is caused by unsafe `Heat`, unsafe `LeakedElectricity`, or the wet-electric unsafe rule.
- `LeakedFuel` alone and `Water` alone are not `Unsafe`. - `LeakedFuel` alone and `Water` alone are not `Unsafe`.
- `UnsafeEntryLoss` happens only when `MoveRobot` enters an `Unsafe` destination without applicable protection. - `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`. - 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. - `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`. - 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. - 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`. - 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. - Terminal access is local and does not persist after the robot leaves.
- Forecasts are systemic simulations over copied state, never authored level prose. - 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. - Every network present beneath `ReactorControlProp` must have positive amount and intensity.
- Required per-carrier consumer counts must be `Enabled` and `Producing`. - Required per-carrier consumer counts must be `Enabled` and `Producing`.
- Missing readiness blocks `Ready` but does not directly cause `Lost`. - Missing readiness blocks `Ready` but does not directly cause `Lost`.
## P0 Simulation Tests ## P0 Simulation Tests
- [ ] Update existing tests so their names and assertions use `Pulse`, `Step`, `Water`, `Unsafe`, and `ReactorReadiness` terminology. - [x] Update existing tests so their names and assertions use `Pulse`, `Step`, `Water`, `Unsafe`, and `ReactorReadiness` terminology.
- [ ] Add tests for fixed pulse length. - [x] Add tests for fixed pulse length.
- Every accepted `LengthyAction` advances one `Pulse`. - Every accepted `LengthyAction` advances one `Pulse`.
- Each pulse resolves the configured number of `Step`s. - Each pulse resolves the configured number of `Step`s.
- `MoveRobot` and inspection-like calls do not resolve a pulse. - `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. - 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. - 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. - Open valve allows branch propagation.
- Closed valve isolates damaged branches and can starve downstream consumers/reactor feed. - Closed valve isolates damaged branches and can starve downstream consumers/reactor feed.
- Toggling a valve triggers exactly one `Pulse`. - 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. - Valve discharge requires a linked enabled control and fed water branch.
- Direct valve interaction is invalid or unavailable. - Direct valve interaction is invalid or unavailable.
- Discharge creates `Water` at the authored outlet and applies pressure debt. - Discharge creates `Water` at the authored outlet and applies pressure debt.
- Disabling the linked control or isolating the sprinkler branch stops fresh discharge. - 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. - Fed fuel, water, and electricity leaks inject to the correct access face.
- Isolated leaks stop new surface injection. - Isolated leaks stop new surface injection.
- Repairs restore structural integrity without cleaning existing surface values. - Repairs restore structural integrity without cleaning existing surface values.
- [ ] Add tests for surface interactions. - [x] Add tests for surface interactions.
- `Water` dilutes `LeakedFuel`. - `Water` dilutes `LeakedFuel`.
- `Water` quenches `Heat`. - `Water` quenches `Heat`.
- Evaporation removes water and cools heat according to balance values. - Evaporation removes water and cools heat according to balance values.
- Wet cells conduct electricity faster than dry cells. - Wet cells conduct electricity faster than dry cells.
- Fuel plus electricity or heat can ignite and create heat while consuming fuel. - Fuel plus electricity or heat can ignite and create heat while consuming fuel.
- Closed doors block designed surface propagation paths. - 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 heat loses without active heat protection.
- Moving into unsafe electricity loses. - Moving into unsafe electricity loses.
- Moving into wet-electric unsafe cells loses. - Moving into wet-electric unsafe cells loses.
- Moving into fuel-only or sprinkler-water-only cells does not lose. - 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. - 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. - Powered door toggles and blocks/unblocks surface propagation.
- Unpowered door interaction changes no door state but still triggers a pulse. - Unpowered door interaction changes no door state but still triggers a pulse.
- Powered terminal interaction enables local visibility and forecasts. - 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 ## P1 Editor, Schema, And Level Data
- [ ] Bump the level schema version when adding new prop/link/outlet state. - [x] Bump the level schema version when adding new prop/link/outlet state.
- [ ] Update serialization round trips for new fields. - [x] Update serialization round trips for new fields.
- `IsolationValveProp` carrier and open/closed state. - `IsolationValveProp` carrier and open/closed state.
- `SprinklerControlProp` enabled/disabled state and linked valve id or position. - `SprinklerControlProp` enabled/disabled state and linked valve id or position.
- `SprinklerValve` wall position, outlet/access face, linked control, and water connection. - `SprinklerValve` wall position, outlet/access face, linked control, and water connection.
- Powered terminal active state if it becomes serialized runtime state. - Powered terminal active state if it becomes serialized runtime state.
- [ ] Update `LevelEditor` tools. - [x] Update `LevelEditor` tools.
- Add isolation valve placement. - Add isolation valve placement.
- Add sprinkler control placement. - Add sprinkler control placement.
- Add wall-mounted sprinkler valve placement and outlet/access cycling. - Add wall-mounted sprinkler valve placement and outlet/access cycling.
- Keep electricity leak wall access cycling. - Keep electricity leak wall access cycling.
- Prevent invalid prop placement on walls except designed wall-mounted sprinkler valves. - 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 powered doors have valid geometry and local electricity.
- Validate terminal power requirements where needed. - Validate terminal power requirements where needed.
- Validate wall-mounted sprinkler valve geometry, outlet/access face, water connection, and exactly one linked control. - 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. - Update `default_campaign_manifest.json` to the final order.
- Remove or demote old placeholder levels so they are not presented as campaign content. - 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 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. - Prefer shared builders for linear networks, forks, leaks, wall electricity faces, doors, controls, consumers, and reactors.
- Avoid duplicating low-level array setup across tests. - Avoid duplicating low-level array setup across tests.
## P1 Godot UX Integration ## 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. - Replace it with contextual `LengthyAction` commands and `ActivateReactor` when ready.
- Keep any debug pulse control behind an explicit development-only path. - 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`. - Use `PulseAdvanced` or equivalent instead of `TurnAdvanced`.
- Ensure accepted no-op powered-prop interactions still notify pulse playback. - Ensure accepted no-op powered-prop interactions still notify pulse playback.
- Ensure refused invalid actions do not mutate state or trigger 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 ## Verification Rules
- [ ] After each implementation iteration, run the focused simulation tests that cover the changed system. - [x] After each implementation iteration, run the focused simulation tests that cover the changed system.
- [ ] Run full `dotnet test` before committing simulation or serializer changes. - [x] 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. - [x] 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. - [x] For documentation-only iterations, no cleanup pass is required.
- [ ] Keep documentation aligned whenever code changes terminology or behavior. - [x] Keep documentation aligned whenever code changes terminology or behavior.
- [ ] Commit each completed iteration with a brief summary. - [x] Commit each completed iteration with a brief summary.

View File

@@ -32,7 +32,7 @@ public partial class ForecastList : PanelContainer
{ {
var pos = forecast.Position; var pos = forecast.Position;
var posStr = pos != null ? $" [{pos.X},{pos.Y}]" : ""; 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) private static void ApplyForecastColor(Label label, EForecastKind kind)

View File

@@ -514,6 +514,9 @@ public partial class GridViewport : Control
EPropType.Consumer => new(0.36f, 0.48f, 0.67f), EPropType.Consumer => new(0.36f, 0.48f, 0.67f),
EPropType.Junction => new(0.56f, 0.44f, 0.70f), EPropType.Junction => new(0.56f, 0.44f, 0.70f),
EPropType.AllSeeingEyeTerminal => new(0.33f, 0.59f, 0.61f), 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.RemedySupply => new(0.30f, 0.57f, 0.34f),
EPropType.ReactorControl => new(0.69f, 0.28f, 0.29f), EPropType.ReactorControl => new(0.69f, 0.28f, 0.29f),
_ => Colors.Gray _ => Colors.Gray
@@ -527,6 +530,9 @@ public partial class GridViewport : Control
EPropType.Consumer => "CON", EPropType.Consumer => "CON",
EPropType.Junction => $"J {prop.JunctionMode}", EPropType.Junction => $"J {prop.JunctionMode}",
EPropType.AllSeeingEyeTerminal => "EYE", 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.RemedySupply => RemedyShort(prop.RemedyType),
EPropType.ReactorControl => "REACT", EPropType.ReactorControl => "REACT",
_ => string.Empty _ => string.Empty

View File

@@ -6,7 +6,7 @@ public sealed class GameSession
{ {
public event StateChangedHandler? LevelStateChanged; public event StateChangedHandler? LevelStateChanged;
public event StateChangedHandler? RobotMoved; public event StateChangedHandler? RobotMoved;
public event StateChangedHandler? TurnAdvanced; public event StateChangedHandler? PulseAdvanced;
public event StateChangedHandler? LevelWon; public event StateChangedHandler? LevelWon;
public event StateChangedHandler? LevelLost; public event StateChangedHandler? LevelLost;
@@ -31,20 +31,13 @@ public sealed class GameSession
return true; return true;
} }
public LevelState EndTurn()
{
LevelState = m_Engine.EndTurn(LevelState);
OnTurnAdvanced();
return LevelState;
}
public bool InteractProp() public bool InteractProp()
{ {
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won) if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
return false; return false;
LevelState = m_Engine.InteractProp(LevelState); LevelState = m_Engine.InteractProp(LevelState);
OnTurnAdvanced(); OnPulseAdvanced();
return true; return true;
} }
@@ -54,7 +47,7 @@ public sealed class GameSession
return false; return false;
LevelState = m_Engine.InteractLeak(LevelState, carrier, useRemedy); LevelState = m_Engine.InteractLeak(LevelState, carrier, useRemedy);
OnTurnAdvanced(); OnPulseAdvanced();
return true; return true;
} }
@@ -64,7 +57,7 @@ public sealed class GameSession
return false; return false;
LevelState = m_Engine.ApplyHeatShield(LevelState); LevelState = m_Engine.ApplyHeatShield(LevelState);
OnTurnAdvanced(); OnPulseAdvanced();
return true; return true;
} }
@@ -89,10 +82,10 @@ public sealed class GameSession
LevelStateChanged?.Invoke(this); LevelStateChanged?.Invoke(this);
} }
private void OnTurnAdvanced() private void OnPulseAdvanced()
{ {
CheckOutcome(); CheckOutcome();
TurnAdvanced?.Invoke(this); PulseAdvanced?.Invoke(this);
} }
private void CheckOutcome() private void CheckOutcome()

View File

@@ -1,5 +1,5 @@
{ {
"Version": 3, "Version": 4,
"Level": { "Level": {
"Name": "Black Start", "Name": "Black Start",
"Width": 10, "Width": 10,

View File

@@ -1,5 +1,5 @@
{ {
"Version": 3, "Version": 4,
"Level": { "Level": {
"Name": "Coolant Restart", "Name": "Coolant Restart",
"Width": 8, "Width": 8,

View File

@@ -1,5 +1,5 @@
{ {
"Version": 3, "Version": 4,
"Level": { "Level": {
"Name": "Fuel Bleed", "Name": "Fuel Bleed",
"Width": 8, "Width": 8,

View File

@@ -56,7 +56,7 @@ public partial class LevelScreen : ScreenBase
UpdateUI(); UpdateUI();
m_Session.LevelStateChanged += OnLevelStateChanged; m_Session.LevelStateChanged += OnLevelStateChanged;
m_Session.RobotMoved += OnRobotMoved; m_Session.RobotMoved += OnRobotMoved;
m_Session.TurnAdvanced += OnTurnAdvanced; m_Session.PulseAdvanced += OnPulseAdvanced;
} }
private GridViewport CreateGridViewport() 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("Move", OnMoveAction, "Move robot to adjacent floor cell"));
actions.AddChild(CreateButton("Interact", OnInteractAction, "Interact with prop at robot position")); actions.AddChild(CreateButton("Interact", OnInteractAction, "Interact with prop at robot position"));
actions.AddChild(CreateButton("Repair", OnRepairAction, "Repair leak 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())); actions.AddChild(CreateButton("Main Menu", () => m_App?.ShowMainMenu()));
return actions; return actions;
} }
@@ -168,11 +167,6 @@ public partial class LevelScreen : ScreenBase
} }
} }
private void OnEndTurnAction()
{
m_Session?.EndTurn();
}
private void UpdateUI() private void UpdateUI()
{ {
if (m_Session is null || m_Inspector is null || m_ForecastList is null || m_InventoryStrip is null) 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(); UpdateUI();
} }
private void OnTurnAdvanced(GameSession sender) private void OnPulseAdvanced(GameSession sender)
{ {
UpdateUI(); UpdateUI();
if (sender.LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won) if (sender.LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)

View File

@@ -37,18 +37,6 @@ public sealed class SimulationEngine
return ResolveStep(level); 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> Forecast(LevelState level) public IReadOnlyList<Forecast> Forecast(LevelState level)
{ {
if (!level.HasActivePoweredTerminalAccess()) if (!level.HasActivePoweredTerminalAccess())

View File

@@ -16,7 +16,7 @@
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" /> <AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
<AppBarSeparator /> <AppBarSeparator />
<AppBarButton x:Name="PlayPauseButton" Icon="Play" Label="Play" Click="PlayPause_Click" /> <AppBarButton x:Name="PlayPauseButton" Icon="Play" Label="Play" Click="PlayPause_Click" />
<AppBarButton Icon="Forward" Label="End Turn" Click="EndTurn_Click" /> <AppBarButton Icon="Forward" Label="Debug Pulse" Click="DebugPulse_Click" />
<AppBarButton Label="Interact" Click="Interact_Click" /> <AppBarButton Label="Interact" Click="Interact_Click" />
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" /> <AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" /> <AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
@@ -85,7 +85,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel> <StackPanel>
<TextBlock Text="Turn" Foreground="#9EA7AE" /> <TextBlock Text="Pulse" Foreground="#9EA7AE" />
<TextBlock x:Name="TurnText" FontSize="22" Foreground="#F4F1E8" /> <TextBlock x:Name="TurnText" FontSize="22" Foreground="#F4F1E8" />
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1"> <StackPanel Grid.Column="1">

View File

@@ -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) 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) private void SimulationTimer_Tick(object? sender, object e)
{ {
RunSimulationStep(); RunDebugPulse();
} }
private void StartSimulationTimer() private void StartSimulationTimer()
@@ -254,9 +254,9 @@ public sealed partial class MainWindow
PlayPauseButton.Label = isPlaying ? "Pause" : "Play"; 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(); RefreshInspector();
LevelCanvas.Invalidate(); LevelCanvas.Invalidate();
} }
@@ -878,7 +878,7 @@ public sealed partial class MainWindow
private void RefreshInspector() private void RefreshInspector()
{ {
LevelNameText.Text = m_Level.Name; 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) StatusText.Text = string.IsNullOrWhiteSpace(m_EditorFeedback)
? $"{m_Level.Global.LevelState}: {m_Level.Global.Status}" ? $"{m_Level.Global.LevelState}: {m_Level.Global.Status}"
: $"{m_Level.Global.LevelState}: {m_EditorFeedback}"; : $"{m_Level.Global.LevelState}: {m_EditorFeedback}";
@@ -1152,6 +1152,9 @@ public sealed partial class MainWindow
EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178), EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178),
EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55), EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55),
EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156), 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.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73), EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73),
_ => Colors.Gray _ => Colors.Gray
@@ -1178,6 +1181,9 @@ public sealed partial class MainWindow
EEditorTool.Junction => EPropType.Junction, EEditorTool.Junction => EPropType.Junction,
EEditorTool.Door => EPropType.Door, EEditorTool.Door => EPropType.Door,
EEditorTool.AllSeeingEyeTerminal => EPropType.AllSeeingEyeTerminal, EEditorTool.AllSeeingEyeTerminal => EPropType.AllSeeingEyeTerminal,
EEditorTool.IsolationValve => EPropType.IsolationValve,
EEditorTool.SprinklerControl => EPropType.SprinklerControl,
EEditorTool.SprinklerValve => EPropType.SprinklerValve,
EEditorTool.RemedySupply => EPropType.RemedySupply, EEditorTool.RemedySupply => EPropType.RemedySupply,
EEditorTool.ReactorControl => EPropType.ReactorControl, EEditorTool.ReactorControl => EPropType.ReactorControl,
_ => EPropType.None _ => EPropType.None
@@ -1192,6 +1198,9 @@ public sealed partial class MainWindow
EPropType.Junction => "prop-junction", EPropType.Junction => "prop-junction",
EPropType.Door => "prop-door", EPropType.Door => "prop-door",
EPropType.AllSeeingEyeTerminal => "prop-eye-terminal", 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.RemedySupply => "prop-remedy",
EPropType.ReactorControl => "prop-reactor", EPropType.ReactorControl => "prop-reactor",
_ => "prop" _ => "prop"
@@ -1231,6 +1240,9 @@ public sealed partial class MainWindow
EPropType.Junction => $"J {prop.JunctionMode}", EPropType.Junction => $"J {prop.JunctionMode}",
EPropType.Door => "DOOR", EPropType.Door => "DOOR",
EPropType.AllSeeingEyeTerminal => "EYE", 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.RemedySupply => RemedyShort(prop.RemedyType),
EPropType.ReactorControl => "REACT", EPropType.ReactorControl => "REACT",
_ => string.Empty _ => string.Empty
@@ -1300,10 +1312,15 @@ public sealed partial class MainWindow
CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"), CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Water, "Water Source"), CarrierTool(EEditorTool.Flow, ECarrierType.Water, "Water Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Electricity, "Electric 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.Consumer, "Consumer"),
Tool(EEditorTool.Junction, "Junction"), Tool(EEditorTool.Junction, "Junction"),
Tool(EEditorTool.Door, "Door"), Tool(EEditorTool.Door, "Door"),
Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"), Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"),
Tool(EEditorTool.SprinklerControl, "Sprinkler Control"),
Tool(EEditorTool.SprinklerValve, "Sprinkler Valve"),
RemedyTool(ERemedyType.FuelNeutralizer, "Fuel Remedy"), RemedyTool(ERemedyType.FuelNeutralizer, "Fuel Remedy"),
RemedyTool(ERemedyType.WaterNeutralizer, "Water Remedy"), RemedyTool(ERemedyType.WaterNeutralizer, "Water Remedy"),
RemedyTool(ERemedyType.ElectricityNeutralizer, "Electric Remedy"), RemedyTool(ERemedyType.ElectricityNeutralizer, "Electric Remedy"),
@@ -1333,6 +1350,9 @@ public sealed partial class MainWindow
or EEditorTool.Junction or EEditorTool.Junction
or EEditorTool.Door or EEditorTool.Door
or EEditorTool.AllSeeingEyeTerminal or EEditorTool.AllSeeingEyeTerminal
or EEditorTool.IsolationValve
or EEditorTool.SprinklerControl
or EEditorTool.SprinklerValve
or EEditorTool.RemedySupply or EEditorTool.RemedySupply
or EEditorTool.ReactorControl or EEditorTool.ReactorControl
or EEditorTool.SurfaceHazard or EEditorTool.SurfaceHazard
@@ -1341,7 +1361,7 @@ public sealed partial class MainWindow
} }
var activeCarrier = LayerCarrier(m_ActiveLayer); 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() private void RefreshForecasts()

View File

@@ -70,6 +70,31 @@ public sealed class LevelEditorTests
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).ElectricityServiceState); 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] [Fact]
public void ElectricityLeakUsesAuthoredWallAccessFace() public void ElectricityLeakUsesAuthoredWallAccessFace()
{ {

View File

@@ -67,7 +67,7 @@ public sealed class SimulationEngineTests
var next = m_Engine.MoveRobot(level, new(2, 1)); var next = m_Engine.MoveRobot(level, new(2, 1));
Assert.Equal(new(2, 1), next.Robot.Position); Assert.Equal(new(2, 1), next.Robot.Position);
Assert.Equal(0, next.Global.Turn); Assert.Equal(0, next.Global.Pulse);
} }
[Fact] [Fact]
@@ -434,6 +434,21 @@ public sealed class SimulationEngineTests
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition); 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] [Fact]
public void LevelSerializationRejectsOldSchema() public void LevelSerializationRejectsOldSchema()
{ {