Finish Godot campaign polish
This commit is contained in:
8
TASKS.md
8
TASKS.md
@@ -180,10 +180,10 @@ This backlog tracks what must change so the implementation matches `docs/design.
|
|||||||
|
|
||||||
## P2 Polish And Release Tasks
|
## 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.
|
- [x] 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.
|
- [x] Add campaign completion flow for the final Group 6 level.
|
||||||
- [ ] Add loading and malformed-level error states for campaign level loading.
|
- [x] 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] Revisit art labels/icons for `Water`, `Unsafe`, powered props, and sprinkler controls after mechanics are implemented.
|
||||||
|
|
||||||
## Verification Rules
|
## Verification Rules
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public sealed class GameSession
|
|||||||
var trace = m_Engine.InteractPropWithPulseTrace(LevelState);
|
var trace = m_Engine.InteractPropWithPulseTrace(LevelState);
|
||||||
LevelState = trace.FinalState;
|
LevelState = trace.FinalState;
|
||||||
LastPulseSteps = trace.Steps;
|
LastPulseSteps = trace.Steps;
|
||||||
|
LastPulseFeedback = CreatePulseFeedback(LevelState);
|
||||||
if (LevelState.Global.Pulse == pulse)
|
if (LevelState.Global.Pulse == pulse)
|
||||||
{
|
{
|
||||||
LevelStateChanged?.Invoke(this);
|
LevelStateChanged?.Invoke(this);
|
||||||
@@ -60,6 +61,7 @@ public sealed class GameSession
|
|||||||
var trace = m_Engine.InteractLeakWithPulseTrace(LevelState, carrier, useRemedy);
|
var trace = m_Engine.InteractLeakWithPulseTrace(LevelState, carrier, useRemedy);
|
||||||
LevelState = trace.FinalState;
|
LevelState = trace.FinalState;
|
||||||
LastPulseSteps = trace.Steps;
|
LastPulseSteps = trace.Steps;
|
||||||
|
LastPulseFeedback = CreatePulseFeedback(LevelState);
|
||||||
if (LevelState.Global.Pulse == pulse)
|
if (LevelState.Global.Pulse == pulse)
|
||||||
{
|
{
|
||||||
LevelStateChanged?.Invoke(this);
|
LevelStateChanged?.Invoke(this);
|
||||||
@@ -79,6 +81,7 @@ public sealed class GameSession
|
|||||||
var trace = m_Engine.ApplyHeatShieldWithPulseTrace(LevelState);
|
var trace = m_Engine.ApplyHeatShieldWithPulseTrace(LevelState);
|
||||||
LevelState = trace.FinalState;
|
LevelState = trace.FinalState;
|
||||||
LastPulseSteps = trace.Steps;
|
LastPulseSteps = trace.Steps;
|
||||||
|
LastPulseFeedback = CreatePulseFeedback(LevelState);
|
||||||
if (LevelState.Global.Pulse == pulse)
|
if (LevelState.Global.Pulse == pulse)
|
||||||
{
|
{
|
||||||
LevelStateChanged?.Invoke(this);
|
LevelStateChanged?.Invoke(this);
|
||||||
@@ -108,6 +111,7 @@ public sealed class GameSession
|
|||||||
{
|
{
|
||||||
LevelState = m_StartSnapshot;
|
LevelState = m_StartSnapshot;
|
||||||
LastPulseSteps = Array.Empty<LevelState>();
|
LastPulseSteps = Array.Empty<LevelState>();
|
||||||
|
LastPulseFeedback = Array.Empty<string>();
|
||||||
LevelStateChanged?.Invoke(this);
|
LevelStateChanged?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +142,30 @@ public sealed class GameSession
|
|||||||
PulseAdvanced?.Invoke(this);
|
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()
|
private void CheckOutcome()
|
||||||
{
|
{
|
||||||
if (LevelState.Global.LevelState == ELevelState.Won)
|
if (LevelState.Global.LevelState == ELevelState.Won)
|
||||||
@@ -150,6 +178,7 @@ public sealed class GameSession
|
|||||||
public GridPosition RobotPosition => LevelState.Robot.Position;
|
public GridPosition RobotPosition => LevelState.Robot.Position;
|
||||||
public GlobalState GlobalState => LevelState.Global;
|
public GlobalState GlobalState => LevelState.Global;
|
||||||
public IReadOnlyList<Forecast> Forecasts => LevelState.Forecasts;
|
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<LevelState> LastPulseSteps { get; private set; } = Array.Empty<LevelState>();
|
||||||
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
|
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
|
||||||
public string LevelPath { get; private set; } = string.Empty;
|
public string LevelPath { get; private set; } = string.Empty;
|
||||||
|
|||||||
@@ -16,4 +16,16 @@ public partial class GameOverScreen : ScreenBase
|
|||||||
actions.AddChild(CreateButton("Retry Current Level", app.RetryCurrentLevel));
|
actions.AddChild(CreateButton("Retry Current Level", app.RetryCurrentLevel));
|
||||||
actions.AddChild(CreateButton("Main Menu", app.ShowMainMenu));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -431,13 +431,22 @@ public partial class LevelScreen : ScreenBase
|
|||||||
timer.Stop();
|
timer.Stop();
|
||||||
UpdateUI();
|
UpdateUI();
|
||||||
m_InputLocked = false;
|
m_InputLocked = false;
|
||||||
SetEditorStatus(m_EditMode ? CurrentValidationText() : $"Pulse {m_Session?.LevelState.Global.Pulse ?? 0}");
|
SetEditorStatus(m_EditMode ? CurrentValidationText() : CurrentPulseFeedback());
|
||||||
timer.QueueFree();
|
timer.QueueFree();
|
||||||
};
|
};
|
||||||
AddChild(timer);
|
AddChild(timer);
|
||||||
timer.Start();
|
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)
|
private void ShowPlaybackSnapshot(LevelState snapshot)
|
||||||
{
|
{
|
||||||
m_GridViewport?.SetLevelState(snapshot);
|
m_GridViewport?.SetLevelState(snapshot);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using ReactorMaintenance.Godot.Data;
|
using ReactorMaintenance.Godot.Data;
|
||||||
using ReactorMaintenance.Godot.Screens;
|
using ReactorMaintenance.Godot.Screens;
|
||||||
|
using ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
namespace ReactorMaintenance.Godot;
|
namespace ReactorMaintenance.Godot;
|
||||||
|
|
||||||
@@ -49,7 +50,17 @@ public partial class AppController : Control
|
|||||||
m_Session.MarkCurrentLevelLoaded();
|
m_Session.MarkCurrentLevelLoaded();
|
||||||
var level = m_Session.CurrentLevel;
|
var level = m_Session.CurrentLevel;
|
||||||
var gameSession = new GameSession();
|
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);
|
gameSession.Initialize(levelState, level.LevelPath);
|
||||||
ShowScreen<LevelScreen>("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, gameSession, level, m_Session.LevelNumber, m_Session.LevelCount));
|
ShowScreen<LevelScreen>("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, gameSession, level, m_Session.LevelNumber, m_Session.LevelCount));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user