Rework Win2D editor layers
This commit is contained in:
13
TASKS.md
13
TASKS.md
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,10 +55,9 @@
|
||||
</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."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="Shift+left drag pans. Cursor drag moves the robot or a prop."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
m_PanX += deltaX;
|
||||
m_PanY += deltaY;
|
||||
ClampPan();
|
||||
LevelCanvas.Invalidate();
|
||||
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)
|
||||
SelectOrPaintAt(point.Position);
|
||||
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 carrier in OrderedUndergroundLayers())
|
||||
DrawUndergroundLayer(drawing, layout, carrier, CarrierColor(carrier), UndergroundOpacity(carrier));
|
||||
}
|
||||
|
||||
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 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);
|
||||
var cell = m_Level.GetUnderground(position, carrier);
|
||||
if (!cell.IsPresent)
|
||||
continue;
|
||||
|
||||
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 DrawUndergroundCell(CanvasDrawingSession drawing, Rect rect, GridPosition position, ECarrierType carrier, Color color)
|
||||
private void DrawNetworkConnection(
|
||||
CanvasDrawingSession drawing,
|
||||
CanvasLayout layout,
|
||||
ECarrierType carrier,
|
||||
GridPosition position,
|
||||
GridPosition neighbor,
|
||||
Color color,
|
||||
float lineWidth)
|
||||
{
|
||||
var cell = m_Level.GetUnderground(position, carrier);
|
||||
if (!cell.IsPresent)
|
||||
if (!m_Level.InBounds(neighbor) || !m_Level.GetUnderground(neighbor, carrier).IsPresent)
|
||||
return;
|
||||
|
||||
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 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)
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user