Complete Godot pulse UX integration
This commit is contained in:
11
TASKS.md
11
TASKS.md
@@ -5,7 +5,7 @@ This backlog tracks what must change so the implementation matches `docs/design.
|
||||
## Audit Snapshot
|
||||
|
||||
- Current simulation tests pass: `60/60` 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.
|
||||
- Godot level play includes pulse step playback, terminal-gated layer controls, campaign loading, and in-game level editing with JSON save.
|
||||
- Campaign data follows 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.
|
||||
|
||||
@@ -160,20 +160,20 @@ This backlog tracks what must change so the implementation matches `docs/design.
|
||||
- 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.
|
||||
- [ ] Animate `Pulse` playback as a short cascade of `Step`s.
|
||||
- [x] Animate `Pulse` playback as a short cascade of `Step`s.
|
||||
- Show leak growth, sprinkler discharge, evaporation, quenching, ignition, wet conduction, and readiness updates.
|
||||
- Disable conflicting inputs during playback.
|
||||
- End playback on the final post-pulse decision state.
|
||||
- [ ] Gate underground layer controls and forecasts.
|
||||
- [x] Gate underground layer controls and forecasts.
|
||||
- Hide or disable layer toggles unless the robot is at an active and powered `AllSeeingEyeTerminal`.
|
||||
- Show why terminal access is unavailable when unpowered or away from the terminal.
|
||||
- Use `Pulse +N` wording in forecast UI.
|
||||
- [ ] Update grid rendering and inspector text.
|
||||
- [x] Update grid rendering and inspector text.
|
||||
- Render `Water` separately from underground `water`.
|
||||
- Render `Unsafe` cells with a distinct movement warning treatment.
|
||||
- Render isolation valve state, sprinkler control state, wall-mounted sprinkler outlet, powered door state, and powered terminal state.
|
||||
- Inspector should display visible surface values, prop state, consumer per-carrier state, and underground values only when terminal access allows it.
|
||||
- [ ] Update action affordances.
|
||||
- [x] Update action affordances.
|
||||
- Movement remains direct and quick.
|
||||
- Prop interactions should explain unavailable power, invalid position, missing remedy, depleted supply, and reactor-not-ready causes.
|
||||
- Disabled actions remain inspectable so players can understand why an action is unavailable.
|
||||
@@ -183,7 +183,6 @@ This backlog tracks what must change so the implementation matches `docs/design.
|
||||
- [ ] Add concise pulse-result feedback for major outcomes: isolated leak, restored pressure, downstream starvation, reactor ready, wet-electric risk, and terminal heat danger.
|
||||
- [ ] Add campaign completion flow for the final Group 6 level.
|
||||
- [ ] Add loading and malformed-level error states for campaign level loading.
|
||||
- [ ] Verify Win2D editor export and Godot loader compatibility for the new schema.
|
||||
- [ ] Revisit art labels/icons for `Water`, `Unsafe`, powered props, and sprinkler controls after mechanics are implemented.
|
||||
|
||||
## Verification Rules
|
||||
|
||||
@@ -23,19 +23,76 @@ public partial class CellInspector : PanelContainer
|
||||
m_Text.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||
}
|
||||
|
||||
public void SetCellInfo(GridPosition? position, ECellTerrain terrain, EPropType prop, EConsumerServiceState serviceState, float fuelHazard, float waterHazard, float electricityHazard, float heatHazard)
|
||||
public void SetCellInfo(LevelState? level, GridPosition? position, bool canSeeUnderground)
|
||||
{
|
||||
var pos = position ?? new(-1, -1);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Selected Cell: {pos.X},{pos.Y}");
|
||||
if (level is null || position is null || !level.InBounds(pos))
|
||||
{
|
||||
m_Text.Text = sb.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
var terrain = level.GetTerrain(pos);
|
||||
var prop = level.GetProp(pos);
|
||||
var surface = level.GetSurface(pos);
|
||||
sb.AppendLine($"Terrain: {terrain}");
|
||||
sb.AppendLine($"Prop: {prop}");
|
||||
if (prop != EPropType.None)
|
||||
sb.AppendLine($"Service: {serviceState}");
|
||||
sb.AppendLine($"Hazards: {FormatHazards(fuelHazard, waterHazard, electricityHazard, heatHazard)}");
|
||||
sb.AppendLine($"Prop: {FormatProp(prop)}");
|
||||
if (prop.Type != EPropType.None)
|
||||
sb.AppendLine($"Service: {FormatService(prop)}");
|
||||
sb.AppendLine($"Surface: {FormatHazards(surface.Fuel, surface.Water, surface.Electricity, surface.Heat)}");
|
||||
sb.AppendLine(surface.IsUnsafe() ? "Movement: Unsafe" : "Movement: Safe");
|
||||
|
||||
if (canSeeUnderground)
|
||||
sb.AppendLine($"Underground: {FormatUnderground(level, pos)}");
|
||||
else
|
||||
sb.AppendLine("Underground: terminal access required");
|
||||
m_Text.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private static string FormatProp(PropState prop)
|
||||
{
|
||||
return prop.Type switch {
|
||||
EPropType.Flow => $"{prop.Carrier} Flow {prop.SwitchState}",
|
||||
EPropType.Consumer => $"Consumer {prop.SwitchState}",
|
||||
EPropType.IsolationValve => $"{prop.Carrier} Valve {(prop.IsOpen ? "Open" : "Closed")}",
|
||||
EPropType.SprinklerControl => $"Sprinkler Control {prop.SwitchState}",
|
||||
EPropType.SprinklerValve => $"Sprinkler Valve -> {FormatPosition(prop.OutletPosition)}",
|
||||
EPropType.Door => $"Door {prop.DoorState}",
|
||||
EPropType.AllSeeingEyeTerminal => prop.Active ? "All-Seeing-Eye Active" : "All-Seeing-Eye",
|
||||
EPropType.ReactorControl => $"Reactor Control {prop.ReactorId}",
|
||||
EPropType.RemedySupply => prop.Depleted ? $"{prop.RemedyType} Empty" : $"{prop.RemedyType} Supply",
|
||||
_ => prop.Type.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatService(PropState prop)
|
||||
{
|
||||
if (prop.Type == EPropType.Consumer)
|
||||
return $"fuel {prop.FuelServiceState}, water {prop.WaterServiceState}, electricity {prop.ElectricityServiceState}";
|
||||
|
||||
return prop.ServiceState.ToString();
|
||||
}
|
||||
|
||||
private static string FormatUnderground(LevelState level, GridPosition position)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
var cell = level.GetUnderground(position, carrier);
|
||||
if (cell.IsPresent)
|
||||
parts.Add($"{carrier} {cell.State} {cell.Amount:F1}/{cell.Intensity:F1}");
|
||||
}
|
||||
|
||||
return parts.Count > 0 ? string.Join(", ", parts) : "none";
|
||||
}
|
||||
|
||||
private static string FormatPosition(GridPosition? position)
|
||||
{
|
||||
return position is { } p ? $"{p.X},{p.Y}" : "unlinked";
|
||||
}
|
||||
|
||||
private static string FormatHazards(float fuel, float water, float electricity, float heat)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
@@ -28,6 +28,18 @@ public partial class ForecastList : PanelContainer
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUnavailable(string reason)
|
||||
{
|
||||
foreach (var child in m_Items.GetChildren())
|
||||
child.QueueFree();
|
||||
|
||||
m_Items.AddChild(new Label { Text = "Forecasts" });
|
||||
m_Items.AddChild(new Label {
|
||||
Text = reason,
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatForecast(Forecast forecast)
|
||||
{
|
||||
var pos = forecast.Position;
|
||||
|
||||
@@ -42,6 +42,7 @@ public partial class GridViewport : Control
|
||||
DrawTerrain(layout, SurfaceOpacity());
|
||||
DrawUnderground(layout);
|
||||
DrawSurfaceHazards(layout, SurfaceOpacity());
|
||||
DrawUnsafeWarnings(layout, SurfaceOpacity());
|
||||
DrawDoors(layout, SurfaceOpacity());
|
||||
DrawProps(layout, SurfaceOpacity());
|
||||
DrawLeaks(layout, SurfaceOpacity());
|
||||
@@ -206,6 +207,21 @@ public partial class GridViewport : Control
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawUnsafeWarnings(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var position in AllPositions().Where(m_LevelState.IsFloor))
|
||||
{
|
||||
if (!m_LevelState.GetSurface(position).IsUnsafe())
|
||||
continue;
|
||||
|
||||
var rect = Inset(layout.CellRect(position), 0.08f);
|
||||
DrawRect(rect, WithOpacity(c_UnsafeColor, opacity), false, Math.Max(2, layout.CellSize * 0.07f));
|
||||
}
|
||||
}
|
||||
|
||||
private void FillHazard(Rect2 rect, float amount, Color color, float inset, float opacity, float caution, float critical)
|
||||
{
|
||||
var overlayOpacity = SurfaceOverlayOpacity(amount, caution, critical);
|
||||
@@ -628,6 +644,7 @@ public partial class GridViewport : Control
|
||||
private static readonly Color c_ReachableColor = new(0.78f, 0.96f, 0.84f, 0.20f);
|
||||
private static readonly Color c_ReadyColor = new(0.46f, 0.95f, 0.52f);
|
||||
private static readonly Color c_SelectedColor = new(1.0f, 1.0f, 1.0f, 0.95f);
|
||||
private static readonly Color c_UnsafeColor = new(1.0f, 0.9f, 0.25f, 0.9f);
|
||||
|
||||
private bool m_IsPanning;
|
||||
private LevelState? m_LevelState;
|
||||
|
||||
@@ -37,7 +37,16 @@ public sealed class GameSession
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.InteractProp(LevelState);
|
||||
var pulse = LevelState.Global.Pulse;
|
||||
var trace = m_Engine.InteractPropWithPulseTrace(LevelState);
|
||||
LevelState = trace.FinalState;
|
||||
LastPulseSteps = trace.Steps;
|
||||
if (LevelState.Global.Pulse == pulse)
|
||||
{
|
||||
LevelStateChanged?.Invoke(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPulseAdvanced();
|
||||
return true;
|
||||
}
|
||||
@@ -47,7 +56,16 @@ public sealed class GameSession
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.InteractLeak(LevelState, carrier, useRemedy);
|
||||
var pulse = LevelState.Global.Pulse;
|
||||
var trace = m_Engine.InteractLeakWithPulseTrace(LevelState, carrier, useRemedy);
|
||||
LevelState = trace.FinalState;
|
||||
LastPulseSteps = trace.Steps;
|
||||
if (LevelState.Global.Pulse == pulse)
|
||||
{
|
||||
LevelStateChanged?.Invoke(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPulseAdvanced();
|
||||
return true;
|
||||
}
|
||||
@@ -57,7 +75,16 @@ public sealed class GameSession
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.ApplyHeatShield(LevelState);
|
||||
var pulse = LevelState.Global.Pulse;
|
||||
var trace = m_Engine.ApplyHeatShieldWithPulseTrace(LevelState);
|
||||
LevelState = trace.FinalState;
|
||||
LastPulseSteps = trace.Steps;
|
||||
if (LevelState.Global.Pulse == pulse)
|
||||
{
|
||||
LevelStateChanged?.Invoke(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
OnPulseAdvanced();
|
||||
return true;
|
||||
}
|
||||
@@ -80,6 +107,7 @@ public sealed class GameSession
|
||||
public void Retry()
|
||||
{
|
||||
LevelState = m_StartSnapshot;
|
||||
LastPulseSteps = Array.Empty<LevelState>();
|
||||
LevelStateChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
@@ -122,6 +150,7 @@ public sealed class GameSession
|
||||
public GridPosition RobotPosition => LevelState.Robot.Position;
|
||||
public GlobalState GlobalState => LevelState.Global;
|
||||
public IReadOnlyList<Forecast> Forecasts => LevelState.Forecasts;
|
||||
public IReadOnlyList<LevelState> LastPulseSteps { get; private set; } = Array.Empty<LevelState>();
|
||||
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
|
||||
public string LevelPath { get; private set; } = string.Empty;
|
||||
public IReadOnlyList<ReactorState> Reactors => LevelState.Reactors;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using ReactorMaintenance.Godot.Controls;
|
||||
using ReactorMaintenance.Godot.Data;
|
||||
using ReactorMaintenance.Simulation;
|
||||
using Timer = Godot.Timer;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Screens;
|
||||
|
||||
@@ -77,24 +78,27 @@ public partial class LevelScreen : ScreenBase
|
||||
private Control CreateLayerControls()
|
||||
{
|
||||
var controls = new HBoxContainer();
|
||||
controls.AddChild(CreateLayerToggle("Fuel", true, pressed => {
|
||||
m_FuelLayerToggle = CreateLayerToggle("Fuel", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowFuelLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
controls.AddChild(CreateLayerToggle("Water", true, pressed => {
|
||||
});
|
||||
controls.AddChild(m_FuelLayerToggle);
|
||||
m_WaterLayerToggle = CreateLayerToggle("Water", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowWaterLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
controls.AddChild(CreateLayerToggle("Electricity", true, pressed => {
|
||||
});
|
||||
controls.AddChild(m_WaterLayerToggle);
|
||||
m_ElectricityLayerToggle = CreateLayerToggle("Electricity", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowElectricityLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
});
|
||||
controls.AddChild(m_ElectricityLayerToggle);
|
||||
|
||||
var activeLayer = new OptionButton();
|
||||
activeLayer.AddItem("Surface", 0);
|
||||
@@ -113,6 +117,7 @@ public partial class LevelScreen : ScreenBase
|
||||
m_GridViewport.QueueRedraw();
|
||||
};
|
||||
controls.AddChild(activeLayer);
|
||||
m_ActiveLayerSelect = activeLayer;
|
||||
return controls;
|
||||
}
|
||||
|
||||
@@ -132,41 +137,70 @@ 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("Heat Shield", OnHeatShieldAction, "Spend one heat shield for temporary movement safety"));
|
||||
actions.AddChild(CreateButton("Activate", OnActivateAction, "Activate a ready reactor control at the robot position"));
|
||||
actions.AddChild(CreateButton("Main Menu", () => m_App?.ShowMainMenu()));
|
||||
return actions;
|
||||
}
|
||||
|
||||
private void OnMoveAction()
|
||||
{
|
||||
if (m_Session is null || m_EditMode) return;
|
||||
if (m_Session is null || m_EditMode || m_InputLocked) return;
|
||||
|
||||
var current = m_Session.RobotPosition;
|
||||
var next = current with { X = current.X + 1 };
|
||||
if (!m_Session.MoveRobot(next))
|
||||
{
|
||||
next = current with { Y = current.Y + 1 };
|
||||
m_Session.MoveRobot(next);
|
||||
if (!m_Session.MoveRobot(next))
|
||||
SetEditorStatus("No quick move target to the east or south.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInteractAction()
|
||||
{
|
||||
if (!m_EditMode)
|
||||
m_Session?.InteractProp();
|
||||
if (!m_EditMode && !m_InputLocked)
|
||||
RunAction(() => m_Session?.InteractProp() ?? false);
|
||||
}
|
||||
|
||||
private void OnRepairAction()
|
||||
{
|
||||
if (m_Session is null || m_EditMode) return;
|
||||
if (m_Session is null || m_EditMode || m_InputLocked) return;
|
||||
|
||||
foreach (var leak in m_Session.Leaks)
|
||||
{
|
||||
if (leak.AccessPosition == m_Session.RobotPosition && !leak.Repaired)
|
||||
{
|
||||
m_Session.InteractLeak(leak.Carrier, true);
|
||||
RunAction(() => m_Session.InteractLeak(leak.Carrier, true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SetEditorStatus("No reachable leak with available remedy.");
|
||||
}
|
||||
|
||||
private void OnHeatShieldAction()
|
||||
{
|
||||
if (m_Session is null || m_EditMode || m_InputLocked)
|
||||
return;
|
||||
|
||||
RunAction(m_Session.ApplyHeatShield);
|
||||
}
|
||||
|
||||
private void OnActivateAction()
|
||||
{
|
||||
if (m_Session is null || m_EditMode || m_InputLocked)
|
||||
return;
|
||||
|
||||
var before = m_Session.LevelState.Global.LevelState;
|
||||
if (!m_Session.ActivateReactor() || before == m_Session.LevelState.Global.LevelState)
|
||||
SetEditorStatus("Reactor is not ready at this position.");
|
||||
}
|
||||
|
||||
private void RunAction(Func<bool> action)
|
||||
{
|
||||
if (!action())
|
||||
SetEditorStatus(m_Session?.LevelState.Global.Status ?? "Action unavailable.");
|
||||
}
|
||||
|
||||
private Control CreateEditorControls()
|
||||
@@ -235,10 +269,14 @@ public partial class LevelScreen : ScreenBase
|
||||
|
||||
var ls = m_Session.LevelState;
|
||||
m_Header?.SetLevel(m_Level?.Name ?? ls.Name, m_LevelNumber, m_LevelCount, ls.Global.LevelState);
|
||||
UpdateTerminalGating(ls);
|
||||
m_GridViewport?.SetLevelState(ls);
|
||||
UpdateInspector(m_GridViewport?.SelectedCell ?? new Vector2I(ls.Robot.Position.X, ls.Robot.Position.Y));
|
||||
|
||||
m_ForecastList.SetForecasts(ls.Forecasts);
|
||||
if (ls.HasActivePoweredTerminalAccess())
|
||||
m_ForecastList.SetForecasts(ls.Forecasts);
|
||||
else
|
||||
m_ForecastList.SetUnavailable("Terminal access required.");
|
||||
|
||||
m_InventoryStrip.SetInventory(
|
||||
m_Session.LevelState.Robot.FuelNeutralizers,
|
||||
@@ -256,21 +294,11 @@ public partial class LevelScreen : ScreenBase
|
||||
var position = new GridPosition(cell.X, cell.Y);
|
||||
if (!ls.InBounds(position))
|
||||
{
|
||||
m_Inspector.SetCellInfo(null, ECellTerrain.Wall, EPropType.None, EConsumerServiceState.Unknown, 0, 0, 0, 0);
|
||||
m_Inspector.SetCellInfo(null, null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
var surface = ls.GetSurface(position);
|
||||
var prop = ls.GetProp(position);
|
||||
m_Inspector.SetCellInfo(
|
||||
position,
|
||||
ls.GetTerrain(position),
|
||||
prop.Type,
|
||||
prop.ServiceState,
|
||||
surface.Fuel,
|
||||
surface.Water,
|
||||
surface.Electricity,
|
||||
surface.Heat);
|
||||
m_Inspector.SetCellInfo(ls, position, ls.HasActivePoweredTerminalAccess());
|
||||
}
|
||||
|
||||
private void OnLevelStateChanged(GameSession sender)
|
||||
@@ -285,7 +313,7 @@ public partial class LevelScreen : ScreenBase
|
||||
|
||||
private void OnPulseAdvanced(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
BeginPulsePlayback(sender.LastPulseSteps);
|
||||
if (sender.LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
{
|
||||
ShowOutcomeOverlay(sender.LevelState.Global.LevelState);
|
||||
@@ -332,7 +360,7 @@ public partial class LevelScreen : ScreenBase
|
||||
|
||||
private void OnGridClicked(Vector2I cell)
|
||||
{
|
||||
if (m_Session is null)
|
||||
if (m_Session is null || m_InputLocked)
|
||||
return;
|
||||
|
||||
var destination = new GridPosition(cell.X, cell.Y);
|
||||
@@ -347,6 +375,78 @@ public partial class LevelScreen : ScreenBase
|
||||
m_Session.MoveRobot(destination);
|
||||
}
|
||||
|
||||
private void UpdateTerminalGating(LevelState level)
|
||||
{
|
||||
var canSeeUnderground = level.HasActivePoweredTerminalAccess();
|
||||
foreach (var toggle in new[] { m_FuelLayerToggle, m_WaterLayerToggle, m_ElectricityLayerToggle })
|
||||
{
|
||||
if (toggle is null)
|
||||
continue;
|
||||
|
||||
toggle.Disabled = !canSeeUnderground;
|
||||
toggle.TooltipText = canSeeUnderground ? "Toggle terminal-visible underground layer" : "Use a powered All-Seeing-Eye terminal to view underground layers";
|
||||
}
|
||||
|
||||
if (m_ActiveLayerSelect is not null)
|
||||
{
|
||||
m_ActiveLayerSelect.Disabled = !canSeeUnderground;
|
||||
if (!canSeeUnderground)
|
||||
{
|
||||
m_ActiveLayerSelect.Select(0);
|
||||
if (m_GridViewport is not null)
|
||||
m_GridViewport.ActiveUndergroundLayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_GridViewport is null)
|
||||
return;
|
||||
|
||||
m_GridViewport.ShowFuelLayer = canSeeUnderground && (m_FuelLayerToggle?.ButtonPressed ?? false);
|
||||
m_GridViewport.ShowWaterLayer = canSeeUnderground && (m_WaterLayerToggle?.ButtonPressed ?? false);
|
||||
m_GridViewport.ShowElectricityLayer = canSeeUnderground && (m_ElectricityLayerToggle?.ButtonPressed ?? false);
|
||||
}
|
||||
|
||||
private void BeginPulsePlayback(IReadOnlyList<LevelState> steps)
|
||||
{
|
||||
if (steps.Count == 0)
|
||||
{
|
||||
UpdateUI();
|
||||
return;
|
||||
}
|
||||
|
||||
m_InputLocked = true;
|
||||
SetEditorStatus("Pulse resolving...");
|
||||
var index = 0;
|
||||
var timer = new Timer {
|
||||
OneShot = false,
|
||||
WaitTime = 0.12
|
||||
};
|
||||
timer.Timeout += () => {
|
||||
var snapshot = steps[Math.Min(index, steps.Count - 1)];
|
||||
ShowPlaybackSnapshot(snapshot);
|
||||
index++;
|
||||
if (index < steps.Count)
|
||||
return;
|
||||
|
||||
timer.Stop();
|
||||
UpdateUI();
|
||||
m_InputLocked = false;
|
||||
SetEditorStatus(m_EditMode ? CurrentValidationText() : $"Pulse {m_Session?.LevelState.Global.Pulse ?? 0}");
|
||||
timer.QueueFree();
|
||||
};
|
||||
AddChild(timer);
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private void ShowPlaybackSnapshot(LevelState snapshot)
|
||||
{
|
||||
m_GridViewport?.SetLevelState(snapshot);
|
||||
var cell = m_GridViewport?.SelectedCell ?? new Vector2I(snapshot.Robot.Position.X, snapshot.Robot.Position.Y);
|
||||
var position = new GridPosition(cell.X, cell.Y);
|
||||
m_Inspector?.SetCellInfo(snapshot, snapshot.InBounds(position) ? position : snapshot.Robot.Position, snapshot.HasActivePoweredTerminalAccess());
|
||||
SetEditorStatus($"Pulse {snapshot.Global.Pulse + 1} Step {snapshot.Global.Step}");
|
||||
}
|
||||
|
||||
private EditorToolCommand CurrentEditorCommand()
|
||||
{
|
||||
return new() {
|
||||
@@ -390,13 +490,18 @@ public partial class LevelScreen : ScreenBase
|
||||
};
|
||||
}
|
||||
|
||||
private OptionButton? m_ActiveLayerSelect;
|
||||
|
||||
private AppController? m_App;
|
||||
private OptionButton? m_CarrierSelect;
|
||||
private bool m_EditMode;
|
||||
private Label? m_EditorStatus;
|
||||
private CheckBox? m_ElectricityLayerToggle;
|
||||
private ForecastList? m_ForecastList;
|
||||
private CheckBox? m_FuelLayerToggle;
|
||||
private GridViewport? m_GridViewport;
|
||||
private LevelHeader? m_Header;
|
||||
private bool m_InputLocked;
|
||||
private CellInspector? m_Inspector;
|
||||
private InventoryStrip? m_InventoryStrip;
|
||||
private CampaignLevel? m_Level;
|
||||
@@ -407,4 +512,5 @@ public partial class LevelScreen : ScreenBase
|
||||
private OptionButton? m_RemedySelect;
|
||||
private GameSession? m_Session;
|
||||
private OptionButton? m_ToolSelect;
|
||||
private CheckBox? m_WaterLayerToggle;
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
public sealed record PulseTrace(LevelState FinalState, IReadOnlyList<LevelState> Steps);
|
||||
|
||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
return PlayerActionSystem.MoveRobot(level, destination);
|
||||
@@ -12,16 +14,55 @@ public sealed class SimulationEngine
|
||||
return PlayerActionSystem.InteractProp(level, ResolvePulse);
|
||||
}
|
||||
|
||||
public PulseTrace InteractPropWithPulseTrace(LevelState level)
|
||||
{
|
||||
PulseTrace? trace = null;
|
||||
var finalState = PlayerActionSystem.InteractProp(level, Resolve);
|
||||
return trace ?? new(finalState, Array.Empty<LevelState>());
|
||||
|
||||
LevelState Resolve(LevelState accepted)
|
||||
{
|
||||
trace = ResolvePulseWithTrace(accepted);
|
||||
return trace.FinalState;
|
||||
}
|
||||
}
|
||||
|
||||
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolvePulse);
|
||||
}
|
||||
|
||||
public PulseTrace InteractLeakWithPulseTrace(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
PulseTrace? trace = null;
|
||||
var finalState = PlayerActionSystem.InteractLeak(level, carrier, useRemedy, Resolve);
|
||||
return trace ?? new(finalState, Array.Empty<LevelState>());
|
||||
|
||||
LevelState Resolve(LevelState accepted)
|
||||
{
|
||||
trace = ResolvePulseWithTrace(accepted);
|
||||
return trace.FinalState;
|
||||
}
|
||||
}
|
||||
|
||||
public LevelState ApplyHeatShield(LevelState level)
|
||||
{
|
||||
return PlayerActionSystem.ApplyHeatShield(level, ResolvePulse);
|
||||
}
|
||||
|
||||
public PulseTrace ApplyHeatShieldWithPulseTrace(LevelState level)
|
||||
{
|
||||
PulseTrace? trace = null;
|
||||
var finalState = PlayerActionSystem.ApplyHeatShield(level, Resolve);
|
||||
return trace ?? new(finalState, Array.Empty<LevelState>());
|
||||
|
||||
LevelState Resolve(LevelState accepted)
|
||||
{
|
||||
trace = ResolvePulseWithTrace(accepted);
|
||||
return trace.FinalState;
|
||||
}
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
return ReactorSystem.Activate(level);
|
||||
@@ -46,13 +87,22 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
private LevelState ResolvePulse(LevelState level)
|
||||
{
|
||||
return ResolvePulseWithTrace(level).FinalState;
|
||||
}
|
||||
|
||||
private PulseTrace ResolvePulseWithTrace(LevelState level)
|
||||
{
|
||||
var next = ValidateAndPropagate(level);
|
||||
if (next.Global.LevelState == ELevelState.Lost)
|
||||
return next;
|
||||
return new(next, [next]);
|
||||
|
||||
var steps = new List<LevelState>();
|
||||
for (var i = 0; i < Balancing.Current.StepsPerPulse; i++)
|
||||
{
|
||||
next = ResolveStepContent(next);
|
||||
steps.Add(next);
|
||||
}
|
||||
|
||||
next = ReactorSystem.DeriveState(next);
|
||||
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
||||
@@ -63,7 +113,9 @@ public sealed class SimulationEngine
|
||||
}
|
||||
};
|
||||
|
||||
return next with { Forecasts = Forecast(next) };
|
||||
next = next with { Forecasts = Forecast(next) };
|
||||
steps.Add(next);
|
||||
return new(next, steps);
|
||||
}
|
||||
|
||||
private LevelState ResolveStep(LevelState level, bool refreshForecasts = true)
|
||||
|
||||
Reference in New Issue
Block a user