Rework Win2D editor for design model

This commit is contained in:
2026-05-10 18:59:00 +02:00
parent 851f6d27e8
commit 30963a9bde
14 changed files with 817 additions and 816 deletions

View File

@@ -1,18 +1,20 @@
# Reactor Maintenance
# Reactor Maintenance
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`.
## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, leaks, doors, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
## Commands
```powershell
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
```
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.

View File

@@ -6,7 +6,9 @@
- Scope approved: implement `docs/design.md` end-to-end with deterministic defaults and no backward compatibility.
- Simulation core has been replaced with the first design-native model and deterministic engine slice.
- Simulation and test projects now target `net10.0` because this Linux environment only has the .NET 10 runtime.
- Win2D editor still references the removed legacy model and is the next major implementation area.
- Win2D editor has been rewritten against the new design model.
- Win2D project now targets `net10.0-windows10.0.19041.0` to match the simulation project.
- Linux can restore and compile the referenced simulation project, but full WinUI/XAML compilation still requires a Windows-capable XAML compiler environment.
## Completed Work
@@ -24,16 +26,23 @@
- Attempted `dotnet jb cleanupcode --build=False ...`; unavailable in this environment because `dotnet-jb` is not installed.
- Reviewed the first slice and fixed an action-resolution maintainability issue before commit.
- Verified `git diff --check` reports no whitespace errors.
- Ran `dotnet jb cleanupcode --build=False ...` successfully after ReSharper install and normalized line endings back to LF.
- Reworked the Win2D editor for the new model: full tool list, layer-aware painting, terrain, underground carriers, surface hazards, props, doors, leaks, robot, forecasts, save validation, starter level, and simple play actions.
- Removed old editor dependencies on legacy props, pressure pipes, smoke, fire, and global power/cooling/core-stability fields.
- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes after the editor rewrite: 11 passed.
- Attempted Win2D build on Linux with `dotnet build src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj -p:EnableWindowsTargeting=true -p:Platform=x64`; it fails at Windows `XamlCompiler.exe` with exec format error.
- Attempted managed XAML compiler path with `-p:UseXamlCompilerExecutable=false`; it fails loading the WinUI XAML compiler task dependency under this Linux/.NET 10 setup.
- Updated `README.md` for the new design-model editor, .NET 10 target, and Linux/Windows build expectations.
## Current Work
- Commit the first simulation-core rewrite slice.
- Commit the Win2D editor rewrite slice.
## Future Work
1. Expand simulation fidelity where the first slice is intentionally simplified: junction branch inference, ambiguity validation, complete pair table coverage, richer rule predicates/effects, and stronger forecast proof cases.
2. Update the Win2D editor for all authored layers and new runtime inspection.
3. Add editor workflows for reactor bindings, door edge selection, electricity wall leak faces, rule events, and layer-specific painting.
2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring.
3. Verify and polish the Win2D app on Windows where the XAML compiler can run.
4. Update README and any affected docs to reflect the new schema, .NET target, editor controls, and deterministic defaults.
5. Build the Win2D project on a Windows-capable environment after the editor rewrite.
6. Add broader tests for junction ratios, ambiguous junctions, all rule event families, serialization edge cases, and editor operations.

View File

@@ -331,7 +331,6 @@ public sealed record RuleEventState
}
public sealed record Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);
public sealed record ValidationIssue(string Message, GridPosition? Position = null);
public sealed record ValidationReport

View File

@@ -454,16 +454,17 @@ public sealed class SimulationEngine
var hasCritical = level.Surface.Any(surface => BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical);
var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired);
var state = hasCritical ? ELevelState.Critical : hasCaution ? ELevelState.Caution : ELevelState.Stable;
var state = hasCritical ? ELevelState.Critical :
hasCaution ? ELevelState.Caution : ELevelState.Stable;
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
}
private static bool IsReactorReady(LevelState level, ReactorBinding reactor)
{
return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel)
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
}
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
@@ -535,10 +536,11 @@ public sealed class SimulationEngine
private LevelState AdvanceDurations(LevelState level)
{
var surface = level.Surface.Select(cell => cell with {
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
}).ToArray();
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
})
.ToArray();
return level with { Surface = surface };
}

View File

@@ -15,7 +15,9 @@
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
<AppBarSeparator />
<AppBarButton Icon="Play" Label="Simulate" Click="Simulate_Click" />
<AppBarButton Icon="Play" Label="End Turn" Click="EndTurn_Click" />
<AppBarButton Label="Interact" Click="Interact_Click" />
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
</CommandBar>
@@ -38,15 +40,18 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
Padding="5" Margin="0,0,8,8">
<Image Width="96" Height="96" Source="{Binding Icon}" Stretch="Uniform" />
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
<TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords" TextAlignment="Center"
FontSize="12" />
</ToggleButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
Foreground="#9EA7AE" TextWrapping="Wrap" />
<TextBlock Text="Door chooses the first adjacent floor edge. Reactor controls auto-bind to the first available consumers."
Foreground="#9EA7AE"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
@@ -95,15 +100,7 @@
<DataTemplate>
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
CornerRadius="3">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Width="28" Height="28" Source="{Binding Icon}" />
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="#F4F1E8"
TextWrapping="Wrap" />
</Grid>
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

@@ -1,11 +1,10 @@
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using ReactorMaintenance.Simulation;
using System.Globalization;
using Windows.Foundation;
@@ -21,23 +20,17 @@ public sealed partial class MainWindow
{
private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
{
public Rect CellRect(int x, int y)
public Rect CellRect(GridPosition position)
{
return new(OriginX + (x * CellSize), OriginY + (y * CellSize), CellSize, CellSize);
}
public Rect DualTileRect(int x, int y)
{
return new(OriginX + ((x - 0.5) * CellSize), OriginY + ((y - 0.5) * CellSize), CellSize, CellSize);
return new(OriginX + position.X * CellSize, OriginY + position.Y * CellSize, CellSize, CellSize);
}
}
private sealed record ForecastViewModel(BitmapImage Icon, string Message);
private sealed record ForecastViewModel(string Message);
private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label)
private sealed class EditorToolViewModel(EEditorTool tool, string label)
{
public EEditorTool Tool { get; } = tool;
public BitmapImage? Icon { get; } = icon;
public string Label { get; } = label;
public bool IsSelected { get; set; }
}
@@ -47,7 +40,7 @@ public sealed partial class MainWindow
InitializeComponent();
m_Level = BuildStarterLevel();
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, ToolLabel(tool)) { IsSelected = tool == m_SelectedTool }).ToArray();
ToolPicker.ItemsSource = m_EditorTools;
RefreshInspector();
}
@@ -63,18 +56,6 @@ public sealed partial class MainWindow
m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
m_FireSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "fire.png");
m_PropSprites[ECellProp.Reactor] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "reactor.png");
m_PropSprites[ECellProp.CoolingPump] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "cooling-pump.png");
m_PropSprites[ECellProp.Generator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "generator.png");
m_PropSprites[ECellProp.PressureRegulator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "pressure-regulator.png");
m_PropSprites[ECellProp.DiagnosticTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "diagnostic-terminal.png");
m_PropSprites[ECellProp.ControlTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "control-terminal.png");
m_PipeTilemaps[EPipeMedium.Pressure] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-pressure-tilemap.png");
m_PipeTilemaps[EPipeMedium.Coolant] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-coolant-tilemap.png");
m_PipeTilemaps[EPipeMedium.Fuel] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-fuel-tilemap.png");
}
private static async Task<CanvasBitmap> LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
@@ -85,12 +66,12 @@ public sealed partial class MainWindow
private void ToolToggle_Checked(object sender, RoutedEventArgs e)
{
if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
{
m_SelectedTool = tool.Tool;
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
}
if ((sender as FrameworkElement)?.DataContext is not EditorToolViewModel tool)
return;
m_SelectedTool = tool.Tool;
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
}
private void New_Click(object sender, RoutedEventArgs e)
@@ -115,8 +96,8 @@ public sealed partial class MainWindow
return;
var json = await FileIO.ReadTextAsync(file);
m_Level = LevelSerializer.Deserialize(json);
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
var loaded = LevelSerializer.Deserialize(json);
m_Level = loaded with { Forecasts = m_Simulation.Forecast(loaded) };
m_CurrentFile = file;
m_SelectedCell = null;
RefreshInspector();
@@ -133,6 +114,10 @@ public sealed partial class MainWindow
{
try
{
var report = new LevelValidator().Validate(m_Level);
if (!report.IsValid)
throw new InvalidOperationException(report.Errors[0].Message);
var file = m_CurrentFile;
if (file is null)
{
@@ -156,9 +141,23 @@ public sealed partial class MainWindow
}
}
private void Simulate_Click(object sender, RoutedEventArgs e)
private void EndTurn_Click(object sender, RoutedEventArgs e)
{
m_Level = m_Simulation.AdvanceTurn(m_Level);
m_Level = m_Simulation.EndTurn(m_Level);
RefreshInspector();
LevelCanvas.Invalidate();
}
private void Interact_Click(object sender, RoutedEventArgs e)
{
m_Level = m_Simulation.InteractProp(m_Level);
RefreshInspector();
LevelCanvas.Invalidate();
}
private void HeatShield_Click(object sender, RoutedEventArgs e)
{
m_Level = m_Simulation.ApplyHeatShield(m_Level);
RefreshInspector();
LevelCanvas.Invalidate();
}
@@ -175,43 +174,42 @@ public sealed partial class MainWindow
var point = e.GetCurrentPoint(LevelCanvas);
if (point.Properties.IsRightButtonPressed)
{
RemovePropAt(point.Position);
ClearAt(point.Position);
e.Handled = true;
return;
}
if (point.Properties.IsLeftButtonPressed)
{
_ = LevelCanvas.CapturePointer(e.Pointer);
m_LeftPointerDown = true;
m_LeftPointerDownPoint = point.Position;
m_LastPanPoint = point.Position;
m_DragExceededClickThreshold = false;
e.Handled = true;
}
if (!point.Properties.IsLeftButtonPressed)
return;
_ = LevelCanvas.CapturePointer(e.Pointer);
m_LeftPointerDown = true;
m_LeftPointerDownPoint = point.Position;
m_LastPanPoint = point.Position;
m_DragExceededClickThreshold = false;
e.Handled = true;
}
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
if (m_LeftPointerDown)
{
var deltaX = point.Position.X - m_LastPanPoint.X;
var deltaY = point.Position.Y - m_LastPanPoint.Y;
m_LastPanPoint = point.Position;
var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
m_DragExceededClickThreshold = true;
m_PanX += deltaX;
m_PanY += deltaY;
ClampPan();
LevelCanvas.Invalidate();
e.Handled = true;
if (!m_LeftPointerDown)
return;
}
var point = e.GetCurrentPoint(LevelCanvas);
var deltaX = point.Position.X - m_LastPanPoint.X;
var deltaY = point.Position.Y - m_LastPanPoint.Y;
m_LastPanPoint = point.Position;
var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
if (Math.Sqrt(totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY) > c_ClickPixelThreshold)
m_DragExceededClickThreshold = true;
m_PanX += deltaX;
m_PanY += deltaY;
ClampPan();
LevelCanvas.Invalidate();
e.Handled = true;
}
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
@@ -226,6 +224,12 @@ public sealed partial class MainWindow
e.Handled = true;
}
private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
{
m_LeftPointerDown = false;
m_DragExceededClickThreshold = false;
}
private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
@@ -237,59 +241,34 @@ public sealed partial class MainWindow
e.Handled = true;
}
private void ZoomAt(Point point, double zoomFactor)
{
var oldLayout = GetLayout();
var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
var newCellSize = GetBaseCellSize() * m_Zoom;
var originWithoutPan = GetCenteredOrigin(newCellSize);
m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize);
m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize);
ClampPan();
LevelCanvas.Invalidate();
}
private void SelectOrPaintAt(Point point)
{
if (m_SelectedTool == EEditorTool.Cursor)
SelectAt(point);
else
PaintAt(point);
}
private void SelectAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
m_SelectedCell = position;
if (m_SelectedTool != EEditorTool.Cursor)
{
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
m_Level = AutoBindReactors(m_Level);
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
}
RefreshInspector();
LevelCanvas.Invalidate();
}
private void RemovePropAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
var cell = m_Level.GetCell(position);
m_SelectedCell = position;
m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None });
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
RefreshInspector();
LevelCanvas.Invalidate();
}
private void PaintAt(Point point)
private void ClearAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
m_SelectedCell = position;
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
Doors = m_Level.Doors.Where(door => door.A != position && door.B != position).ToArray(),
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
};
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
RefreshInspector();
LevelCanvas.Invalidate();
@@ -302,212 +281,125 @@ public sealed partial class MainWindow
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
DrawTerrain(drawing, layout);
DrawCellOverlays(drawing, layout);
//DrawGrid(drawing, layout);
DrawUnderground(drawing, layout);
DrawSurface(drawing, layout);
DrawDoors(drawing, layout);
DrawProps(drawing, layout);
DrawLeaks(drawing, layout);
DrawRobot(drawing, layout);
DrawGrid(drawing, layout);
}
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var y = 0; y <= m_Level.Height; y++)
foreach (var position in AllPositions())
{
for (var x = 0; x <= m_Level.Width; x++)
{
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
}
var rect = layout.CellRect(position);
var color = m_Level.GetTerrain(position) == ECellTerrain.Wall ? ColorHelper.FromArgb(255, 41, 47, 52) : ColorHelper.FromArgb(255, 32, 38, 42);
drawing.FillRectangle(rect, color);
}
}
private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var y = 0; y < m_Level.Height; y++)
foreach (var position in AllPositions())
{
for (var x = 0; x < m_Level.Width; x++)
{
var position = new GridPosition(x, y);
var cell = m_Level.GetCell(position);
var rect = layout.CellRect(x, y);
DrawPipe(drawing, position, cell, rect);
if (cell.LeakRate > 0)
DrawImage(drawing, m_LeakSprite, Inset(rect, 0.12));
if (cell.Hazards.Heat > 0)
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.08), Math.Clamp(cell.Hazards.Heat / 10.0f, 0.35f, 0.9f));
if (cell.Hazards.Fire)
DrawImage(drawing, m_FireSprite, Inset(rect, 0.08));
if (m_SelectedCell == position)
drawing.DrawRectangle(rect, Colors.White, 3);
DrawCellProp(drawing, cell, rect);
}
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);
}
}
private void DrawPipe(CanvasDrawingSession drawing, GridPosition position, CellState cell, Rect rect)
private void DrawUndergroundCell(CanvasDrawingSession drawing, Rect rect, GridPosition position, ECarrierType carrier, Color color)
{
if (!cell.HasPipe || !m_PipeTilemaps.TryGetValue(cell.Pipe, out var tilemap))
var cell = m_Level.GetUnderground(position, carrier);
if (!cell.IsPresent)
return;
var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe));
drawing.DrawImage(tilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
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);
}
private int GetPipeConnectionMask(GridPosition position, EPipeMedium medium)
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout)
{
var mask = 0;
if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium))
mask |= c_NorthConnection;
if (HasMatchingPipe(position with { X = position.X + 1 }, medium))
mask |= c_EastConnection;
if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium))
mask |= c_SouthConnection;
if (HasMatchingPipe(position with { X = position.X - 1 }, medium))
mask |= c_WestConnection;
return mask;
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);
if (surface.Heat > 0)
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f));
}
}
private bool HasMatchingPipe(GridPosition position, EPipeMedium medium)
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset)
{
return m_Level.InBounds(position) && m_Level.GetCell(position).Pipe == medium;
}
private static Rect PipeTileSourceRect(int connectionMask)
{
var tileIndex = connectionMask switch {
0 => 0,
c_NorthConnection => 1,
c_EastConnection => 2,
c_SouthConnection => 3,
c_WestConnection => 4,
c_NorthConnection | c_EastConnection => 5,
c_EastConnection | c_SouthConnection => 6,
c_SouthConnection | c_WestConnection => 7,
c_WestConnection | c_NorthConnection => 8,
c_NorthConnection | c_SouthConnection => 9,
c_EastConnection | c_WestConnection => 10,
c_NorthConnection | c_EastConnection | c_SouthConnection => 11,
c_EastConnection | c_SouthConnection | c_WestConnection => 12,
c_SouthConnection | c_WestConnection | c_NorthConnection => 13,
c_WestConnection | c_NorthConnection | c_EastConnection => 14,
c_NorthConnection | c_EastConnection | c_SouthConnection | c_WestConnection => 15,
_ => throw new ArgumentOutOfRangeException(nameof(connectionMask), connectionMask, "Unsupported pipe connection mask.")
};
return new(
tileIndex % c_PipeTilemapColumns * c_PipeTilemapTileSize,
tileIndex / c_PipeTilemapColumns * c_PipeTilemapTileSize,
c_PipeTilemapTileSize,
c_PipeTilemapTileSize);
}
private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
{
if (image is not null)
drawing.DrawImage(image, rect, image.Bounds, opacity, CanvasImageInterpolation.HighQualityCubic);
}
private static Rect Inset(Rect rect, double fraction)
{
var inset = rect.Width * fraction;
return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
}
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
{
if (m_TerrainTilemap is null)
if (amount <= 0)
return;
var wallMask = c_AllCorners ^ floorMask;
var sourceRect = TilemapSourceRect(wallMask);
drawing.DrawImage(m_TerrainTilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
var alpha = (byte)Math.Clamp(40 + amount / Balancing.Current.MaxValue * 130, 40, 170);
drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
}
private static Rect TilemapSourceRect(int wallMask)
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
{
var tilePosition = wallMask switch {
c_BottomLeftCorner => new(0, 0),
c_TopRightCorner | c_BottomRightCorner => new(1, 0),
c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
c_AllCorners => new(2, 1),
c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
c_TopRightCorner => new(0, 2),
c_TopLeftCorner | c_TopRightCorner => new(1, 2),
c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
0 => new(0, 3),
c_BottomRightCorner => new(1, 3),
c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
c_TopLeftCorner => new GridPosition(3, 3),
_ => throw new ArgumentOutOfRangeException(nameof(wallMask), wallMask, "Unsupported tile mask.")
};
return new(
tilePosition.X * c_TilemapTileSize,
tilePosition.Y * c_TilemapTileSize,
c_TilemapTileSize,
c_TilemapTileSize);
}
private int GetDualTileMask(int x, int y)
{
var mask = 0;
if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
mask |= c_TopLeftCorner;
if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
mask |= c_TopRightCorner;
if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
mask |= c_BottomLeftCorner;
if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
mask |= c_BottomRightCorner;
return mask;
}
private ECellTerrain GetTerrainOrWall(int x, int y)
{
var position = new GridPosition(x, y);
return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall;
}
private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect)
{
if (m_PropSprites.TryGetValue(cell.Prop, out var sprite))
drawing.DrawImage(sprite, rect, sprite.Bounds, 1.0f, CanvasImageInterpolation.HighQualityCubic);
}
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
{
for (var x = 0; x <= m_Level.Width; x++)
foreach (var door in m_Level.Doors)
{
var xPos = (float)(layout.OriginX + (x * layout.CellSize));
drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1);
var centerA = Center(layout.CellRect(door.A));
var centerB = Center(layout.CellRect(door.B));
drawing.DrawLine((float)centerA.X, (float)centerA.Y, (float)centerB.X, (float)centerB.Y, door.State == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, 5);
}
}
for (var y = 0; y <= m_Level.Height; y++)
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout)
{
foreach (var position in AllPositions())
{
var yPos = (float)(layout.OriginY + (y * layout.CellSize));
drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
var prop = m_Level.GetProp(position);
if (prop.Type == EPropType.None)
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)));
}
}
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout)
{
foreach (var leak in m_Level.Leaks.Where(leak => !leak.Repaired))
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1));
}
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
{
var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y);
DrawImage(drawing, m_RobotSprite, rect);
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04));
}
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
{
foreach (var position in AllPositions())
{
var rect = layout.CellRect(position);
drawing.DrawRectangle(rect, ColorHelper.FromArgb(90, 91, 104, 115), 1);
if (m_SelectedCell == position)
drawing.DrawRectangle(rect, Colors.White, 3);
}
}
private static void DrawCenteredText(CanvasDrawingSession drawing, string text, Rect rect, Color color, float fontSize)
{
using var format = new CanvasTextFormat {
FontSize = fontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Center,
VerticalAlignment = CanvasVerticalAlignment.Center,
WordWrapping = CanvasWordWrapping.NoWrap
};
drawing.DrawText(text, rect, color, format);
}
private bool TryGetGridPosition(Point point, out GridPosition position)
@@ -539,19 +431,14 @@ public sealed partial class MainWindow
{
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2);
return new((availableWidth - cellSize * m_Level.Width) / 2, (availableHeight - cellSize * m_Level.Height) / 2);
}
private void ClampPan()
{
var cellSize = GetBaseCellSize() * m_Zoom;
var contentWidth = cellSize * m_Level.Width;
var contentHeight = cellSize * m_Level.Height;
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth);
m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight);
m_PanX = ClampAxisPan(m_PanX, cellSize * m_Level.Width, Math.Max(1, LevelCanvas.ActualWidth));
m_PanY = ClampAxisPan(m_PanY, cellSize * m_Level.Height, Math.Max(1, LevelCanvas.ActualHeight));
}
private static double ClampAxisPan(double pan, double contentSize, double availableSize)
@@ -563,152 +450,258 @@ public sealed partial class MainWindow
return Math.Clamp(pan, -maxPan, maxPan);
}
private void ZoomAt(Point point, double zoomFactor)
{
var oldLayout = GetLayout();
var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
var newCellSize = GetBaseCellSize() * m_Zoom;
var originWithoutPan = GetCenteredOrigin(newCellSize);
m_PanX = point.X - originWithoutPan.X - cellX * newCellSize;
m_PanY = point.Y - originWithoutPan.Y - cellY * newCellSize;
ClampPan();
LevelCanvas.Invalidate();
}
private void RefreshInspector()
{
LevelNameText.Text = m_Level.Name;
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
StatusText.Text = m_Level.Global.Status;
GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10";
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
GlobalText.Text = $"Actions: {m_Level.Global.ActionsRemaining}/{Balancing.Current.ActionsPerTurn}\n"
+ $"All-seeing-eye: {(m_Level.Global.AllSeeingEyeUnlocked ? "unlocked" : "locked")}\n"
+ $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {m_Level.Doors.Count}";
if (m_SelectedCell is { } position && m_Level.InBounds(position))
{
var cell = m_Level.GetCell(position);
CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}";
}
else
CellText.Text = "No cell selected.";
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel(FailureIcon(forecast.Kind), forecast.Message)).ToArray();
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();
}
private static BitmapImage FailureIcon(EFailureKind kind)
private string CellInspectionText(GridPosition position)
{
return ImageFromOutputPath("Images", "Failures", FailureIconFileName(kind));
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 $"Position: {position.X},{position.Y}\n"
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
+ $"Prop: {prop.Type} {prop.Carrier} {prop.SwitchState} {prop.ServiceState}\n"
+ $"Fuel: {UndergroundText(fuel)}\n"
+ $"Coolant: {UndergroundText(coolant)}\n"
+ $"Electricity: {UndergroundText(electricity)}\n"
+ $"Surface fuel/coolant/electricity/heat: {Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}\n"
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
}
private static string FailureIconFileName(EFailureKind kind)
private static string UndergroundText(UndergroundCell cell)
{
return kind switch {
EFailureKind.PipeBurst => "failure-pipe-burst.png",
EFailureKind.Ignition => "failure-ignition.png",
EFailureKind.Meltdown => "failure-meltdown.png",
EFailureKind.StabilityCollapse => "failure-stability-collapse.png",
EFailureKind.ReactorReady => "failure-reactor-ready.png",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported failure kind.")
};
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}";
}
private static BitmapImage? EditorToolIcon(EEditorTool tool)
private static string Format(float value)
{
return tool switch {
EEditorTool.Cursor => PropImage("cursor.png"),
EEditorTool.Floor => PropImage("floor.png"),
EEditorTool.Wall => PropImage("wall.png"),
EEditorTool.Reactor => PropImage("reactor.png"),
EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
EEditorTool.Generator => PropImage("generator.png"),
EEditorTool.PressureRegulator => PropImage("pressure-regulator.png"),
EEditorTool.DiagnosticTerminal => PropImage("diagnostic-terminal.png"),
EEditorTool.ControlTerminal => PropImage("control-terminal.png"),
EEditorTool.CoolantPipe => PipeImage("pipe-coolant-tilemap.png"),
EEditorTool.FuelPipe => PipeImage("pipe-fuel-tilemap.png"),
EEditorTool.PressurePipe => PipeImage("pipe-pressure-tilemap.png"),
EEditorTool.Leak => PropImage("leak.png"),
EEditorTool.Repair => PropImage("repair.png"),
EEditorTool.Heat => PropImage("heat.png"),
EEditorTool.Fire => PropImage("fire.png"),
EEditorTool.Robot => PropImage("robot.png"),
_ => throw new ArgumentOutOfRangeException(nameof(tool), tool, "Unsupported editor tool.")
};
}
private static BitmapImage PropImage(string fileName)
{
return ImageFromOutputPath("Images", "Props", fileName);
}
private static BitmapImage PipeImage(string fileName)
{
return ImageFromOutputPath("Images", "Pipes", fileName);
}
private static BitmapImage ImageFromOutputPath(params string[] pathParts)
{
return new(new(Path.Combine([AppContext.BaseDirectory, .. pathParts])));
return value.ToString("0.0", CultureInfo.InvariantCulture);
}
private static LevelState BuildStarterLevel()
{
var level = LevelState.Create("Cooling Sector B", 16, 12);
level = level.SetCell(new(3, 5), new() {
Prop = ECellProp.CoolingPump,
Pipe = EPipeMedium.Coolant,
Flow = 5,
Pressure = 5,
Powered = true
});
level = level.SetCell(new(4, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 5,
Pressure = 7
});
level = level.SetCell(new(5, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 3,
Pressure = 8,
LeakRate = 2,
Integrity = 4
});
level = level.SetCell(new(6, 5), new() {
Pipe = EPipeMedium.Coolant,
Flow = 3,
Pressure = 7
});
level = level.SetCell(new(8, 5), new() {
Prop = ECellProp.Reactor,
Hazards = new() {
Heat = 6,
Stability = 8
}
});
level = level.SetCell(new(2, 8), new() {
Prop = ECellProp.Generator,
Pipe = EPipeMedium.Fuel,
Flow = 4,
Pressure = 6,
Powered = true
});
level = level.SetCell(new(11, 4), new() {
Prop = ECellProp.DiagnosticTerminal,
Powered = true
});
level = level.SetCell(new(12, 8), new() {
Prop = ECellProp.ControlTerminal,
Powered = true
});
level = AddNetwork(level, ECarrierType.Fuel, new(2, 3), new(5, 3));
level = AddNetwork(level, ECarrierType.Coolant, new(2, 5), new(5, 5));
level = AddNetwork(level, ECarrierType.Electricity, new(2, 7), new(5, 7));
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal });
level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield });
level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
Leaks = [new LeakState { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
Doors = [new DoorState { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }],
Robot = new() { Position = new(10, 5) },
Reactors = [
new ReactorBinding {
ReactorId = 1,
ControlPosition = new(10, 5),
FuelConsumerPosition = new(5, 3),
CoolantConsumerPosition = new(5, 5),
ElectricityConsumerPosition = new(5, 7)
}
]
};
return level with { Forecasts = new SimulationEngine().Forecast(level) };
}
private const int c_TilemapTileSize = 512;
private const int c_PipeTilemapTileSize = 256;
private const int c_PipeTilemapColumns = 4;
private const int c_TopLeftCorner = 1;
private const int c_TopRightCorner = 2;
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 const int c_NorthConnection = 1;
private const int c_EastConnection = 2;
private const int c_SouthConnection = 4;
private const int c_WestConnection = 8;
private static LevelState AddNetwork(LevelState level, ECarrierType carrier, GridPosition start, GridPosition end)
{
var minX = Math.Min(start.X, end.X);
var maxX = Math.Max(start.X, end.X);
var minY = Math.Min(start.Y, end.Y);
var maxY = Math.Max(start.Y, end.Y);
for (var y = minY; y <= maxY; y++)
{
for (var x = minX; x <= maxX; x++)
level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
}
return level;
}
private static LevelState AutoBindReactors(LevelState level)
{
if (level.Reactors.Count == 0)
return level;
var fuel = FirstConsumer(level, ECarrierType.Fuel);
var coolant = FirstConsumer(level, ECarrierType.Coolant);
var electricity = FirstConsumer(level, ECarrierType.Electricity);
var reactors = level.Reactors.Select(reactor => reactor with {
FuelConsumerPosition = fuel ?? reactor.FuelConsumerPosition,
CoolantConsumerPosition = coolant ?? reactor.CoolantConsumerPosition,
ElectricityConsumerPosition = electricity ?? reactor.ElectricityConsumerPosition
})
.ToArray();
return level with { Reactors = reactors };
}
private static GridPosition? FirstConsumer(LevelState level, ECarrierType carrier)
{
return AllPositions(level).FirstOrDefault(position => level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var propCarrier } && propCarrier == carrier);
}
private IEnumerable<GridPosition> AllPositions()
{
return AllPositions(m_Level);
}
private static IEnumerable<GridPosition> AllPositions(LevelState level)
{
for (var y = 0; y < level.Height; y++)
{
for (var x = 0; x < level.Width; x++)
yield return new(x, y);
}
}
private static Rect Inset(Rect rect, double fraction)
{
var inset = rect.Width * fraction;
return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
}
private static Point Center(Rect rect)
{
return new(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
}
private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
{
if (image is not null)
drawing.DrawImage(image, rect, image.Bounds, opacity);
}
private static Color PropColor(PropState prop)
{
return prop.Type switch {
EPropType.Flow => CarrierColor(prop.Carrier),
EPropType.Consumer => ColorHelper.FromArgb(255, 93, 123, 170),
EPropType.TJunction or EPropType.CrossJunction => ColorHelper.FromArgb(255, 143, 111, 178),
EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55),
EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156),
EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73),
_ => Colors.Gray
};
}
private static Color CarrierColor(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => c_FuelColor,
ECarrierType.Coolant => c_CoolantColor,
ECarrierType.Electricity => c_ElectricityColor,
_ => Colors.White
};
}
private static string PropLabel(PropState prop)
{
return prop.Type switch {
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON",
EPropType.TJunction => $"T {prop.TJunctionMode}",
EPropType.CrossJunction => $"X {prop.CrossJunctionMode}",
EPropType.Door => "DOOR",
EPropType.AllSeeingEyeTerminal => "EYE",
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
EPropType.ReactorControl => "REACT",
_ => string.Empty
};
}
private static string CarrierShort(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => "F",
ECarrierType.Coolant => "C",
ECarrierType.Electricity => "E",
_ => "?"
};
}
private static string RemedyShort(ERemedyType remedy)
{
return remedy switch {
ERemedyType.FuelNeutralizer => "F REM",
ERemedyType.CoolantNeutralizer => "C REM",
ERemedyType.ElectricityNeutralizer => "E REM",
ERemedyType.HeatShield => "H SHD",
_ => "REM"
};
}
private static string ToolLabel(EEditorTool tool)
{
return tool switch {
EEditorTool.FuelUnderground => "Fuel Net",
EEditorTool.CoolantUnderground => "Coolant Net",
EEditorTool.ElectricityUnderground => "Electric Net",
EEditorTool.FuelFlow => "Fuel Source",
EEditorTool.CoolantFlow => "Coolant Source",
EEditorTool.ElectricityFlow => "Electric Source",
EEditorTool.FuelConsumer => "Fuel Consumer",
EEditorTool.CoolantConsumer => "Coolant Consumer",
EEditorTool.ElectricityConsumer => "Electric Consumer",
EEditorTool.AllSeeingEyeTerminal => "Eye Terminal",
EEditorTool.FuelRemedySupply => "Fuel Remedy",
EEditorTool.CoolantRemedySupply => "Coolant Remedy",
EEditorTool.ElectricityRemedySupply => "Electric Remedy",
EEditorTool.HeatRemedySupply => "Heat Shield",
EEditorTool.ReactorControl => "Reactor",
EEditorTool.FuelHazard => "Fuel Hazard",
EEditorTool.CoolantHazard => "Coolant Hazard",
EEditorTool.ElectricityHazard => "Electric Hazard",
_ => tool.ToString()
};
}
private const double c_MinZoom = 0.5;
private const double c_MaxZoom = 4;
private const double c_ZoomStep = 1.15;
private const double c_ClickPixelThreshold = 10;
private static readonly Color c_FuelColor = ColorHelper.FromArgb(255, 220, 170, 68);
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 SimulationEngine m_Simulation = new();
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
private readonly Dictionary<EPipeMedium, CanvasBitmap> m_PipeTilemaps = [];
private StorageFile? m_CurrentFile;
private LevelState m_Level;
private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
@@ -725,5 +718,4 @@ public sealed partial class MainWindow
private CanvasBitmap? m_RobotSprite;
private CanvasBitmap? m_LeakSprite;
private CanvasBitmap? m_HeatSprite;
private CanvasBitmap? m_FireSprite;
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>