diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml
index 8868156..aa76577 100644
--- a/src/ReactorMaintenance.Win2D/MainWindow.xaml
+++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml
@@ -93,22 +93,17 @@
-
+
-
-
-
-
-
+ CornerRadius="3">
+ TextWrapping="Wrap" />
@@ -117,17 +112,36 @@
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CornerRadius="3">
@@ -141,6 +155,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
index a1cd7e1..9194d57 100644
--- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
+++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs
@@ -45,6 +45,7 @@ public sealed partial class MainWindow
private sealed record ForecastViewModel(string Message);
private sealed record InspectorItemViewModel(string Label, string Value, string Description);
+ private sealed record NetworkInspectionViewModel(string Carrier, string State, string Amount, string Intensity, string Integrity);
private sealed class EditorToolViewModel(EditorToolCommand command, string label) : INotifyPropertyChanged
{
@@ -260,12 +261,13 @@ public sealed partial class MainWindow
m_DragExceededClickThreshold = false;
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
m_CursorDragStartCell = null;
+ m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_InvalidDragCell = null;
m_EditorFeedback = string.Empty;
m_LastPaintedCell = null;
if (!m_IsPanning && m_SelectedTool.Tool == EEditorTool.Cursor && TryGetGridPosition(point.Position, out var position))
- m_CursorDragStartCell = position;
+ StartCursorDrag(position);
else if (!m_IsPanning && IsDragPaintTool(m_SelectedTool.Tool) && TryGetGridPosition(point.Position, out position))
{
m_LastPaintedCell = position;
@@ -337,7 +339,7 @@ public sealed partial class MainWindow
RefreshInspector();
LevelCanvas.Invalidate();
}
- else if (!m_DragExceededClickThreshold && !IsDragPaintTool(m_SelectedTool.Tool))
+ else if (!m_DragExceededClickThreshold && !m_CursorDragStartRejected && !IsDragPaintTool(m_SelectedTool.Tool))
{
SelectOrPaintAt(point.Position);
}
@@ -346,6 +348,7 @@ public sealed partial class MainWindow
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
+ m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_LastPaintedCell = null;
m_DragExceededClickThreshold = false;
@@ -358,6 +361,7 @@ public sealed partial class MainWindow
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
+ m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_LastPaintedCell = null;
m_DragExceededClickThreshold = false;
@@ -413,7 +417,25 @@ public sealed partial class MainWindow
private static bool IsDragPaintTool(EEditorTool tool)
{
- return tool is EEditorTool.Floor or EEditorTool.Wall;
+ return tool is EEditorTool.Floor or EEditorTool.Wall or EEditorTool.Underground;
+ }
+
+ private void StartCursorDrag(GridPosition position)
+ {
+ m_SelectedCell = position;
+ if (HasMovableAt(position))
+ {
+ m_CursorDragStartCell = position;
+ RefreshInspector();
+ LevelCanvas.Invalidate();
+ return;
+ }
+
+ m_CursorDragStartRejected = true;
+ m_InvalidDragCell = position;
+ m_EditorFeedback = "No movable robot, prop, source, or leak starts here.";
+ RefreshInspector();
+ LevelCanvas.Invalidate();
}
private void ClearAt(Point point)
@@ -788,13 +810,29 @@ public sealed partial class MainWindow
GlobalGrid.ItemsSource = new[] {
new InspectorItemViewModel("Inventory", $"F {m_Level.Robot.FuelNeutralizers} / C {m_Level.Robot.CoolantNeutralizers} / E {m_Level.Robot.ElectricityNeutralizers} / H {m_Level.Robot.HeatShields}", "Remedies carried by the robot."),
new InspectorItemViewModel("Heat Shield", m_Level.Robot.HeatImmunitySteps.ToString(CultureInfo.InvariantCulture), "Remaining protected movement steps."),
- new InspectorItemViewModel("Required F/C/E", $"{m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}", "Producing consumers needed for readiness."),
new InspectorItemViewModel("Objects", $"R {m_Level.Reactors.Count} / L {m_Level.Leaks.Count} / D {doorCount}", "Reactors, active leaks, and doors.")
};
+ RequiredGrid.ItemsSource = new[] {
+ new InspectorItemViewModel("Fuel", m_Level.RequiredFuelConsumers.ToString(CultureInfo.InvariantCulture), "Producing consumers needed for readiness."),
+ new InspectorItemViewModel("Coolant", m_Level.RequiredCoolantConsumers.ToString(CultureInfo.InvariantCulture), "Producing consumers needed for readiness."),
+ new InspectorItemViewModel("Electric", m_Level.RequiredElectricityConsumers.ToString(CultureInfo.InvariantCulture), "Producing consumers needed for readiness.")
+ };
+
+ if (m_SelectedCell is { } position && m_Level.InBounds(position))
+ {
+ SelectedCellTitleText.Text = SelectedCellTitle(position);
+ CellGrid.ItemsSource = CellInspectionItems(position);
+ SurfaceGrid.ItemsSource = SurfaceInspectionItems(position);
+ NetworkGrid.ItemsSource = NetworkInspectionItems(position);
+ }
+ else
+ {
+ SelectedCellTitleText.Text = "None";
+ CellGrid.ItemsSource = new[] { new InspectorItemViewModel("Selection", "None", "Select a grid cell to inspect it.") };
+ SurfaceGrid.ItemsSource = Array.Empty();
+ NetworkGrid.ItemsSource = Array.Empty();
+ }
- CellGrid.ItemsSource = m_SelectedCell is { } position && m_Level.InBounds(position)
- ? CellInspectionItems(position)
- : new[] { new InspectorItemViewModel("Selection", "None", "Select a grid cell to inspect it.") };
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
}
@@ -827,25 +865,61 @@ public sealed partial class MainWindow
{
var prop = m_Level.GetProp(position);
var surface = m_Level.GetSurface(position);
- var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
- var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
- var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
return [
new("Position", $"{position.X},{position.Y}", "Grid coordinate."),
new("Terrain", m_Level.GetTerrain(position).ToString(), "Static surface cell type."),
new("Prop", $"{prop.Type} {prop.SwitchState}", "Placed surface object and switch state."),
new("Services F/C/E", $"{prop.FuelServiceState}/{prop.CoolantServiceState}/{prop.ElectricityServiceState}", "Consumer service status."),
- new("Fuel Net", UndergroundText(fuel), "Amount, intensity, and integrity."),
- new("Coolant Net", UndergroundText(coolant), "Amount, intensity, and integrity."),
- new("Electric Net", UndergroundText(electricity), "Amount, intensity, and integrity."),
- new("Surface F/C/E/H", $"{Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}", "Visible hazards and heat."),
new("Blocks F/C/E", $"{surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}", "Temporary remedy entry blocks.")
];
}
- private static string UndergroundText(UndergroundCell cell)
+ private InspectorItemViewModel[] SurfaceInspectionItems(GridPosition position)
{
- return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)} integrity {cell.StructuralIntegrity}";
+ var surface = m_Level.GetSurface(position);
+ return [
+ new("Fuel", Format(surface.Fuel), "Visible fuel hazard."),
+ new("Coolant", Format(surface.Coolant), "Visible coolant hazard."),
+ new("Electric", Format(surface.Electricity), "Visible electricity hazard."),
+ new("Heat", Format(surface.Heat), "Visible heat.")
+ ];
+ }
+
+ private NetworkInspectionViewModel[] NetworkInspectionItems(GridPosition position)
+ {
+ return [
+ NetworkInspectionItem("Fuel", m_Level.GetUnderground(position, ECarrierType.Fuel)),
+ NetworkInspectionItem("Coolant", m_Level.GetUnderground(position, ECarrierType.Coolant)),
+ NetworkInspectionItem("Electric", m_Level.GetUnderground(position, ECarrierType.Electricity))
+ ];
+ }
+
+ private static NetworkInspectionViewModel NetworkInspectionItem(string carrier, UndergroundCell cell)
+ {
+ return new(carrier, cell.State.ToString(), Format(cell.Amount), Format(cell.Intensity), cell.StructuralIntegrity.ToString(CultureInfo.InvariantCulture));
+ }
+
+ private string SelectedCellTitle(GridPosition position)
+ {
+ var prop = m_Level.GetProp(position);
+ if (prop.Type != EPropType.None)
+ return $"{prop.Type} {prop.SwitchState}";
+
+ if (m_Level.Robot.Position == position)
+ return "Robot";
+
+ var leak = m_Level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == position || leak.UndergroundPosition == position));
+ if (leak is not null)
+ return $"{leak.Carrier} Leak";
+
+ var surface = m_Level.GetSurface(position);
+ if (surface.Fuel > 0 || surface.Coolant > 0 || surface.Electricity > 0)
+ return "Surface Hazard";
+
+ if (surface.Heat > 0)
+ return "Heat";
+
+ return m_Level.GetTerrain(position).ToString();
}
private static string Format(float value)
@@ -957,6 +1031,11 @@ public sealed partial class MainWindow
return m_Level.Robot.Position == source ? "robot" : null;
}
+ private bool HasMovableAt(GridPosition source)
+ {
+ return MovableImageKey(source) is not null;
+ }
+
private float SurfaceOpacity()
{
return m_ActiveLayer == EEditorLayer.Surface ? 1.0f : 0.5f;
@@ -1217,6 +1296,7 @@ public sealed partial class MainWindow
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
private StorageFile? m_CurrentFile;
private GridPosition? m_CursorDragStartCell;
+ private bool m_CursorDragStartRejected;
private bool m_DragExceededClickThreshold;
private GridPosition? m_DragPreviewDestination;
private string m_EditorFeedback = string.Empty;
diff --git a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs
index b41bae2..313d394 100644
--- a/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs
+++ b/tests/ReactorMaintenance.Simulation.Tests/LevelEditorTests.cs
@@ -42,6 +42,21 @@ public sealed class LevelEditorTests
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Electricity).State);
}
+ [Fact]
+ public void UndergroundToolCanPaintAdjacentCellsRepeatedly()
+ {
+ var level = LevelState.Create("Network editor", 6, 6);
+ var command = new EditorToolCommand { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel };
+
+ level = LevelEditor.Apply(level, new(1, 1), command);
+ level = LevelEditor.Apply(level, new(2, 1), command);
+ level = LevelEditor.Apply(level, new(3, 1), command);
+
+ Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
+ Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(2, 1), ECarrierType.Fuel).State);
+ Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(3, 1), ECarrierType.Fuel).State);
+ }
+
[Fact]
public void ConsumerToolPlacesCarrierAgnosticConsumer()
{