Finish Godot campaign polish

This commit is contained in:
2026-05-14 11:09:22 +02:00
parent ad5445b09d
commit 6feaf84e39
5 changed files with 67 additions and 6 deletions

View File

@@ -180,10 +180,10 @@ This backlog tracks what must change so the implementation matches `docs/design.
## P2 Polish And Release Tasks
- [ ] 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.
- [ ] Revisit art labels/icons for `Water`, `Unsafe`, powered props, and sprinkler controls after mechanics are implemented.
- [x] Add concise pulse-result feedback for major outcomes: isolated leak, restored pressure, downstream starvation, reactor ready, wet-electric risk, and terminal heat danger.
- [x] Add campaign completion flow for the final Group 6 level.
- [x] Add loading and malformed-level error states for campaign level loading.
- [x] Revisit art labels/icons for `Water`, `Unsafe`, powered props, and sprinkler controls after mechanics are implemented.
## Verification Rules

View File

@@ -41,6 +41,7 @@ public sealed class GameSession
var trace = m_Engine.InteractPropWithPulseTrace(LevelState);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
@@ -60,6 +61,7 @@ public sealed class GameSession
var trace = m_Engine.InteractLeakWithPulseTrace(LevelState, carrier, useRemedy);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
@@ -79,6 +81,7 @@ public sealed class GameSession
var trace = m_Engine.ApplyHeatShieldWithPulseTrace(LevelState);
LevelState = trace.FinalState;
LastPulseSteps = trace.Steps;
LastPulseFeedback = CreatePulseFeedback(LevelState);
if (LevelState.Global.Pulse == pulse)
{
LevelStateChanged?.Invoke(this);
@@ -108,6 +111,7 @@ public sealed class GameSession
{
LevelState = m_StartSnapshot;
LastPulseSteps = Array.Empty<LevelState>();
LastPulseFeedback = Array.Empty<string>();
LevelStateChanged?.Invoke(this);
}
@@ -138,6 +142,30 @@ public sealed class GameSession
PulseAdvanced?.Invoke(this);
}
private static IReadOnlyList<string> CreatePulseFeedback(LevelState level)
{
var feedback = new List<string>();
if (level.Global.LevelState == ELevelState.Ready)
feedback.Add("Reactor ready");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Fuel) == EConsumerServiceState.Starved))
feedback.Add("Fuel consumer starved");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Water) == EConsumerServiceState.Starved))
feedback.Add("Water consumer starved");
if (level.Props.Any(prop => prop.Type == EPropType.Consumer && prop.ServiceStateFor(ECarrierType.Electricity) == EConsumerServiceState.Starved))
feedback.Add("Electricity consumer starved");
if (level.Surface.Any(surface => surface.IsWetElectricUnsafe()))
feedback.Add("Wet-electric risk");
if (level.Surface.Any(surface => surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold))
feedback.Add("Heat danger");
return feedback.Count > 0 ? feedback : [level.Global.Status];
}
private void CheckOutcome()
{
if (LevelState.Global.LevelState == ELevelState.Won)
@@ -150,6 +178,7 @@ public sealed class GameSession
public GridPosition RobotPosition => LevelState.Robot.Position;
public GlobalState GlobalState => LevelState.Global;
public IReadOnlyList<Forecast> Forecasts => LevelState.Forecasts;
public IReadOnlyList<string> LastPulseFeedback { get; private set; } = Array.Empty<string>();
public IReadOnlyList<LevelState> LastPulseSteps { get; private set; } = Array.Empty<LevelState>();
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
public string LevelPath { get; private set; } = string.Empty;

View File

@@ -16,4 +16,16 @@ public partial class GameOverScreen : ScreenBase
actions.AddChild(CreateButton("Retry Current Level", app.RetryCurrentLevel));
actions.AddChild(CreateButton("Main Menu", app.ShowMainMenu));
}
public void ConfigureLoadError(AppController app, CampaignLevel level, string message)
{
var body = CreatePage("Level Load Error");
body.Alignment = BoxContainer.AlignmentMode.Center;
body.AddChild(CreateBodyText($"{level.Name}: {message}"));
var actions = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
body.AddChild(actions);
actions.AddChild(CreateButton("Retry", app.RetryCurrentLevel));
actions.AddChild(CreateButton("Main Menu", app.ShowMainMenu));
}
}

View File

@@ -431,13 +431,22 @@ public partial class LevelScreen : ScreenBase
timer.Stop();
UpdateUI();
m_InputLocked = false;
SetEditorStatus(m_EditMode ? CurrentValidationText() : $"Pulse {m_Session?.LevelState.Global.Pulse ?? 0}");
SetEditorStatus(m_EditMode ? CurrentValidationText() : CurrentPulseFeedback());
timer.QueueFree();
};
AddChild(timer);
timer.Start();
}
private string CurrentPulseFeedback()
{
if (m_Session is null)
return "Play mode";
var feedback = m_Session.LastPulseFeedback.Count > 0 ? string.Join(", ", m_Session.LastPulseFeedback) : m_Session.LevelState.Global.Status;
return $"Pulse {m_Session.LevelState.Global.Pulse}: {feedback}";
}
private void ShowPlaybackSnapshot(LevelState snapshot)
{
m_GridViewport?.SetLevelState(snapshot);

View File

@@ -1,6 +1,7 @@
using Godot;
using ReactorMaintenance.Godot.Data;
using ReactorMaintenance.Godot.Screens;
using ReactorMaintenance.Simulation;
namespace ReactorMaintenance.Godot;
@@ -49,7 +50,17 @@ public partial class AppController : Control
m_Session.MarkCurrentLevelLoaded();
var level = m_Session.CurrentLevel;
var gameSession = new GameSession();
var levelState = LevelStateLoader.Load(level.LevelPath);
LevelState levelState;
try
{
levelState = LevelStateLoader.Load(level.LevelPath);
}
catch (Exception ex)
{
ShowScreen<GameOverScreen>("res://Scenes/GameOverScreen.tscn", screen => screen.ConfigureLoadError(this, level, ex.Message));
return;
}
gameSession.Initialize(levelState, level.LevelPath);
ShowScreen<LevelScreen>("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, gameSession, level, m_Session.LevelNumber, m_Session.LevelCount));
}