Simulation bridge
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using Godot;
|
||||
using Godot;
|
||||
using ReactorMaintenance.Simulation;
|
||||
using System.Text;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
@@ -18,9 +20,31 @@ public partial class CellInspector : PanelContainer
|
||||
body.AddChild(header);
|
||||
|
||||
body.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;
|
||||
}
|
||||
|
||||
public void SetCellInfo(GridPosition? position, ECellTerrain terrain, EPropType prop, EConsumerServiceState serviceState, float fuelHazard, float coolantHazard, float electricityHazard, float heatHazard)
|
||||
{
|
||||
var pos = position ?? new(-1, -1);
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"Selected Cell: {pos.X},{pos.Y}");
|
||||
sb.AppendLine($"Terrain: {terrain}");
|
||||
sb.AppendLine($"Prop: {prop}");
|
||||
if (prop != EPropType.None)
|
||||
sb.AppendLine($"Service: {serviceState}");
|
||||
sb.AppendLine($"Hazards: {FormatHazards(fuelHazard, coolantHazard, electricityHazard, heatHazard)}");
|
||||
m_Text.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private static string FormatHazards(float fuel, float coolant, float electricity, float heat)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (fuel > 0) parts.Add($"fuel {fuel:F1}");
|
||||
if (coolant > 0) parts.Add($"coolant {coolant:F1}");
|
||||
if (electricity > 0) parts.Add($"electricity {electricity:F1}");
|
||||
if (heat > 0) parts.Add($"heat {heat:F1}");
|
||||
return parts.Count > 0 ? string.Join(", ", parts) : "none";
|
||||
}
|
||||
|
||||
private readonly Label m_Text = new();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Godot;
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
@@ -7,17 +8,44 @@ public partial class ForecastList : PanelContainer
|
||||
public override void _Ready()
|
||||
{
|
||||
AddChild(m_Items);
|
||||
SetForecasts(["Turn +1: Pressure stable", "Turn +2: No forecast warnings"]);
|
||||
SetForecasts(Array.Empty<Forecast>());
|
||||
}
|
||||
|
||||
public void SetForecasts(IReadOnlyList<string> forecasts)
|
||||
public void SetForecasts(IReadOnlyList<Forecast> 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 });
|
||||
{
|
||||
var label = new Label {
|
||||
Text = FormatForecast(forecast),
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart
|
||||
};
|
||||
ApplyForecastColor(label, forecast.Kind);
|
||||
m_Items.AddChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatForecast(Forecast forecast)
|
||||
{
|
||||
var pos = forecast.Position;
|
||||
var posStr = pos != null ? $" [{pos.X},{pos.Y}]" : "";
|
||||
return $"Turn +{forecast.Turns}: {forecast.Message}{posStr}";
|
||||
}
|
||||
|
||||
private static void ApplyForecastColor(Label label, EForecastKind kind)
|
||||
{
|
||||
var color = kind switch {
|
||||
EForecastKind.TerminalLoss => new(1.0f, 0.36f, 0.32f),
|
||||
EForecastKind.ConsumerStarved => new(1.0f, 0.6f, 0.2f),
|
||||
EForecastKind.HazardGrowth => new(1.0f, 0.8f, 0.2f),
|
||||
EForecastKind.StructuralIntegrity => new(0.6f, 0.8f, 1.0f),
|
||||
EForecastKind.ReactorReady => new(0.45f, 1.0f, 0.58f),
|
||||
_ => Colors.White
|
||||
};
|
||||
label.AddThemeColorOverride("font_color", color);
|
||||
}
|
||||
|
||||
private readonly VBoxContainer m_Items = new();
|
||||
|
||||
@@ -4,20 +4,6 @@ namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
internal static class FrontendAssets
|
||||
{
|
||||
public const string CoolantIcon = "res://Assets/Ui/coolant_icon.png";
|
||||
public const string ElectricIcon = "res://Assets/Ui/electric_icon.png";
|
||||
public const string FuelIcon = "res://Assets/Ui/fuel_icon.png";
|
||||
public const string HeatShieldIcon = "res://Assets/Ui/heat_shield_icon.png";
|
||||
public const string MaintenanceRobot = "res://Assets/Characters/maintenance_robot.png";
|
||||
public const string PrimaryButtonAccent = "res://Assets/Ui/primary_button_accent.png";
|
||||
public const string ScannerEyeIcon = "res://Assets/Ui/scanner_eye_icon.png";
|
||||
public const string StateBadgeFrame = "res://Assets/Ui/state_badge_frame.png";
|
||||
|
||||
public static Texture2D? LoadTexture(string path)
|
||||
{
|
||||
return ResourceLoader.Exists(path) ? ResourceLoader.Load<Texture2D>(path) : null;
|
||||
}
|
||||
|
||||
public static TextureRect CreateIcon(string path, Vector2 size)
|
||||
{
|
||||
return new() {
|
||||
@@ -28,4 +14,18 @@ internal static class FrontendAssets
|
||||
MouseFilter = Control.MouseFilterEnum.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static Texture2D? LoadTexture(string path)
|
||||
{
|
||||
return ResourceLoader.Exists(path) ? ResourceLoader.Load<Texture2D>(path) : null;
|
||||
}
|
||||
|
||||
public const string CoolantIcon = "res://Assets/Ui/coolant_icon.png";
|
||||
public const string ElectricIcon = "res://Assets/Ui/electric_icon.png";
|
||||
public const string FuelIcon = "res://Assets/Ui/fuel_icon.png";
|
||||
public const string HeatShieldIcon = "res://Assets/Ui/heat_shield_icon.png";
|
||||
public const string MaintenanceRobot = "res://Assets/Characters/maintenance_robot.png";
|
||||
public const string PrimaryButtonAccent = "res://Assets/Ui/primary_button_accent.png";
|
||||
public const string ScannerEyeIcon = "res://Assets/Ui/scanner_eye_icon.png";
|
||||
public const string StateBadgeFrame = "res://Assets/Ui/state_badge_frame.png";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Godot;
|
||||
using Godot;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
@@ -6,23 +6,43 @@ public partial class InventoryStrip : HBoxContainer
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
AddChild(CreateItem("Fuel Neutralizer", 2, FrontendAssets.FuelIcon));
|
||||
AddChild(CreateItem("Coolant Neutralizer", 2, FrontendAssets.CoolantIcon));
|
||||
AddChild(CreateItem("Electric Neutralizer", 1, FrontendAssets.ElectricIcon));
|
||||
AddChild(CreateItem("Heat Shield", 1, FrontendAssets.HeatShieldIcon));
|
||||
AddChild(CreateItem("Fuel Neutralizer", 2, FrontendAssets.FuelIcon, out var fuelLabel));
|
||||
AddChild(CreateItem("Coolant Neutralizer", 2, FrontendAssets.CoolantIcon, out var coolantLabel));
|
||||
AddChild(CreateItem("Electric Neutralizer", 1, FrontendAssets.ElectricIcon, out var electricLabel));
|
||||
AddChild(CreateItem("Heat Shield", 1, FrontendAssets.HeatShieldIcon, out var heatLabel));
|
||||
m_FuelLabel = fuelLabel;
|
||||
m_CoolantLabel = coolantLabel;
|
||||
m_ElectricLabel = electricLabel;
|
||||
m_HeatLabel = heatLabel;
|
||||
}
|
||||
|
||||
private static HBoxContainer CreateItem(string name, int count, string iconPath)
|
||||
public void SetInventory(int fuelNeutralizers, int coolantNeutralizers, int electricNeutralizers, int heatShields)
|
||||
{
|
||||
m_FuelLabel.Text = $"{fuelNeutralizers}";
|
||||
m_CoolantLabel.Text = $"{coolantNeutralizers}";
|
||||
m_ElectricLabel.Text = $"{electricNeutralizers}";
|
||||
m_HeatLabel.Text = $"{heatShields}";
|
||||
}
|
||||
|
||||
private static HBoxContainer CreateItem(string name, int count, string iconPath, out Label countLabel)
|
||||
{
|
||||
var item = new HBoxContainer {
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill
|
||||
};
|
||||
item.AddChild(FrontendAssets.CreateIcon(iconPath, new(30, 30)));
|
||||
item.AddChild(new Label {
|
||||
Text = $"{name}: {count}",
|
||||
countLabel = new() {
|
||||
Text = $"{count}",
|
||||
Name = name,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill
|
||||
});
|
||||
};
|
||||
item.AddChild(countLabel);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private Label m_CoolantLabel = null!;
|
||||
private Label m_ElectricLabel = null!;
|
||||
|
||||
private Label m_FuelLabel = null!;
|
||||
private Label m_HeatLabel = null!;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Godot;
|
||||
using ReactorMaintenance.Godot.Data;
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
@@ -18,12 +18,25 @@ public partial class LevelHeader : HBoxContainer
|
||||
m_Summary.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
}
|
||||
|
||||
public void SetLevel(CampaignLevel level, int levelNumber, int levelCount, string state)
|
||||
public void SetLevel(string title, int levelNumber, int levelCount, ELevelState state)
|
||||
{
|
||||
m_Title.Text = level.Name;
|
||||
m_Title.Text = title;
|
||||
m_Progress.Text = $"Level {levelNumber} / {levelCount}";
|
||||
m_Badge.SetState(state);
|
||||
m_Summary.Text = "Heat nominal | Reactor offline";
|
||||
m_Badge.SetState(StateToString(state));
|
||||
m_Summary.Text = "Heat: nominal | Reactor: offline";
|
||||
}
|
||||
|
||||
private static string StateToString(ELevelState state)
|
||||
{
|
||||
return state switch {
|
||||
ELevelState.Stable => "Stable",
|
||||
ELevelState.Caution => "Caution",
|
||||
ELevelState.Critical => "Critical",
|
||||
ELevelState.Ready => "Ready",
|
||||
ELevelState.Lost => "Lost",
|
||||
ELevelState.Won => "Won",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
|
||||
private readonly StateBadge m_Badge = new();
|
||||
|
||||
@@ -21,4 +21,4 @@ public partial class PrimaryButton : Button
|
||||
TooltipText = tooltip;
|
||||
Disabled = disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,4 @@ public partial class StateBadge : PanelContainer
|
||||
}
|
||||
|
||||
private readonly Label m_Text = new();
|
||||
}
|
||||
}
|
||||
121
src/ReactorMaintenance.Godot/Data/GameSession.cs
Normal file
121
src/ReactorMaintenance.Godot/Data/GameSession.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Data;
|
||||
|
||||
public sealed class GameSession
|
||||
{
|
||||
public event StateChangedHandler? LevelStateChanged;
|
||||
public event StateChangedHandler? RobotMoved;
|
||||
public event StateChangedHandler? TurnAdvanced;
|
||||
public event StateChangedHandler? LevelWon;
|
||||
public event StateChangedHandler? LevelLost;
|
||||
|
||||
public void Initialize(LevelState levelState)
|
||||
{
|
||||
LevelState = levelState;
|
||||
m_StartSnapshot = LevelSerializer.Deserialize(LevelSerializer.Serialize(levelState));
|
||||
}
|
||||
|
||||
public bool MoveRobot(GridPosition destination)
|
||||
{
|
||||
if (LevelState.Terrain[(destination.Y * LevelState.Width) + destination.X] == ECellTerrain.Wall)
|
||||
return false;
|
||||
|
||||
if (destination.X < 0 || destination.X >= LevelState.Width ||
|
||||
destination.Y < 0 || destination.Y >= LevelState.Height)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.MoveRobot(LevelState, destination);
|
||||
RobotMoved?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
public LevelState EndTurn()
|
||||
{
|
||||
LevelState = m_Engine.EndTurn(LevelState);
|
||||
OnTurnAdvanced();
|
||||
return LevelState;
|
||||
}
|
||||
|
||||
public bool InteractProp()
|
||||
{
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.InteractProp(LevelState);
|
||||
OnTurnAdvanced();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool InteractLeak(ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.InteractLeak(LevelState, carrier, useRemedy);
|
||||
OnTurnAdvanced();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ApplyHeatShield()
|
||||
{
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.ApplyHeatShield(LevelState);
|
||||
OnTurnAdvanced();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ActivateReactor()
|
||||
{
|
||||
if (LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return false;
|
||||
|
||||
LevelState = m_Engine.ActivateReactor(LevelState);
|
||||
CheckOutcome();
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> GetForecasts()
|
||||
{
|
||||
return m_Engine.Forecast(LevelState);
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
LevelState = m_StartSnapshot;
|
||||
LevelStateChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
private void OnTurnAdvanced()
|
||||
{
|
||||
CheckOutcome();
|
||||
TurnAdvanced?.Invoke(this);
|
||||
}
|
||||
|
||||
private void CheckOutcome()
|
||||
{
|
||||
if (LevelState.Global.LevelState == ELevelState.Won)
|
||||
LevelWon?.Invoke(this);
|
||||
else if (LevelState.Global.LevelState == ELevelState.Lost)
|
||||
LevelLost?.Invoke(this);
|
||||
}
|
||||
|
||||
public LevelState LevelState { get; private set; } = null!;
|
||||
public GridPosition RobotPosition => LevelState.Robot.Position;
|
||||
public GlobalState GlobalState => LevelState.Global;
|
||||
public IReadOnlyList<Forecast> Forecasts => LevelState.Forecasts;
|
||||
public IReadOnlyList<LeakState> Leaks => LevelState.Leaks;
|
||||
public IReadOnlyList<ReactorState> Reactors => LevelState.Reactors;
|
||||
public IReadOnlyList<PropState> Props => LevelState.Props;
|
||||
public ECellTerrain[] Terrain => LevelState.Terrain;
|
||||
public SurfaceState[] Surface => LevelState.Surface;
|
||||
public UndergroundCell[] UndergroundFuel => LevelState.Fuel;
|
||||
public UndergroundCell[] UndergroundCoolant => LevelState.Coolant;
|
||||
public UndergroundCell[] UndergroundElectricity => LevelState.Electricity;
|
||||
public delegate void StateChangedHandler(GameSession sender);
|
||||
|
||||
private readonly SimulationEngine m_Engine = new();
|
||||
private LevelState m_StartSnapshot = null!;
|
||||
}
|
||||
34
src/ReactorMaintenance.Godot/Data/LevelStateLoader.cs
Normal file
34
src/ReactorMaintenance.Godot/Data/LevelStateLoader.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
using FileAccess = Godot.FileAccess;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Data;
|
||||
|
||||
public static class LevelStateLoader
|
||||
{
|
||||
public static LevelState Load(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Level path must not be null or empty.", nameof(path));
|
||||
|
||||
if (!FileAccess.FileExists(path))
|
||||
throw new FileNotFoundException($"Level file not found: {path}");
|
||||
|
||||
var json = FileAccess.GetFileAsString(path);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
throw new InvalidOperationException($"Level file is empty: {path}");
|
||||
|
||||
try
|
||||
{
|
||||
return LevelSerializer.Deserialize(json);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to deserialize level from {path}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
3974
src/ReactorMaintenance.Godot/Data/Levels/black_start.json
Normal file
3974
src/ReactorMaintenance.Godot/Data/Levels/black_start.json
Normal file
File diff suppressed because it is too large
Load Diff
2798
src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json
Normal file
2798
src/ReactorMaintenance.Godot/Data/Levels/coolant_restart.json
Normal file
File diff suppressed because it is too large
Load Diff
2811
src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json
Normal file
2811
src/ReactorMaintenance.Godot/Data/Levels/fuel_bleed.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,23 @@
|
||||
using Godot;
|
||||
using Godot;
|
||||
using ReactorMaintenance.Godot.Controls;
|
||||
using ReactorMaintenance.Godot.Data;
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Screens;
|
||||
|
||||
public partial class LevelScreen : ScreenBase
|
||||
{
|
||||
public void Configure(AppController app, CampaignLevel level, int levelNumber, int levelCount)
|
||||
public void Configure(AppController app, GameSession session, CampaignLevel level, int levelNumber, int levelCount)
|
||||
{
|
||||
m_App = app;
|
||||
m_Session = session;
|
||||
m_Level = level;
|
||||
m_OutcomeVisible = false;
|
||||
|
||||
var body = CreatePage(string.Empty);
|
||||
var header = new LevelHeader();
|
||||
body.AddChild(header);
|
||||
header.SetLevel(level, levelNumber, levelCount, "Stable");
|
||||
header.SetLevel(level.Name, levelNumber, levelCount, session.LevelState.Global.LevelState);
|
||||
|
||||
var flavor = CreateBodyText(level.FlavorText);
|
||||
flavor.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
@@ -23,109 +26,163 @@ public partial class LevelScreen : ScreenBase
|
||||
var split = new HSplitContainer { SizeFlagsVertical = SizeFlags.ExpandFill };
|
||||
body.AddChild(split);
|
||||
|
||||
split.AddChild(CreateGridPlaceholder());
|
||||
split.AddChild(CreateSidePanel(level));
|
||||
m_GridPanel = CreateGridContainer();
|
||||
split.AddChild(m_GridPanel);
|
||||
|
||||
body.AddChild(new InventoryStrip());
|
||||
m_Inspector = new();
|
||||
m_ForecastList = new();
|
||||
var sidePanel = new VBoxContainer { CustomMinimumSize = new(300, 0) };
|
||||
sidePanel.AddChild(m_Inspector);
|
||||
sidePanel.AddChild(m_ForecastList);
|
||||
split.AddChild(sidePanel);
|
||||
|
||||
m_InventoryStrip = new();
|
||||
body.AddChild(m_InventoryStrip);
|
||||
body.AddChild(CreateActionBar());
|
||||
|
||||
m_OverlayLayer = new();
|
||||
AddChild(m_OverlayLayer);
|
||||
|
||||
UpdateUI();
|
||||
m_Session.LevelStateChanged += OnLevelStateChanged;
|
||||
m_Session.TurnAdvanced += OnTurnAdvanced;
|
||||
}
|
||||
|
||||
private static PanelContainer CreateGridPlaceholder()
|
||||
private PanelContainer CreateGridContainer()
|
||||
{
|
||||
var panel = new PanelContainer { CustomMinimumSize = new(560, 360), SizeFlagsHorizontal = SizeFlags.ExpandFill, SizeFlagsVertical = SizeFlags.ExpandFill };
|
||||
var viewport = new Control {
|
||||
return new() {
|
||||
CustomMinimumSize = new(560, 360),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill
|
||||
};
|
||||
panel.AddChild(viewport);
|
||||
|
||||
var label = new Label {
|
||||
Text = "Level Grid Placeholder\nRobot marker, terrain, props, hazards, and underground overlays will render here.",
|
||||
AnchorRight = 1,
|
||||
AnchorBottom = 1,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart
|
||||
};
|
||||
viewport.AddChild(label);
|
||||
|
||||
var robot = new TextureRect {
|
||||
Texture = FrontendAssets.LoadTexture(FrontendAssets.MaintenanceRobot),
|
||||
AnchorLeft = 0.5f,
|
||||
AnchorTop = 0.5f,
|
||||
AnchorRight = 0.5f,
|
||||
AnchorBottom = 0.5f,
|
||||
OffsetLeft = -48,
|
||||
OffsetTop = -32,
|
||||
OffsetRight = 48,
|
||||
OffsetBottom = 64,
|
||||
ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional,
|
||||
StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered,
|
||||
MouseFilter = Control.MouseFilterEnum.Ignore
|
||||
};
|
||||
viewport.AddChild(robot);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static VBoxContainer CreateSidePanel(CampaignLevel level)
|
||||
{
|
||||
var sidePanel = new VBoxContainer { CustomMinimumSize = new(300, 0) };
|
||||
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("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("End Turn", OnEndTurnAction));
|
||||
actions.AddChild(CreateButton("Main Menu", () => m_App?.ShowMainMenu()));
|
||||
return actions;
|
||||
}
|
||||
|
||||
private void ShowLoseOverlay()
|
||||
private void OnMoveAction()
|
||||
{
|
||||
if (m_Session is null) 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInteractAction()
|
||||
{
|
||||
m_Session?.InteractProp();
|
||||
}
|
||||
|
||||
private void OnRepairAction()
|
||||
{
|
||||
if (m_Session is null) return;
|
||||
|
||||
foreach (var leak in m_Session.Leaks)
|
||||
{
|
||||
if (leak.AccessPosition == m_Session.RobotPosition && !leak.Repaired)
|
||||
{
|
||||
m_Session.InteractLeak(leak.Carrier, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEndTurnAction()
|
||||
{
|
||||
m_Session?.EndTurn();
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (m_Session is null || m_Inspector is null || m_ForecastList is null || m_InventoryStrip is null)
|
||||
return;
|
||||
|
||||
var ls = m_Session.LevelState;
|
||||
var robotPos = m_Session.RobotPosition;
|
||||
var index = (robotPos.Y * ls.Width) + robotPos.X;
|
||||
var surface = ls.Surface[index];
|
||||
var prop = index < ls.Props.Length ? ls.Props[index] : new();
|
||||
|
||||
m_Inspector.SetCellInfo(
|
||||
robotPos,
|
||||
ls.Terrain[index],
|
||||
prop.Type,
|
||||
prop.ServiceState,
|
||||
surface.Fuel,
|
||||
surface.Coolant,
|
||||
surface.Electricity,
|
||||
surface.Heat);
|
||||
|
||||
m_ForecastList.SetForecasts(ls.Forecasts);
|
||||
|
||||
m_InventoryStrip.SetInventory(
|
||||
m_Session.LevelState.Robot.FuelNeutralizers,
|
||||
m_Session.LevelState.Robot.CoolantNeutralizers,
|
||||
m_Session.LevelState.Robot.ElectricityNeutralizers,
|
||||
m_Session.LevelState.Robot.HeatShields);
|
||||
}
|
||||
|
||||
private void OnLevelStateChanged(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnTurnAdvanced(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
if (sender.LevelState.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
{
|
||||
ShowOutcomeOverlay(sender.LevelState.Global.LevelState);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowOutcomeOverlay(ELevelState state)
|
||||
{
|
||||
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);
|
||||
}
|
||||
Control overlay;
|
||||
|
||||
private void ShowWinOverlay()
|
||||
{
|
||||
if (m_OutcomeVisible || m_App is null || m_Level is null || m_OverlayLayer is null)
|
||||
return;
|
||||
if (state == ELevelState.Won)
|
||||
{
|
||||
overlay = new WinOverlay();
|
||||
((WinOverlay)overlay).Configure(m_Level, m_App.CompleteCurrentLevel, m_App.ShowMainMenu);
|
||||
}
|
||||
else
|
||||
{
|
||||
overlay = new LoseOverlay();
|
||||
((LoseOverlay)overlay).Configure(m_Level, m_App.RetryCurrentLevel, m_App.ShowGameOver, m_App.ShowMainMenu);
|
||||
}
|
||||
|
||||
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);
|
||||
m_OverlayLayer.AddChild(center);
|
||||
}
|
||||
|
||||
private AppController? m_App;
|
||||
private ForecastList? m_ForecastList;
|
||||
private PanelContainer? m_GridPanel;
|
||||
private CellInspector? m_Inspector;
|
||||
private InventoryStrip? m_InventoryStrip;
|
||||
private CampaignLevel? m_Level;
|
||||
private bool m_OutcomeVisible;
|
||||
private CanvasLayer? m_OverlayLayer;
|
||||
}
|
||||
private GameSession? m_Session;
|
||||
}
|
||||
@@ -47,7 +47,11 @@ public partial class AppController : Control
|
||||
public void LoadCurrentLevel()
|
||||
{
|
||||
m_Session.MarkCurrentLevelLoaded();
|
||||
ShowScreen<LevelScreen>("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, m_Session.CurrentLevel, m_Session.LevelNumber, m_Session.LevelCount));
|
||||
var level = m_Session.CurrentLevel;
|
||||
var gameSession = new GameSession();
|
||||
var levelState = LevelStateLoader.Load(level.LevelPath);
|
||||
gameSession.Initialize(levelState);
|
||||
ShowScreen<LevelScreen>("res://Scenes/LevelScreen.tscn", screen => screen.Configure(this, gameSession, level, m_Session.LevelNumber, m_Session.LevelCount));
|
||||
}
|
||||
|
||||
public void RetryCurrentLevel()
|
||||
|
||||
Reference in New Issue
Block a user