Rework Win2D editor layers

This commit is contained in:
2026-05-11 22:34:19 +02:00
parent 69ed79ce86
commit 0651603fd2
5 changed files with 345 additions and 217 deletions

View File

@@ -44,9 +44,20 @@
- Verified after the simulation rework:
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 23 tests,
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
- Reworked the Win2D editor workflow:
- added the Surface/Electricity/Fuel/Coolant layer combobox,
- filtered tools by active layer and fixed exclusive tool selection,
- rendered underground networks as carrier-colored centerline networks with source dots and layer opacity rules,
- removed Rule Events, Reactor Binding, and pending workflow panels from the editor UI,
- replaced two-click electricity leak authoring with electric-layer leak access cycling,
- made Shift+left drag pan in all tools and Cursor drag move the robot or props.
- Added editor-helper tests for electricity leak access cycling and cursor drag movement behavior.
- Verified after the editor overhaul:
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 26 tests,
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
## Current Work
- Rework Win2D editor layer selection, rendering, tool filtering, drag behavior, and removed panels.
- Editor overhaul implementation is complete; commit is pending.
## Editor Overhaul Requirements
- Add a layer combobox with Surface, Electricity, Fuel, and Coolant.

View File

@@ -2,6 +2,52 @@
public static class LevelEditor
{
public static LevelState MoveOccupant(LevelState level, GridPosition source, GridPosition destination)
{
if (!level.InBounds(source) || !level.IsFloor(destination) || source == destination)
return level;
var prop = level.GetProp(source);
if (prop.Type == EPropType.None)
return level.Robot.Position == source ? level with { Robot = level.Robot with { Position = destination } } : level;
if (level.GetProp(destination).Type != EPropType.None)
return level;
var next = level.SetProp(source, new()).SetProp(destination, prop);
if (prop.Type != EPropType.ReactorControl)
return next;
return next with {
Reactors = next.Reactors
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
.ToArray()
};
}
public static LevelState CycleElectricityLeakAccess(LevelState level, GridPosition undergroundPosition)
{
if (!level.InBounds(undergroundPosition))
return level;
if (!level.GetUnderground(undergroundPosition, ECarrierType.Electricity).IsPresent)
return level;
var accessPositions = undergroundPosition.Neighbors().Where(level.IsFloor).ToArray();
if (accessPositions.Length == 0)
return level;
var existingLeak = level.Leaks.FirstOrDefault(leak => leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition == undergroundPosition);
var nextAccessPosition = accessPositions[0];
if (existingLeak is not null)
{
var index = Array.IndexOf(accessPositions, existingLeak.AccessPosition);
nextAccessPosition = accessPositions[(index + 1) % accessPositions.Length];
}
return SetLeak(level, undergroundPosition, nextAccessPosition, ECarrierType.Electricity);
}
public static LevelState Apply(LevelState level, GridPosition position, EditorToolCommand command)
{
if (!level.InBounds(position))

View File

@@ -30,6 +30,10 @@
<ScrollViewer Grid.Column="0" Background="#1C2126">
<StackPanel Padding="12" Spacing="10">
<TextBlock Text="Layer" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ComboBox x:Name="LayerPicker"
SelectionChanged="LayerPicker_SelectionChanged"
HorizontalAlignment="Stretch" />
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ItemsControl x:Name="ToolPicker">
<ItemsControl.ItemsPanel>
@@ -51,8 +55,7 @@
</ItemsControl>
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
Foreground="#9EA7AE" TextWrapping="Wrap" />
<TextBlock
Text="Door and wall electricity leaks use two clicks: choose the source cell, then the adjacent floor face."
<TextBlock Text="Shift+left drag pans. Cursor drag moves the robot or a prop."
Foreground="#9EA7AE"
TextWrapping="Wrap" />
</StackPanel>
@@ -96,37 +99,6 @@
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Editor Workflow" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="WorkflowText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<TextBlock Text="Reactor Binding" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="ReactorBindingText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<Grid ColumnSpacing="8" RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="Fuel" Click="BindFuel_Click" HorizontalAlignment="Stretch" />
<Button Grid.Column="1" Content="Coolant" Click="BindCoolant_Click"
HorizontalAlignment="Stretch" />
<Button Grid.Column="2" Content="Electric" Click="BindElectricity_Click"
HorizontalAlignment="Stretch" />
</Grid>
<TextBlock Text="Rule Events" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<TextBlock x:Name="RuleEventText" Foreground="#CCD4DA" TextWrapping="Wrap" />
<Grid ColumnSpacing="8" RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="Warn Next Turn" Click="AddWarningRule_Click" HorizontalAlignment="Stretch" />
<Button Grid.Column="1" Content="Leak Next Turn" Click="AddLeakRule_Click"
HorizontalAlignment="Stretch" />
</Grid>
<Button Content="Remove Last Rule" Click="RemoveLastRule_Click" HorizontalAlignment="Stretch" />
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
<ItemsControl x:Name="ForecastList">
<ItemsControl.ItemTemplate>

View File

@@ -4,10 +4,14 @@ using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using ReactorMaintenance.Simulation;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Windows.System;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI;
@@ -18,6 +22,14 @@ namespace ReactorMaintenance.Win2D;
public sealed partial class MainWindow
{
private enum EEditorLayer
{
Surface,
Electricity,
Fuel,
Coolant
}
private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
{
public Rect CellRect(GridPosition position)
@@ -33,11 +45,32 @@ public sealed partial class MainWindow
private sealed record ForecastViewModel(string Message);
private sealed class EditorToolViewModel(EditorToolCommand command, string label)
private sealed class EditorToolViewModel(EditorToolCommand command, string label) : INotifyPropertyChanged
{
private bool m_IsSelected;
public event PropertyChangedEventHandler? PropertyChanged;
public EditorToolCommand Command { get; } = command;
public string Label { get; } = label;
public bool IsSelected { get; set; }
public bool IsSelected
{
get => m_IsSelected;
set
{
if (m_IsSelected == value)
return;
m_IsSelected = value;
OnPropertyChanged();
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
public MainWindow()
@@ -46,7 +79,9 @@ public sealed partial class MainWindow
m_Level = BuildStarterLevel();
m_EditorTools = BuildEditorTools();
ToolPicker.ItemsSource = m_EditorTools;
LayerPicker.ItemsSource = Enum.GetValues<EEditorLayer>();
LayerPicker.SelectedItem = EEditorLayer.Surface;
RefreshToolPicker();
RefreshInspector();
}
@@ -74,20 +109,45 @@ public sealed partial class MainWindow
if ((sender as FrameworkElement)?.DataContext is not EditorToolViewModel tool)
return;
m_SelectedTool = tool.Command;
ClearPendingEditorOperation();
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
SelectTool(tool);
RefreshInspector();
}
private void LayerPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LayerPicker.SelectedItem is EEditorLayer layer)
m_ActiveLayer = layer;
if (!IsToolAvailableOnActiveLayer(m_SelectedTool))
m_SelectedTool = new() { Tool = EEditorTool.Cursor };
RefreshToolPicker();
RefreshInspector();
LevelCanvas.Invalidate();
}
private void SelectTool(EditorToolViewModel tool)
{
m_SelectedTool = tool.Command;
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
}
private void RefreshToolPicker()
{
var visibleTools = m_EditorTools.Where(tool => IsToolAvailableOnActiveLayer(tool.Command)).ToArray();
foreach (var tool in m_EditorTools)
tool.IsSelected = tool.Command == m_SelectedTool;
ToolPicker.ItemsSource = visibleTools;
}
private void New_Click(object sender, RoutedEventArgs e)
{
m_Level = BuildStarterLevel();
m_CurrentFile = null;
m_SelectedCell = null;
ClearPendingEditorOperation();
RefreshInspector();
LevelCanvas.Invalidate();
}
@@ -110,7 +170,6 @@ public sealed partial class MainWindow
m_CurrentFile = file;
m_SelectedCell = null;
m_SelectedReactorId = m_Level.Reactors.FirstOrDefault()?.ReactorId;
ClearPendingEditorOperation();
RefreshInspector();
LevelCanvas.Invalidate();
}
@@ -198,6 +257,11 @@ public sealed partial class MainWindow
m_LeftPointerDownPoint = point.Position;
m_LastPanPoint = point.Position;
m_DragExceededClickThreshold = false;
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
m_CursorDragStartCell = null;
if (!m_IsPanning && m_SelectedTool.Tool == EEditorTool.Cursor && TryGetGridPosition(point.Position, out var position))
m_CursorDragStartCell = position;
e.Handled = true;
}
@@ -216,20 +280,44 @@ public sealed partial class MainWindow
if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
m_DragExceededClickThreshold = true;
if (m_IsPanning)
{
m_PanX += deltaX;
m_PanY += deltaY;
ClampPan();
LevelCanvas.Invalidate();
}
e.Handled = true;
}
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
if (m_LeftPointerDown && !m_DragExceededClickThreshold)
if (m_LeftPointerDown)
{
if (m_IsPanning)
{
LevelCanvas.Invalidate();
}
else if (m_CursorDragStartCell is { } source && m_DragExceededClickThreshold && TryGetGridPosition(point.Position, out var destination))
{
m_Level = LevelEditor.MoveOccupant(m_Level, source, destination);
m_SelectedCell = destination;
SelectReactorFromCell(destination);
RefreshForecasts();
RefreshInspector();
LevelCanvas.Invalidate();
}
else if (!m_DragExceededClickThreshold)
{
SelectOrPaintAt(point.Position);
}
}
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
m_DragExceededClickThreshold = false;
LevelCanvas.ReleasePointerCapture(e.Pointer);
e.Handled = true;
@@ -238,6 +326,8 @@ public sealed partial class MainWindow
private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
{
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
m_DragExceededClickThreshold = false;
}
@@ -277,7 +367,6 @@ public sealed partial class MainWindow
return;
m_SelectedCell = position;
ClearPendingEditorOperation();
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
@@ -293,40 +382,41 @@ public sealed partial class MainWindow
var layout = GetLayout();
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
DrawTerrain(drawing, layout);
DrawTerrain(drawing, layout, SurfaceOpacity());
DrawUnderground(drawing, layout);
DrawSurface(drawing, layout);
DrawDoors(drawing, layout);
DrawProps(drawing, layout);
DrawLeaks(drawing, layout);
DrawRobot(drawing, layout);
DrawSurface(drawing, layout, SurfaceOpacity());
DrawDoors(drawing, layout, SurfaceOpacity());
DrawProps(drawing, layout, SurfaceOpacity());
DrawLeaks(drawing, layout, SurfaceOpacity());
DrawRobot(drawing, layout, SurfaceOpacity());
DrawGrid(drawing, layout);
}
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
for (var y = 0; y <= m_Level.Height; y++)
{
for (var x = 0; x <= m_Level.Width; x++)
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y), opacity);
}
}
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask, float opacity)
{
if (m_TerrainTilemap is null)
{
DrawFallbackTerrainTile(drawing, rect, floorMask);
DrawFallbackTerrainTile(drawing, rect, floorMask, opacity);
return;
}
var wallMask = c_AllCorners ^ floorMask;
drawing.DrawImage(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), 1.0f, CanvasImageInterpolation.HighQualityCubic);
drawing.DrawImage(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), opacity, CanvasImageInterpolation.HighQualityCubic);
}
private static void DrawFallbackTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
private static void DrawFallbackTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask, float opacity)
{
var color = floorMask == c_AllCorners ? ColorHelper.FromArgb(255, 32, 38, 42) : ColorHelper.FromArgb(255, 41, 47, 52);
var alpha = (byte)Math.Clamp(opacity * 255, 0, 255);
var color = floorMask == c_AllCorners ? ColorHelper.FromArgb(alpha, 32, 38, 42) : ColorHelper.FromArgb(alpha, 41, 47, 52);
drawing.FillRectangle(rect, color);
}
@@ -390,50 +480,84 @@ public sealed partial class MainWindow
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
{
foreach (var position in AllPositions())
{
var rect = Inset(layout.CellRect(position), 0.18);
DrawUndergroundCell(drawing, rect, position, ECarrierType.Fuel, c_FuelColor);
DrawUndergroundCell(drawing, Inset(rect, -0.08), position, ECarrierType.Coolant, c_CoolantColor);
DrawUndergroundCell(drawing, Inset(rect, -0.16), position, ECarrierType.Electricity, c_ElectricityColor);
}
foreach (var carrier in OrderedUndergroundLayers())
DrawUndergroundLayer(drawing, layout, carrier, CarrierColor(carrier), UndergroundOpacity(carrier));
}
private void DrawUndergroundCell(CanvasDrawingSession drawing, Rect rect, GridPosition position, ECarrierType carrier, Color color)
private IEnumerable<ECarrierType> OrderedUndergroundLayers()
{
var carriers = new[] { ECarrierType.Fuel, ECarrierType.Coolant, ECarrierType.Electricity };
var activeCarrier = LayerCarrier(m_ActiveLayer);
return activeCarrier is null ? carriers : carriers.Where(carrier => carrier != activeCarrier).Append(activeCarrier.Value);
}
private void DrawUndergroundLayer(CanvasDrawingSession drawing, CanvasLayout layout, ECarrierType carrier, Color color, float opacity)
{
var layerColor = WithOpacity(color, opacity);
var lineWidth = (float)Math.Max(4, layout.CellSize * 0.16);
var cellDotRadius = (float)Math.Max(2, layout.CellSize * 0.08);
var sourceDotRadius = (float)Math.Max(5, layout.CellSize * 0.22);
foreach (var position in AllPositions())
{
var cell = m_Level.GetUnderground(position, carrier);
if (!cell.IsPresent)
return;
continue;
drawing.DrawRectangle(rect, color, cell.State == EUndergroundState.Leaking ? 4 : 2);
if (cell.Amount > 0 || cell.Intensity > 0)
drawing.FillCircle((float)(rect.X + (rect.Width / 2)), (float)(rect.Y + (rect.Height / 2)), (float)Math.Max(2, rect.Width * 0.08), color);
var center = Center(layout.CellRect(position));
DrawNetworkConnection(drawing, layout, carrier, position, new(position.X + 1, position.Y), layerColor, lineWidth);
DrawNetworkConnection(drawing, layout, carrier, position, new(position.X, position.Y + 1), layerColor, lineWidth);
drawing.FillCircle((float)center.X, (float)center.Y, cellDotRadius, layerColor);
if (cell.State == EUndergroundState.Leaking)
drawing.DrawCircle((float)center.X, (float)center.Y, sourceDotRadius * 0.7f, Colors.OrangeRed, Math.Max(2, lineWidth * 0.25f));
var prop = m_Level.GetProp(position);
if (prop is { Type: EPropType.Flow } && prop.Carrier == carrier)
drawing.FillCircle((float)center.X, (float)center.Y, sourceDotRadius, layerColor);
}
}
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawNetworkConnection(
CanvasDrawingSession drawing,
CanvasLayout layout,
ECarrierType carrier,
GridPosition position,
GridPosition neighbor,
Color color,
float lineWidth)
{
if (!m_Level.InBounds(neighbor) || !m_Level.GetUnderground(neighbor, carrier).IsPresent)
return;
var from = Center(layout.CellRect(position));
var to = Center(layout.CellRect(neighbor));
drawing.DrawLine((float)from.X, (float)from.Y, (float)to.X, (float)to.Y, color, lineWidth);
}
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var position in AllPositions().Where(m_Level.IsFloor))
{
var surface = m_Level.GetSurface(position);
var rect = layout.CellRect(position);
FillHazard(drawing, rect, surface.Fuel, c_FuelColor, 0.08);
FillHazard(drawing, rect, surface.Coolant, c_CoolantColor, 0.18);
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28);
FillHazard(drawing, rect, surface.Fuel, c_FuelColor, 0.08, opacity);
FillHazard(drawing, rect, surface.Coolant, c_CoolantColor, 0.18, opacity);
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28, opacity);
if (surface.Heat > 0)
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f));
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f) * opacity);
}
}
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset)
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset, float opacity)
{
if (amount <= 0)
return;
var alpha = (byte)Math.Clamp(40 + (amount / Balancing.Current.MaxValue * 130), 40, 170);
var alpha = (byte)Math.Clamp((40 + (amount / Balancing.Current.MaxValue * 130)) * opacity, 0, 170);
drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
}
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var position in AllPositions())
{
@@ -443,7 +567,7 @@ public sealed partial class MainWindow
var rect = layout.CellRect(position);
var center = Center(rect);
var color = prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed;
var color = WithOpacity(prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, opacity);
if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1)))
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
else
@@ -451,7 +575,7 @@ public sealed partial class MainWindow
}
}
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var position in AllPositions())
{
@@ -460,20 +584,20 @@ public sealed partial class MainWindow
continue;
var rect = Inset(layout.CellRect(position), 0.18);
drawing.FillRoundedRectangle(rect, 4, 4, PropColor(prop));
DrawCenteredText(drawing, PropLabel(prop), rect, Colors.White, Math.Max(10, (float)(layout.CellSize * 0.22)));
drawing.FillRoundedRectangle(rect, 4, 4, WithOpacity(PropColor(prop), opacity));
DrawCenteredText(drawing, PropLabel(prop), rect, WithOpacity(Colors.White, opacity), Math.Max(10, (float)(layout.CellSize * 0.22)));
}
}
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var leak in m_Level.Leaks.Where(leak => !leak.Repaired))
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1));
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1), opacity);
}
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04));
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04), opacity);
}
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
@@ -574,9 +698,6 @@ public sealed partial class MainWindow
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
WorkflowText.Text = WorkflowInspectionText();
ReactorBindingText.Text = ReactorBindingInspectionText();
RuleEventText.Text = RuleEventInspectionText();
}
private void ApplySelectedTool(GridPosition position)
@@ -587,10 +708,10 @@ public sealed partial class MainWindow
ApplyDoorTool(position);
break;
case EEditorTool.Leak when m_SelectedTool.Carrier == ECarrierType.Electricity:
ApplyElectricityLeakTool(position);
m_Level = LevelEditor.CycleElectricityLeakAccess(m_Level, position);
RefreshForecasts();
break;
default:
ClearPendingEditorOperation();
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
SelectReactorFromCell(position);
RefreshForecasts();
@@ -600,69 +721,10 @@ public sealed partial class MainWindow
private void ApplyDoorTool(GridPosition position)
{
ClearPendingEditorOperation();
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
RefreshForecasts();
}
private void ApplyElectricityLeakTool(GridPosition position)
{
if (m_Level.GetTerrain(position) == ECellTerrain.Wall)
{
m_PendingElectricityLeakCell = position;
return;
}
if (m_PendingElectricityLeakCell is { } undergroundPosition)
{
m_Level = LevelEditor.SetLeak(m_Level, undergroundPosition, position, ECarrierType.Electricity);
m_PendingElectricityLeakCell = null;
RefreshForecasts();
return;
}
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
RefreshForecasts();
}
private void BindFuel_Click(object sender, RoutedEventArgs e)
{
BindSelectedConsumer(ECarrierType.Fuel);
}
private void BindCoolant_Click(object sender, RoutedEventArgs e)
{
BindSelectedConsumer(ECarrierType.Coolant);
}
private void BindElectricity_Click(object sender, RoutedEventArgs e)
{
BindSelectedConsumer(ECarrierType.Electricity);
}
private void AddWarningRule_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "Rule events were removed from level authoring.";
}
private void AddLeakRule_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "Rule events were removed from level authoring.";
}
private void RemoveLastRule_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "Rule events were removed from level authoring.";
}
private void BindSelectedConsumer(ECarrierType carrier)
{
if (m_SelectedCell is not { } position || m_SelectedReactorId is not { } reactorId)
return;
StatusText.Text = "Reactors now use required consumer counts instead of bindings.";
}
private string CellInspectionText(GridPosition position)
{
var prop = m_Level.GetProp(position);
@@ -673,6 +735,7 @@ public sealed partial class MainWindow
return $"Position: {position.X},{position.Y}\n"
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
+ $"Prop: {prop.Type} {prop.SwitchState} {prop.ServiceState}\n"
+ $"Consumer F/C/E: {prop.FuelServiceState} / {prop.CoolantServiceState} / {prop.ElectricityServiceState}\n"
+ $"Fuel: {UndergroundText(fuel)}\n"
+ $"Coolant: {UndergroundText(coolant)}\n"
+ $"Electricity: {UndergroundText(electricity)}\n"
@@ -680,36 +743,6 @@ public sealed partial class MainWindow
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
}
private string WorkflowInspectionText()
{
if (m_PendingElectricityLeakCell is { } leak)
return $"Electricity leak face: select adjacent floor access for {leak.X},{leak.Y}.";
return "No pending editor operation.";
}
private string ReactorBindingInspectionText()
{
var reactor = m_SelectedReactorId is { } reactorId ? m_Level.Reactors.FirstOrDefault(candidate => candidate.ReactorId == reactorId) : null;
if (reactor is null)
return "Select or place a reactor control.";
return $"Reactor {reactor.ReactorId}\n"
+ $"Control: {PositionText(reactor.ControlPosition)}\n"
+ $"Ready: {reactor.Ready}\n"
+ $"Required F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}";
}
private string RuleEventInspectionText()
{
return "Rule events were removed from level authoring.";
}
private static string PositionText(GridPosition position)
{
return $"{position.X},{position.Y}";
}
private static string UndergroundText(UndergroundCell cell)
{
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)} integrity {cell.StructuralIntegrity}";
@@ -798,6 +831,34 @@ public sealed partial class MainWindow
drawing.DrawImage(image, rect, image.Bounds, opacity);
}
private float SurfaceOpacity()
{
return m_ActiveLayer == EEditorLayer.Surface ? 1.0f : 0.5f;
}
private float UndergroundOpacity(ECarrierType carrier)
{
if (m_ActiveLayer == EEditorLayer.Surface)
return 0.25f;
return LayerCarrier(m_ActiveLayer) == carrier ? 1.0f : 0.25f;
}
private static ECarrierType? LayerCarrier(EEditorLayer layer)
{
return layer switch {
EEditorLayer.Electricity => ECarrierType.Electricity,
EEditorLayer.Fuel => ECarrierType.Fuel,
EEditorLayer.Coolant => ECarrierType.Coolant,
_ => null
};
}
private static Color WithOpacity(Color color, float opacity)
{
return ColorHelper.FromArgb((byte)Math.Clamp(color.A * opacity, 0, 255), color.R, color.G, color.B);
}
private static Color PropColor(PropState prop)
{
return prop.Type switch {
@@ -884,9 +945,7 @@ public sealed partial class MainWindow
CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Coolant, "Coolant Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Electricity, "Electric Source"),
CarrierTool(EEditorTool.Consumer, ECarrierType.Fuel, "Fuel Consumer"),
CarrierTool(EEditorTool.Consumer, ECarrierType.Coolant, "Coolant Consumer"),
CarrierTool(EEditorTool.Consumer, ECarrierType.Electricity, "Electric Consumer"),
Tool(EEditorTool.Consumer, "Consumer"),
Tool(EEditorTool.Junction, "Junction"),
Tool(EEditorTool.Door, "Door"),
Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"),
@@ -906,6 +965,30 @@ public sealed partial class MainWindow
];
}
private bool IsToolAvailableOnActiveLayer(EditorToolCommand command)
{
if (command.Tool == EEditorTool.Cursor)
return true;
if (m_ActiveLayer == EEditorLayer.Surface)
{
return command.Tool is EEditorTool.Floor
or EEditorTool.Wall
or EEditorTool.Consumer
or EEditorTool.Junction
or EEditorTool.Door
or EEditorTool.AllSeeingEyeTerminal
or EEditorTool.RemedySupply
or EEditorTool.ReactorControl
or EEditorTool.SurfaceHazard
or EEditorTool.Heat
or EEditorTool.Robot;
}
var activeCarrier = LayerCarrier(m_ActiveLayer);
return command.Carrier == activeCarrier && command.Tool is EEditorTool.Underground or EEditorTool.Flow or EEditorTool.Leak;
}
private void SelectReactorFromCell(GridPosition position)
{
var prop = m_Level.GetProp(position);
@@ -913,37 +996,11 @@ public sealed partial class MainWindow
m_SelectedReactorId = prop.ReactorId;
}
private bool TryGetAuthoredLeakCarrier(GridPosition position, out ECarrierType carrier)
{
if (!m_Level.IsFloor(position))
{
carrier = default;
return false;
}
foreach (var candidate in Enum.GetValues<ECarrierType>())
{
if (m_Level.GetUnderground(position, candidate).IsPresent)
{
carrier = candidate;
return true;
}
}
carrier = default;
return false;
}
private void RefreshForecasts()
{
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
}
private void ClearPendingEditorOperation()
{
m_PendingElectricityLeakCell = null;
}
private const double c_MinZoom = 0.5;
private const double c_MaxZoom = 4;
private const double c_ZoomStep = 1.15;
@@ -954,7 +1011,7 @@ public sealed partial class MainWindow
private const int c_BottomLeftCorner = 4;
private const int c_BottomRightCorner = 8;
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
private static readonly Color c_FuelColor = ColorHelper.FromArgb(255, 220, 170, 68);
private static readonly Color c_FuelColor = ColorHelper.FromArgb(255, 220, 65, 65);
private static readonly Color c_CoolantColor = ColorHelper.FromArgb(255, 57, 174, 196);
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
private readonly IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
@@ -963,6 +1020,8 @@ public sealed partial class MainWindow
private StorageFile? m_CurrentFile;
private bool m_DragExceededClickThreshold;
private CanvasBitmap? m_HeatSprite;
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
private GridPosition? m_CursorDragStartCell;
private Point m_LastPanPoint;
private CanvasBitmap? m_LeakSprite;
private bool m_LeftPointerDown;
@@ -970,7 +1029,7 @@ public sealed partial class MainWindow
private LevelState m_Level;
private double m_PanX;
private double m_PanY;
private GridPosition? m_PendingElectricityLeakCell;
private bool m_IsPanning;
private CanvasBitmap? m_RobotSprite;
private GridPosition? m_SelectedCell;
private int? m_SelectedReactorId = 1;

