From 33859d2cf6b918860a3457a7dddacfff08e37112 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Tue, 12 May 2026 21:06:48 +0200 Subject: [PATCH] Implement Godot UX scene scaffold --- README.md | 10 +- docs/UX.md | 2 +- .../Controls/CellInspector.cs | 15 +++ .../Controls/CellInspector.cs.uid | 1 + .../Controls/ConfirmDialog.cs | 33 ++++++ .../Controls/ConfirmDialog.cs.uid | 1 + .../Controls/ForecastList.cs | 24 ++++ .../Controls/ForecastList.cs.uid | 1 + .../Controls/InventoryStrip.cs | 23 ++++ .../Controls/InventoryStrip.cs.uid | 1 + .../Controls/LevelHeader.cs | 33 ++++++ .../Controls/LevelHeader.cs.uid | 1 + .../Controls/OutcomeOverlay.cs | 41 +++++++ .../Controls/OutcomeOverlay.cs.uid | 1 + .../Controls/PrimaryButton.cs | 20 ++++ .../Controls/PrimaryButton.cs.uid | 1 + .../Controls/StateBadge.cs | 30 +++++ .../Controls/StateBadge.cs.uid | 1 + .../Data/CampaignLevel.cs | 9 ++ .../Data/CampaignLevel.cs.uid | 1 + .../Data/CampaignManifest.cs | 6 + .../Data/CampaignManifest.cs.uid | 1 + .../Data/CampaignRepository.cs | 39 +++++++ .../Data/CampaignRepository.cs.uid | 1 + .../Data/FrontendSession.cs | 62 ++++++++++ .../Data/FrontendSession.cs.uid | 1 + .../Data/default_campaign_manifest.json | 22 ++++ .../Scenes/CampaignIntro.tscn | 12 ++ .../Scenes/Controls/CellInspector.tscn | 6 + .../Scenes/Controls/ConfirmDialog.tscn | 6 + .../Scenes/Controls/ForecastList.tscn | 6 + .../Scenes/Controls/InventoryStrip.tscn | 6 + .../Scenes/Controls/LevelHeader.tscn | 6 + .../Scenes/Controls/OutcomeOverlay.tscn | 6 + .../Scenes/Controls/PrimaryButton.tscn | 6 + .../Scenes/Controls/StateBadge.tscn | 6 + .../Scenes/GameOverScreen.tscn | 12 ++ .../Scenes/GameWonScreen.tscn | 12 ++ .../Scenes/GenerationScreen.tscn | 12 ++ .../Scenes/LevelScreen.tscn | 12 ++ .../Scenes/LoseOverlay.tscn | 6 + .../Scenes/MainMenu.tscn | 12 ++ .../Scenes/OptionsScreen.tscn | 12 ++ .../Scenes/SplashScreen.tscn | 12 ++ .../Scenes/TutorialScreen.tscn | 12 ++ .../Scenes/WinOverlay.tscn | 6 + .../Screens/CampaignIntro.cs | 20 ++++ .../Screens/CampaignIntro.cs.uid | 1 + .../Screens/GameOverScreen.cs | 19 +++ .../Screens/GameOverScreen.cs.uid | 1 + .../Screens/GameWonScreen.cs | 15 +++ .../Screens/GameWonScreen.cs.uid | 1 + .../Screens/GenerationScreen.cs | 20 ++++ .../Screens/GenerationScreen.cs.uid | 1 + .../Screens/LevelScreen.cs | 107 +++++++++++++++++ .../Screens/LevelScreen.cs.uid | 1 + .../Screens/LoseOverlay.cs | 19 +++ .../Screens/LoseOverlay.cs.uid | 1 + .../Screens/MainMenu.cs | 26 +++++ .../Screens/MainMenu.cs.uid | 1 + .../Screens/OptionsScreen.cs | 26 +++++ .../Screens/OptionsScreen.cs.uid | 1 + .../Screens/ScreenBase.cs | 58 +++++++++ .../Screens/ScreenBase.cs.uid | 1 + .../Screens/SplashScreen.cs | 33 ++++++ .../Screens/SplashScreen.cs.uid | 1 + .../Screens/TutorialScreen.cs | 18 +++ .../Screens/TutorialScreen.cs.uid | 1 + .../Screens/WinOverlay.cs | 18 +++ .../Screens/WinOverlay.cs.uid | 1 + .../Scripts/AppController.cs | 110 ++++++++++++++++++ .../Scripts/AppController.cs.uid | 1 + src/ReactorMaintenance.Godot/main.tscn | 13 ++- src/ReactorMaintenance.Godot/project.godot | 4 +- 74 files changed, 1060 insertions(+), 8 deletions(-) create mode 100644 src/ReactorMaintenance.Godot/Controls/CellInspector.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/CellInspector.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/ForecastList.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/ForecastList.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/LevelHeader.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/LevelHeader.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Controls/StateBadge.cs create mode 100644 src/ReactorMaintenance.Godot/Controls/StateBadge.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignLevel.cs create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignLevel.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignManifest.cs create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignManifest.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignRepository.cs create mode 100644 src/ReactorMaintenance.Godot/Data/CampaignRepository.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Data/FrontendSession.cs create mode 100644 src/ReactorMaintenance.Godot/Data/FrontendSession.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Data/default_campaign_manifest.json create mode 100644 src/ReactorMaintenance.Godot/Scenes/CampaignIntro.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/CellInspector.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/ConfirmDialog.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/ForecastList.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/InventoryStrip.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/LevelHeader.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/OutcomeOverlay.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/PrimaryButton.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/Controls/StateBadge.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/GameOverScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/GameWonScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/GenerationScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/LevelScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/LoseOverlay.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/MainMenu.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/OptionsScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/SplashScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/TutorialScreen.tscn create mode 100644 src/ReactorMaintenance.Godot/Scenes/WinOverlay.tscn create mode 100644 src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/LevelScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/LevelScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/MainMenu.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/MainMenu.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/ScreenBase.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/ScreenBase.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/SplashScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/SplashScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Screens/WinOverlay.cs create mode 100644 src/ReactorMaintenance.Godot/Screens/WinOverlay.cs.uid create mode 100644 src/ReactorMaintenance.Godot/Scripts/AppController.cs create mode 100644 src/ReactorMaintenance.Godot/Scripts/AppController.cs.uid diff --git a/README.md b/README.md index 1d02d40..9bf6294 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Reactor Maintenance -C# simulation with WinUI 3 + Win2D editor and an empty Godot frontend shell for the deterministic grid simulation described in `docs/design.md`. +C# simulation with WinUI 3 + Win2D editor and a Godot frontend shell for the deterministic grid simulation described in `docs/design.md`. ## Projects - `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults. -- `src/ReactorMaintenance.Godot`: empty Godot 4.5 .NET frontend project shell referencing the simulation core. +- `src/ReactorMaintenance.Godot`: Godot 4.5 .NET frontend project with scene routing, UX blueprint screens, reusable UI controls, and a mock campaign manifest referencing future level JSON files. - `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor. - `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations. @@ -29,3 +29,9 @@ dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.cspro The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment. +## Godot Frontend + +The current Godot frontend is a navigable UX scaffold. It starts at a splash screen, routes through the main menu, campaign intro, random generation placeholder, level screen, win/loss overlays, options, tutorial, game over, and campaign complete screens. + +The mock campaign manifest lives at `src\ReactorMaintenance.Godot\Data\default_campaign_manifest.json`. Each entry includes the future serialized simulation level path; those JSON files are intentionally placeholders for authored levels that will use the simulation `LevelSerializer` format. + diff --git a/docs/UX.md b/docs/UX.md index d4bcdc2..651d58c 100644 --- a/docs/UX.md +++ b/docs/UX.md @@ -19,7 +19,7 @@ SplashScreen MainMenu -> New Campaign -> CampaignIntro or LevelScreen -> Continue Campaign -> LevelScreen - -> Play Random Level -> LevelScreen + -> Play Random Level -> GenerationScreen -> LevelScreen -> OptionsScreen -> TutorialScreen diff --git a/src/ReactorMaintenance.Godot/Controls/CellInspector.cs b/src/ReactorMaintenance.Godot/Controls/CellInspector.cs new file mode 100644 index 0000000..5b9e53b --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/CellInspector.cs @@ -0,0 +1,15 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class CellInspector : PanelContainer +{ + public override void _Ready() + { + AddChild(m_Text); + m_Text.Text = "Selected Cell: 0,0\nTerrain: service floor\nProp: none\nHazards: none\nUnderground: hidden"; + m_Text.AutowrapMode = TextServer.AutowrapMode.WordSmart; + } + + private readonly Label m_Text = new(); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/CellInspector.cs.uid b/src/ReactorMaintenance.Godot/Controls/CellInspector.cs.uid new file mode 100644 index 0000000..f69e349 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/CellInspector.cs.uid @@ -0,0 +1 @@ +uid://d0d3kglv6s32m diff --git a/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs b/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs new file mode 100644 index 0000000..0cdd30d --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs @@ -0,0 +1,33 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class ConfirmDialog : PanelContainer +{ + public event Action? Confirmed; + public event Action? Canceled; + + public void Configure(string title, string message, string confirmText = "Confirm") + { + foreach (var child in GetChildren()) + child.QueueFree(); + + var body = new VBoxContainer(); + AddChild(body); + body.AddChild(new Label { Text = title, HorizontalAlignment = HorizontalAlignment.Center }); + body.AddChild(new Label { Text = message, AutowrapMode = TextServer.AutowrapMode.WordSmart }); + + var actions = new HBoxContainer(); + body.AddChild(actions); + + var confirm = new PrimaryButton(); + confirm.Configure(confirmText); + confirm.Pressed += () => Confirmed?.Invoke(); + actions.AddChild(confirm); + + var cancel = new PrimaryButton(); + cancel.Configure("Cancel"); + cancel.Pressed += () => Canceled?.Invoke(); + actions.AddChild(cancel); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs.uid b/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs.uid new file mode 100644 index 0000000..ff369bf --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/ConfirmDialog.cs.uid @@ -0,0 +1 @@ +uid://cp5qe2n4nwb32 diff --git a/src/ReactorMaintenance.Godot/Controls/ForecastList.cs b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs new file mode 100644 index 0000000..2967c8b --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs @@ -0,0 +1,24 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class ForecastList : PanelContainer +{ + public override void _Ready() + { + AddChild(m_Items); + SetForecasts(["Turn +1: Pressure stable", "Turn +2: No forecast warnings"]); + } + + public void SetForecasts(IReadOnlyList forecasts) + { + foreach (var child in m_Items.GetChildren()) + child.QueueFree(); + + m_Items.AddChild(new Label { Text = "Forecasts" }); + foreach (var forecast in forecasts) + m_Items.AddChild(new Label { Text = forecast, AutowrapMode = TextServer.AutowrapMode.WordSmart }); + } + + private readonly VBoxContainer m_Items = new(); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/ForecastList.cs.uid b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs.uid new file mode 100644 index 0000000..55eacc7 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/ForecastList.cs.uid @@ -0,0 +1 @@ +uid://bswy75n15jxl5 diff --git a/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs b/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs new file mode 100644 index 0000000..a4e422a --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs @@ -0,0 +1,23 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class InventoryStrip : HBoxContainer +{ + public override void _Ready() + { + AddChild(CreateItem("Fuel Neutralizer", 2)); + AddChild(CreateItem("Coolant Neutralizer", 2)); + AddChild(CreateItem("Electric Neutralizer", 1)); + AddChild(CreateItem("Heat Shield", 1)); + } + + private static Label CreateItem(string name, int count) + { + return new() { + Text = $"{name}: {count}", + HorizontalAlignment = HorizontalAlignment.Center, + SizeFlagsHorizontal = SizeFlags.ExpandFill + }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs.uid b/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs.uid new file mode 100644 index 0000000..7181697 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/InventoryStrip.cs.uid @@ -0,0 +1 @@ +uid://d1pdwv570am3v diff --git a/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs b/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs new file mode 100644 index 0000000..9e883e5 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs @@ -0,0 +1,33 @@ +using Godot; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class LevelHeader : HBoxContainer +{ + public override void _Ready() + { + AddChild(m_Title); + AddChild(m_Progress); + AddChild(m_Badge); + AddChild(m_Summary); + + m_Title.SizeFlagsHorizontal = SizeFlags.ExpandFill; + m_Title.AddThemeFontSizeOverride("font_size", 24); + m_Progress.HorizontalAlignment = HorizontalAlignment.Center; + m_Summary.HorizontalAlignment = HorizontalAlignment.Right; + } + + public void SetLevel(CampaignLevel level, int levelNumber, int levelCount, string state) + { + m_Title.Text = level.Name; + m_Progress.Text = $"Level {levelNumber} / {levelCount}"; + m_Badge.SetState(state); + m_Summary.Text = "Heat nominal | Reactor offline"; + } + + private readonly StateBadge m_Badge = new(); + private readonly Label m_Progress = new(); + private readonly Label m_Summary = new(); + private readonly Label m_Title = new(); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs.uid b/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs.uid new file mode 100644 index 0000000..fcffbfd --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/LevelHeader.cs.uid @@ -0,0 +1 @@ +uid://cp50vj0rystlk diff --git a/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs b/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs new file mode 100644 index 0000000..06675cf --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs @@ -0,0 +1,41 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class OutcomeOverlay : PanelContainer +{ + public override void _Ready() + { + CustomMinimumSize = new(420, 220); + AddChild(m_Body); + m_Body.AddChild(m_Title); + m_Body.AddChild(m_Message); + m_Body.AddChild(m_Actions); + m_Title.HorizontalAlignment = HorizontalAlignment.Center; + m_Title.AddThemeFontSizeOverride("font_size", 26); + m_Message.AutowrapMode = TextServer.AutowrapMode.WordSmart; + m_Message.HorizontalAlignment = HorizontalAlignment.Center; + } + + protected void Configure(string title, string message, IReadOnlyList<(string Text, Action Pressed)> actions) + { + m_Title.Text = title; + m_Message.Text = message; + + foreach (var child in m_Actions.GetChildren()) + child.QueueFree(); + + foreach (var action in actions) + { + var button = new PrimaryButton(); + button.Configure(action.Text); + button.Pressed += action.Pressed; + m_Actions.AddChild(button); + } + } + + private readonly HBoxContainer m_Actions = new(); + private readonly VBoxContainer m_Body = new(); + private readonly Label m_Message = new(); + private readonly Label m_Title = new(); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs.uid b/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs.uid new file mode 100644 index 0000000..333a312 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/OutcomeOverlay.cs.uid @@ -0,0 +1 @@ +uid://65iow3r0egvo diff --git a/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs b/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs new file mode 100644 index 0000000..8cd9d7c --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs @@ -0,0 +1,20 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class PrimaryButton : Button +{ + public override void _Ready() + { + FocusMode = FocusModeEnum.All; + CustomMinimumSize = new(220, 44); + SizeFlagsHorizontal = SizeFlags.ExpandFill; + } + + public void Configure(string text, string tooltip = "", bool disabled = false) + { + Text = text; + TooltipText = tooltip; + Disabled = disabled; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs.uid b/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs.uid new file mode 100644 index 0000000..f032619 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/PrimaryButton.cs.uid @@ -0,0 +1 @@ +uid://bxk3mw5bybqxr diff --git a/src/ReactorMaintenance.Godot/Controls/StateBadge.cs b/src/ReactorMaintenance.Godot/Controls/StateBadge.cs new file mode 100644 index 0000000..5539aa0 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/StateBadge.cs @@ -0,0 +1,30 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Controls; + +public partial class StateBadge : Label +{ + public override void _Ready() + { + HorizontalAlignment = HorizontalAlignment.Center; + VerticalAlignment = VerticalAlignment.Center; + CustomMinimumSize = new(96, 28); + } + + public void SetState(string state) + { + Text = state; + AddThemeColorOverride("font_color", GetColor(state)); + } + + private static Color GetColor(string state) + { + return state.ToLowerInvariant() switch { + "stable" => new(0.72f, 0.86f, 0.76f), + "caution" => new(1.0f, 0.76f, 0.24f), + "critical" or "lost" => new(1.0f, 0.36f, 0.32f), + "ready" or "won" => new(0.45f, 1.0f, 0.58f), + _ => Colors.White + }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Controls/StateBadge.cs.uid b/src/ReactorMaintenance.Godot/Controls/StateBadge.cs.uid new file mode 100644 index 0000000..9af7b2a --- /dev/null +++ b/src/ReactorMaintenance.Godot/Controls/StateBadge.cs.uid @@ -0,0 +1 @@ +uid://y7qn5eixkr7j diff --git a/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs b/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs new file mode 100644 index 0000000..4ff7612 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs @@ -0,0 +1,9 @@ +namespace ReactorMaintenance.Godot.Data; + +public sealed record CampaignLevel +{ + public string Id { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string FlavorText { get; init; } = string.Empty; + public string LevelPath { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs.uid b/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs.uid new file mode 100644 index 0000000..08cea8e --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignLevel.cs.uid @@ -0,0 +1 @@ +uid://uis6i7yfkrty diff --git a/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs b/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs new file mode 100644 index 0000000..b92c8b5 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs @@ -0,0 +1,6 @@ +namespace ReactorMaintenance.Godot.Data; + +public sealed record CampaignManifest +{ + public IReadOnlyList Levels { get; init; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs.uid b/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs.uid new file mode 100644 index 0000000..886e048 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignManifest.cs.uid @@ -0,0 +1 @@ +uid://bnyc3qlyksmua diff --git a/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs b/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs new file mode 100644 index 0000000..84b8d9c --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using FileAccess = Godot.FileAccess; + +namespace ReactorMaintenance.Godot.Data; + +public static class CampaignRepository +{ + public static CampaignManifest LoadDefault() + { + var json = FileAccess.GetFileAsString(c_DefaultManifestPath); + if (string.IsNullOrWhiteSpace(json)) + return CreateFallback(); + + var manifest = JsonSerializer.Deserialize(json, s_Options); + return manifest is { Levels.Count: > 0 } ? manifest : CreateFallback(); + } + + private static CampaignManifest CreateFallback() + { + return new() { + Levels = [ + new() { + Id = "fallback", + Name = "Fallback Reactor", + FlavorText = "A placeholder level loaded because the campaign manifest was unavailable.", + LevelPath = "res://Data/Levels/fallback.json" + } + ] + }; + } + + private const string c_DefaultManifestPath = "res://Data/default_campaign_manifest.json"; + + private static readonly JsonSerializerOptions s_Options = new() { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs.uid b/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs.uid new file mode 100644 index 0000000..f97f437 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/CampaignRepository.cs.uid @@ -0,0 +1 @@ +uid://bufo8npdbreb1 diff --git a/src/ReactorMaintenance.Godot/Data/FrontendSession.cs b/src/ReactorMaintenance.Godot/Data/FrontendSession.cs new file mode 100644 index 0000000..1fae9e2 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/FrontendSession.cs @@ -0,0 +1,62 @@ +namespace ReactorMaintenance.Godot.Data; + +public sealed class FrontendSession +{ + public FrontendSession(CampaignManifest campaign) + { + m_Campaign = campaign; + } + + public void StartNewCampaign() + { + IsRandomLevel = false; + CampaignIndex = 0; + HasContinue = true; + } + + public void ContinueCampaign() + { + IsRandomLevel = false; + HasContinue = true; + } + + public void StartRandomLevel() + { + IsRandomLevel = true; + } + + public void MarkCurrentLevelLoaded() + { + if (!IsRandomLevel) + HasContinue = true; + } + + public void AdvanceToNextLevel() + { + if (HasNextLevel) + CampaignIndex++; + } + + public void CompleteCampaign() + { + IsRandomLevel = false; + CampaignIndex = 0; + HasContinue = false; + } + + public bool HasContinue { get; private set; } + public bool IsRandomLevel { get; private set; } + public int CampaignIndex { get; private set; } + public CampaignLevel CurrentLevel => IsRandomLevel ? m_RandomLevel : m_Campaign.Levels[CampaignIndex]; + public int LevelNumber => IsRandomLevel ? 1 : CampaignIndex + 1; + public int LevelCount => IsRandomLevel ? 1 : m_Campaign.Levels.Count; + public bool HasNextLevel => !IsRandomLevel && CampaignIndex + 1 < m_Campaign.Levels.Count; + private readonly CampaignManifest m_Campaign; + + private readonly CampaignLevel m_RandomLevel = new() { + Id = "random-maintenance", + Name = "Random Maintenance Shift", + FlavorText = "A generated shift will use authored JSON level data until generation is implemented.", + LevelPath = "res://Data/Levels/random_placeholder.json" + }; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Data/FrontendSession.cs.uid b/src/ReactorMaintenance.Godot/Data/FrontendSession.cs.uid new file mode 100644 index 0000000..5a0ad9b --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/FrontendSession.cs.uid @@ -0,0 +1 @@ +uid://bsbukw3qpo0by diff --git a/src/ReactorMaintenance.Godot/Data/default_campaign_manifest.json b/src/ReactorMaintenance.Godot/Data/default_campaign_manifest.json new file mode 100644 index 0000000..0848a66 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Data/default_campaign_manifest.json @@ -0,0 +1,22 @@ +{ + "levels": [ + { + "id": "coolant-restart", + "name": "Coolant Restart", + "flavorText": "The lower coolant loop is starving the reactor core. Restore enough service to keep the first startup window alive.", + "levelPath": "res://Data/Levels/coolant_restart.json" + }, + { + "id": "fuel-bleed", + "name": "Fuel Bleed", + "flavorText": "A fuel manifold is venting through the maintenance deck. Isolate the leak before pressure cascades into the ignition zone.", + "levelPath": "res://Data/Levels/fuel_bleed.json" + }, + { + "id": "black-start", + "name": "Black Start", + "flavorText": "The final reactor needs fuel, coolant, and electricity in balance before the station can carry load again.", + "levelPath": "res://Data/Levels/black_start.json" + } + ] +} diff --git a/src/ReactorMaintenance.Godot/Scenes/CampaignIntro.tscn b/src/ReactorMaintenance.Godot/Scenes/CampaignIntro.tscn new file mode 100644 index 0000000..4c071e9 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/CampaignIntro.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/CampaignIntro.cs" id="1"] + +[node name="CampaignIntro" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/CellInspector.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/CellInspector.tscn new file mode 100644 index 0000000..05c3d6e --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/CellInspector.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/CellInspector.cs" id="1"] + +[node name="CellInspector" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/ConfirmDialog.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/ConfirmDialog.tscn new file mode 100644 index 0000000..42e7928 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/ConfirmDialog.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/ConfirmDialog.cs" id="1"] + +[node name="ConfirmDialog" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/ForecastList.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/ForecastList.tscn new file mode 100644 index 0000000..91cb813 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/ForecastList.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/ForecastList.cs" id="1"] + +[node name="ForecastList" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/InventoryStrip.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/InventoryStrip.tscn new file mode 100644 index 0000000..e0ae215 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/InventoryStrip.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/InventoryStrip.cs" id="1"] + +[node name="InventoryStrip" type="HBoxContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/LevelHeader.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/LevelHeader.tscn new file mode 100644 index 0000000..14fc664 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/LevelHeader.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/LevelHeader.cs" id="1"] + +[node name="LevelHeader" type="HBoxContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/OutcomeOverlay.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/OutcomeOverlay.tscn new file mode 100644 index 0000000..279992b --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/OutcomeOverlay.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/OutcomeOverlay.cs" id="1"] + +[node name="OutcomeOverlay" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/PrimaryButton.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/PrimaryButton.tscn new file mode 100644 index 0000000..9cf6cf1 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/PrimaryButton.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/PrimaryButton.cs" id="1"] + +[node name="PrimaryButton" type="Button"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/Controls/StateBadge.tscn b/src/ReactorMaintenance.Godot/Scenes/Controls/StateBadge.tscn new file mode 100644 index 0000000..e142c46 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/Controls/StateBadge.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Controls/StateBadge.cs" id="1"] + +[node name="StateBadge" type="Label"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/GameOverScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/GameOverScreen.tscn new file mode 100644 index 0000000..75c43f2 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/GameOverScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/GameOverScreen.cs" id="1"] + +[node name="GameOverScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/GameWonScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/GameWonScreen.tscn new file mode 100644 index 0000000..8034dc6 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/GameWonScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/GameWonScreen.cs" id="1"] + +[node name="GameWonScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/GenerationScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/GenerationScreen.tscn new file mode 100644 index 0000000..575f51e --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/GenerationScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/GenerationScreen.cs" id="1"] + +[node name="GenerationScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/LevelScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/LevelScreen.tscn new file mode 100644 index 0000000..67b4672 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/LevelScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/LevelScreen.cs" id="1"] + +[node name="LevelScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/LoseOverlay.tscn b/src/ReactorMaintenance.Godot/Scenes/LoseOverlay.tscn new file mode 100644 index 0000000..ae72da7 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/LoseOverlay.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/LoseOverlay.cs" id="1"] + +[node name="LoseOverlay" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/MainMenu.tscn b/src/ReactorMaintenance.Godot/Scenes/MainMenu.tscn new file mode 100644 index 0000000..48c2435 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/MainMenu.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/MainMenu.cs" id="1"] + +[node name="MainMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/OptionsScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/OptionsScreen.tscn new file mode 100644 index 0000000..cb7e76c --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/OptionsScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/OptionsScreen.cs" id="1"] + +[node name="OptionsScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/SplashScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/SplashScreen.tscn new file mode 100644 index 0000000..0734b7c --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/SplashScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/SplashScreen.cs" id="1"] + +[node name="SplashScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/TutorialScreen.tscn b/src/ReactorMaintenance.Godot/Scenes/TutorialScreen.tscn new file mode 100644 index 0000000..b3db432 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/TutorialScreen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/TutorialScreen.cs" id="1"] + +[node name="TutorialScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Scenes/WinOverlay.tscn b/src/ReactorMaintenance.Godot/Scenes/WinOverlay.tscn new file mode 100644 index 0000000..79880b4 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scenes/WinOverlay.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://Screens/WinOverlay.cs" id="1"] + +[node name="WinOverlay" type="PanelContainer"] +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs b/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs new file mode 100644 index 0000000..c302301 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs @@ -0,0 +1,20 @@ +using Godot; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class CampaignIntro : ScreenBase +{ + public void Configure(AppController app, CampaignLevel level, int levelNumber, int levelCount) + { + var body = CreatePage(level.Name); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText($"Campaign level {levelNumber} / {levelCount}")); + body.AddChild(CreateBodyText(level.FlavorText)); + + var actions = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter }; + body.AddChild(actions); + actions.AddChild(CreateButton("Begin", app.LoadCurrentLevel)); + actions.AddChild(CreateButton("Back", app.ShowMainMenu)); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs.uid b/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs.uid new file mode 100644 index 0000000..1a09cf1 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/CampaignIntro.cs.uid @@ -0,0 +1 @@ +uid://cd0ffydc3pdc2 diff --git a/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs b/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs new file mode 100644 index 0000000..d622684 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs @@ -0,0 +1,19 @@ +using Godot; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class GameOverScreen : ScreenBase +{ + public void Configure(AppController app, CampaignLevel level) + { + var body = CreatePage("Game Over"); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText($"Failed level: {level.Name}")); + + var actions = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter }; + body.AddChild(actions); + actions.AddChild(CreateButton("Retry Current Level", app.RetryCurrentLevel)); + actions.AddChild(CreateButton("Main Menu", app.ShowMainMenu)); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs.uid new file mode 100644 index 0000000..8296380 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GameOverScreen.cs.uid @@ -0,0 +1 @@ +uid://d1ncb4ysjstdd diff --git a/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs b/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs new file mode 100644 index 0000000..57f9fcc --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs @@ -0,0 +1,15 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class GameWonScreen : ScreenBase +{ + public void Configure(AppController app, int completedLevels) + { + var body = CreatePage("Campaign Complete"); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText($"All {completedLevels} handcrafted reactor incidents are resolved.")); + body.AddChild(CreateBodyText("The maintenance crew has restored enough capacity to bring the station back online.")); + body.AddChild(CreateButton("Main Menu", app.ShowMainMenu)); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs.uid new file mode 100644 index 0000000..9f2ad38 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GameWonScreen.cs.uid @@ -0,0 +1 @@ +uid://cfijlvnlio424 diff --git a/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs b/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs new file mode 100644 index 0000000..02a5e40 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs @@ -0,0 +1,20 @@ +using Godot; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class GenerationScreen : ScreenBase +{ + public void Configure(AppController app, CampaignLevel level) + { + var body = CreatePage("Random Level"); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText(level.FlavorText)); + body.AddChild(CreateBodyText($"Future level JSON: {level.LevelPath}")); + + var actions = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter }; + body.AddChild(actions); + actions.AddChild(CreateButton("Begin", app.LoadCurrentLevel)); + actions.AddChild(CreateButton("Back", app.ShowMainMenu)); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs.uid new file mode 100644 index 0000000..f5348fe --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/GenerationScreen.cs.uid @@ -0,0 +1 @@ +uid://cs63x3a7h8pad diff --git a/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs new file mode 100644 index 0000000..e251b93 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs @@ -0,0 +1,107 @@ +using Godot; +using ReactorMaintenance.Godot.Controls; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class LevelScreen : ScreenBase +{ + public void Configure(AppController app, CampaignLevel level, int levelNumber, int levelCount) + { + m_App = app; + m_Level = level; + + var body = CreatePage(string.Empty); + var header = new LevelHeader(); + body.AddChild(header); + header.SetLevel(level, levelNumber, levelCount, "Stable"); + + var flavor = CreateBodyText(level.FlavorText); + flavor.HorizontalAlignment = HorizontalAlignment.Left; + body.AddChild(flavor); + + var split = new HSplitContainer { SizeFlagsVertical = SizeFlags.ExpandFill }; + body.AddChild(split); + + split.AddChild(CreateGridPlaceholder()); + split.AddChild(CreateSidePanel(level)); + + body.AddChild(new InventoryStrip()); + body.AddChild(CreateActionBar()); + + m_OverlayLayer = new(); + AddChild(m_OverlayLayer); + } + + private static PanelContainer CreateGridPlaceholder() + { + var panel = new PanelContainer { CustomMinimumSize = new(560, 360), SizeFlagsHorizontal = SizeFlags.ExpandFill, SizeFlagsVertical = SizeFlags.ExpandFill }; + var label = new Label { + Text = "Level Grid Placeholder\nRobot marker, terrain, props, hazards, and underground overlays will render here.", + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + AutowrapMode = TextServer.AutowrapMode.WordSmart + }; + panel.AddChild(label); + return panel; + } + + private static VBoxContainer CreateSidePanel(CampaignLevel level) + { + var sidePanel = new VBoxContainer { CustomMinimumSize = new(300, 0) }; + sidePanel.AddChild(new Label { Text = "Inspector" }); + sidePanel.AddChild(new CellInspector()); + sidePanel.AddChild(new ForecastList()); + sidePanel.AddChild(new Label { Text = $"Level JSON: {level.LevelPath}", AutowrapMode = TextServer.AutowrapMode.WordSmart }); + return sidePanel; + } + + private HBoxContainer CreateActionBar() + { + var actions = new HBoxContainer(); + actions.AddChild(CreateButton("Move", () => { }, "Quick action placeholder")); + actions.AddChild(CreateButton("Interact", () => { }, "Lengthy action placeholder")); + actions.AddChild(CreateButton("Repair", () => { }, "Lengthy action placeholder")); + actions.AddChild(CreateButton("Trigger Win", ShowWinOverlay)); + actions.AddChild(CreateButton("Trigger Lose", ShowLoseOverlay)); + actions.AddChild(CreateButton("Main Menu", () => m_App?.ShowMainMenu())); + return actions; + } + + private void ShowLoseOverlay() + { + if (m_OutcomeVisible || m_App is null || m_Level is null || m_OverlayLayer is null) + return; + + m_OutcomeVisible = true; + var overlay = new LoseOverlay(); + overlay.Configure(m_Level, m_App.RetryCurrentLevel, m_App.ShowGameOver, m_App.ShowMainMenu); + AddCenteredOverlay(overlay); + } + + private void ShowWinOverlay() + { + if (m_OutcomeVisible || m_App is null || m_Level is null || m_OverlayLayer is null) + return; + + m_OutcomeVisible = true; + var overlay = new WinOverlay(); + overlay.Configure(m_Level, m_App.CompleteCurrentLevel, m_App.ShowMainMenu); + AddCenteredOverlay(overlay); + } + + private void AddCenteredOverlay(Control overlay) + { + var center = new CenterContainer { + AnchorRight = 1, + AnchorBottom = 1 + }; + center.AddChild(overlay); + m_OverlayLayer?.AddChild(center); + } + + private AppController? m_App; + private CampaignLevel? m_Level; + private bool m_OutcomeVisible; + private CanvasLayer? m_OverlayLayer; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs.uid new file mode 100644 index 0000000..1d82042 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/LevelScreen.cs.uid @@ -0,0 +1 @@ +uid://dix1dibqgxr3y diff --git a/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs b/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs new file mode 100644 index 0000000..d49b185 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs @@ -0,0 +1,19 @@ +using ReactorMaintenance.Godot.Controls; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class LoseOverlay : OutcomeOverlay +{ + public void Configure(CampaignLevel level, Action retryLevel, Action openGameOver, Action mainMenu) + { + Configure( + "Level Lost", + $"{level.Name} has failed. Retry reloads the level from its starting snapshot.", + [ + ("Retry Level", retryLevel), + ("Game Over", openGameOver), + ("Main Menu", mainMenu) + ]); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs.uid b/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs.uid new file mode 100644 index 0000000..22cb117 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/LoseOverlay.cs.uid @@ -0,0 +1 @@ +uid://bvonbqula7dxq diff --git a/src/ReactorMaintenance.Godot/Screens/MainMenu.cs b/src/ReactorMaintenance.Godot/Screens/MainMenu.cs new file mode 100644 index 0000000..b515ed9 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/MainMenu.cs @@ -0,0 +1,26 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class MainMenu : ScreenBase +{ + public void Configure(AppController app, bool hasContinue) + { + var body = CreatePage("Reactor Maintenance"); + body.Alignment = BoxContainer.AlignmentMode.Center; + + var commands = new VBoxContainer { + CustomMinimumSize = new(320, 0), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter + }; + body.AddChild(commands); + + commands.AddChild(CreateButton("New Campaign", app.StartNewCampaign)); + commands.AddChild(CreateButton("Continue Campaign", app.ContinueCampaign, "No campaign progress exists yet.", !hasContinue)); + commands.AddChild(CreateButton("Play Random Level", app.StartRandomLevel)); + commands.AddChild(CreateButton("Options", app.ShowOptions)); + commands.AddChild(CreateButton("Tutorial", app.ShowTutorial)); + + body.AddChild(CreateBodyText("Godot frontend prototype")); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/MainMenu.cs.uid b/src/ReactorMaintenance.Godot/Screens/MainMenu.cs.uid new file mode 100644 index 0000000..1e39862 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/MainMenu.cs.uid @@ -0,0 +1 @@ +uid://dj4he4y5anmdy diff --git a/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs b/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs new file mode 100644 index 0000000..000c0fd --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs @@ -0,0 +1,26 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class OptionsScreen : ScreenBase +{ + public void Configure(AppController app) + { + var body = CreatePage("Options"); + body.Alignment = BoxContainer.AlignmentMode.Center; + + body.AddChild(CreateSlider("Master Volume")); + body.AddChild(CreateSlider("Music Volume")); + body.AddChild(new CheckBox { Text = "Fullscreen" }); + body.AddChild(new OptionButton { Text = "UI Scale" }); + body.AddChild(CreateButton("Back", app.ShowMainMenu)); + } + + private static HBoxContainer CreateSlider(string label) + { + var row = new HBoxContainer { CustomMinimumSize = new(360, 40), SizeFlagsHorizontal = SizeFlags.ShrinkCenter }; + row.AddChild(new Label { Text = label, CustomMinimumSize = new(140, 0) }); + row.AddChild(new HSlider { MinValue = 0, MaxValue = 100, Value = 75, SizeFlagsHorizontal = SizeFlags.ExpandFill }); + return row; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs.uid new file mode 100644 index 0000000..0015f57 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/OptionsScreen.cs.uid @@ -0,0 +1 @@ +uid://bkf0ctd7yk656 diff --git a/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs b/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs new file mode 100644 index 0000000..9040f11 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs @@ -0,0 +1,58 @@ +using Godot; +using ReactorMaintenance.Godot.Controls; + +namespace ReactorMaintenance.Godot.Screens; + +public abstract partial class ScreenBase : Control +{ + protected VBoxContainer CreatePage(string title) + { + foreach (var child in GetChildren()) + child.QueueFree(); + + SetAnchorsPreset(LayoutPreset.FullRect); + + var margin = new MarginContainer { + AnchorRight = 1, + AnchorBottom = 1 + }; + margin.AddThemeConstantOverride("margin_left", 48); + margin.AddThemeConstantOverride("margin_top", 36); + margin.AddThemeConstantOverride("margin_right", 48); + margin.AddThemeConstantOverride("margin_bottom", 36); + AddChild(margin); + + var body = new VBoxContainer { + SizeFlagsHorizontal = SizeFlags.ExpandFill, + SizeFlagsVertical = SizeFlags.ExpandFill + }; + margin.AddChild(body); + + var titleLabel = new Label { + Text = title, + HorizontalAlignment = HorizontalAlignment.Center + }; + titleLabel.AddThemeFontSizeOverride("font_size", 32); + body.AddChild(titleLabel); + + return body; + } + + protected static PrimaryButton CreateButton(string text, Action pressed, string tooltip = "", bool disabled = false) + { + var button = new PrimaryButton(); + button.Configure(text, tooltip, disabled); + button.Pressed += pressed; + return button; + } + + protected static Label CreateBodyText(string text) + { + return new() { + Text = text, + AutowrapMode = TextServer.AutowrapMode.WordSmart, + HorizontalAlignment = HorizontalAlignment.Center, + SizeFlagsHorizontal = SizeFlags.ExpandFill + }; + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs.uid b/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs.uid new file mode 100644 index 0000000..59f401f --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/ScreenBase.cs.uid @@ -0,0 +1 @@ +uid://dcwn6wqeikkpn diff --git a/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs b/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs new file mode 100644 index 0000000..238ae81 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs @@ -0,0 +1,33 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class SplashScreen : ScreenBase +{ + public event Action? Started; + + public override void _Ready() + { + var body = CreatePage("Reactor Maintenance"); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText("Systems booting...")); + GetTree().CreateTimer(1.2).Timeout += Start; + } + + public override void _UnhandledInput(InputEvent @event) + { + if (@event.IsPressed()) + Start(); + } + + private void Start() + { + if (m_Transitioned) + return; + + m_Transitioned = true; + Started?.Invoke(); + } + + private bool m_Transitioned; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs.uid new file mode 100644 index 0000000..008b3f9 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/SplashScreen.cs.uid @@ -0,0 +1 @@ +uid://yiggx6pgqntg diff --git a/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs b/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs new file mode 100644 index 0000000..59ae105 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs @@ -0,0 +1,18 @@ +using Godot; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class TutorialScreen : ScreenBase +{ + public void Configure(AppController app) + { + var body = CreatePage("Tutorial"); + body.Alignment = BoxContainer.AlignmentMode.Center; + body.AddChild(CreateBodyText("Topics: action economy, hazards, forecasts, remedies, heat shields, and reactor activation.")); + + var actions = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter }; + body.AddChild(actions); + actions.AddChild(CreateButton("Start Tutorial Level", app.StartRandomLevel)); + actions.AddChild(CreateButton("Back", app.ShowMainMenu)); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs.uid b/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs.uid new file mode 100644 index 0000000..d73d890 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/TutorialScreen.cs.uid @@ -0,0 +1 @@ +uid://xfq61pdayub7 diff --git a/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs b/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs new file mode 100644 index 0000000..e7e391c --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs @@ -0,0 +1,18 @@ +using ReactorMaintenance.Godot.Controls; +using ReactorMaintenance.Godot.Data; + +namespace ReactorMaintenance.Godot.Screens; + +public partial class WinOverlay : OutcomeOverlay +{ + public void Configure(CampaignLevel level, Action nextLevel, Action mainMenu) + { + Configure( + "Reactor Online", + $"{level.Name} is stabilized. Continue loads the next campaign level or campaign completion screen.", + [ + ("Continue", nextLevel), + ("Main Menu", mainMenu) + ]); + } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs.uid b/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs.uid new file mode 100644 index 0000000..d8d5f80 --- /dev/null +++ b/src/ReactorMaintenance.Godot/Screens/WinOverlay.cs.uid @@ -0,0 +1 @@ +uid://cqiqwgr0usmki diff --git a/src/ReactorMaintenance.Godot/Scripts/AppController.cs b/src/ReactorMaintenance.Godot/Scripts/AppController.cs new file mode 100644 index 0000000..644fcad --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scripts/AppController.cs @@ -0,0 +1,110 @@ +using Godot; +using ReactorMaintenance.Godot.Data; +using ReactorMaintenance.Godot.Screens; + +namespace ReactorMaintenance.Godot; + +public partial class AppController : Control +{ + public override void _Ready() + { + ShowSplash(); + } + + public void ShowSplash() + { + ShowScreen("res://Scenes/SplashScreen.tscn", screen => screen.Started += ShowMainMenu); + } + + public void ShowMainMenu() + { + ShowScreen("res://Scenes/MainMenu.tscn", screen => screen.Configure(this, m_Session.HasContinue)); + } + + public void StartNewCampaign() + { + m_Session.StartNewCampaign(); + ShowCampaignIntro(); + } + + public void ContinueCampaign() + { + m_Session.ContinueCampaign(); + LoadCurrentLevel(); + } + + public void StartRandomLevel() + { + m_Session.StartRandomLevel(); + ShowScreen("res://Scenes/GenerationScreen.tscn", screen => screen.Configure(this, m_Session.CurrentLevel)); + } + + public void ShowCampaignIntro() + { + ShowScreen("res://Scenes/CampaignIntro.tscn", screen => screen.Configure(this, m_Session.CurrentLevel, m_Session.LevelNumber, m_Session.LevelCount)); + } + + public void LoadCurrentLevel() + { + m_Session.MarkCurrentLevelLoaded(); + ShowScreen("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, m_Session.CurrentLevel, m_Session.LevelNumber, m_Session.LevelCount)); + } + + public void RetryCurrentLevel() + { + LoadCurrentLevel(); + } + + public void CompleteCurrentLevel() + { + if (m_Session.IsRandomLevel) + { + StartRandomLevel(); + return; + } + + if (m_Session.HasNextLevel) + { + m_Session.AdvanceToNextLevel(); + ShowCampaignIntro(); + return; + } + + m_Session.CompleteCampaign(); + ShowGameWon(); + } + + public void ShowOptions() + { + ShowScreen("res://Scenes/OptionsScreen.tscn", screen => screen.Configure(this)); + } + + public void ShowTutorial() + { + ShowScreen("res://Scenes/TutorialScreen.tscn", screen => screen.Configure(this)); + } + + public void ShowGameOver() + { + ShowScreen("res://Scenes/GameOverScreen.tscn", screen => screen.Configure(this, m_Session.CurrentLevel)); + } + + private void ShowGameWon() + { + ShowScreen("res://Scenes/GameWonScreen.tscn", screen => screen.Configure(this, m_Session.LevelCount)); + } + + private void ShowScreen(string scenePath, Action configure) where TScreen : Control + { + m_CurrentScreen?.QueueFree(); + + var packedScene = ResourceLoader.Load(scenePath); + var screen = packedScene.Instantiate(); + m_CurrentScreen = screen; + AddChild(screen); + configure(screen); + } + + private readonly FrontendSession m_Session = new(CampaignRepository.LoadDefault()); + private Control? m_CurrentScreen; +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Godot/Scripts/AppController.cs.uid b/src/ReactorMaintenance.Godot/Scripts/AppController.cs.uid new file mode 100644 index 0000000..82c7f2b --- /dev/null +++ b/src/ReactorMaintenance.Godot/Scripts/AppController.cs.uid @@ -0,0 +1 @@ +uid://exf77g2sqb6d diff --git a/src/ReactorMaintenance.Godot/main.tscn b/src/ReactorMaintenance.Godot/main.tscn index b117564..1b1c816 100644 --- a/src/ReactorMaintenance.Godot/main.tscn +++ b/src/ReactorMaintenance.Godot/main.tscn @@ -1,3 +1,12 @@ -[gd_scene format=3 uid="uid://bhhppnvto57ht"] +[gd_scene load_steps=2 format=3 uid="uid://bhhppnvto57ht"] -[node name="Node2D" type="Node2D"] +[ext_resource type="Script" path="res://Scripts/AppController.cs" id="1"] + +[node name="Main" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/src/ReactorMaintenance.Godot/project.godot b/src/ReactorMaintenance.Godot/project.godot index facd18f..481edfd 100644 --- a/src/ReactorMaintenance.Godot/project.godot +++ b/src/ReactorMaintenance.Godot/project.godot @@ -8,12 +8,10 @@ config_version=5 -"config_version"=5 - [application] config/name="Reactor Maintenance" -run/main_scene="uid://bhhppnvto57ht" +run/main_scene="res://main.tscn" config/features=PackedStringArray("4.5", "C#", "Forward Plus") [dotnet]