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() {