View File

@@ -41,6 +41,23 @@ public sealed class LevelEditorTests
Assert.Equal(next.Leaks, rejected.Leaks);
}
[Fact]
public void ElectricityLeakAccessCyclesAcrossAdjacentFloorFaces()
{
var level = LevelState.Create("Electricity leak editor", 6, 6);
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
level = level.SetTerrain(new(2, 1), ECellTerrain.Wall);
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
var first = LevelEditor.CycleElectricityLeakAccess(level, new(2, 2));
var second = LevelEditor.CycleElectricityLeakAccess(first, new(2, 2));
Assert.Single(first.Leaks);
Assert.Equal(new(3, 2), first.Leaks[0].AccessPosition);
Assert.Single(second.Leaks);
Assert.Equal(new(2, 3), second.Leaks[0].AccessPosition);
}
[Fact]
public void ReactorControlToolCreatesUnboundReactorState()
{
@@ -52,4 +69,27 @@ public sealed class LevelEditorTests
Assert.Equal(new(2, 2), next.Reactors[0].ControlPosition);
Assert.Equal(1, next.Reactors[0].ReactorId);
}
[Fact]
public void MoveOccupantMovesRobotToFloorDestination()
{
var level = LevelState.Create("Move editor", 6, 6) with { Robot = new() { Position = new(1, 1) } };
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
Assert.Equal(new(3, 3), next.Robot.Position);
}
[Fact]
public void MoveOccupantMovesPropAndUpdatesReactorControlPosition()
{
var level = LevelState.Create("Move editor", 6, 6);
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.ReactorControl });
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
Assert.Equal(EPropType.None, next.GetProp(new(1, 1)).Type);
Assert.Equal(EPropType.ReactorControl, next.GetProp(new(3, 3)).Type);
Assert.Equal(new(3, 3), next.Reactors[0].ControlPosition);
}
}