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 ## Projects
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles. - `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 painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor. - `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. - `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
## Commands ## Commands
```powershell ```powershell
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj 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 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. - 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 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. - 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 ## Completed Work
@@ -24,16 +26,23 @@
- Attempted `dotnet jb cleanupcode --build=False ...`; unavailable in this environment because `dotnet-jb` is not installed. - 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. - Reviewed the first slice and fixed an action-resolution maintainability issue before commit.
- Verified `git diff --check` reports no whitespace errors. - 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 ## Current Work
- Commit the first simulation-core rewrite slice. - Commit the Win2D editor rewrite slice.
## Future Work ## 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. 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. 2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring.
3. Add editor workflows for reactor bindings, door edge selection, electricity wall leak faces, rule events, and layer-specific painting. 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. 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. 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. 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 Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);
public sealed record ValidationIssue(string Message, GridPosition? Position = null); public sealed record ValidationIssue(string Message, GridPosition? Position = null);
public sealed record ValidationReport 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 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 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() } }; return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
} }
private static bool IsReactorReady(LevelState level, ReactorBinding reactor) private static bool IsReactorReady(LevelState level, ReactorBinding reactor)
{ {
return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel) return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel)
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant) && HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity) && HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat; && level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
} }
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier) private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
@@ -535,10 +536,11 @@ public sealed class SimulationEngine
private LevelState AdvanceDurations(LevelState level) private LevelState AdvanceDurations(LevelState level)
{ {
var surface = level.Surface.Select(cell => cell with { var surface = level.Surface.Select(cell => cell with {
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1), FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1), CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1) ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
}).ToArray(); })
.ToArray();
return level with { Surface = surface }; return level with { Surface = surface };
} }

View File

@@ -15,7 +15,9 @@
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" /> <AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" /> <AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
<AppBarSeparator /> <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" /> <AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
</CommandBar> </CommandBar>
@@ -38,15 +40,18 @@
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}" <ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}" Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
Padding="5" Margin="0,0,8,8"> Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
<Image Width="96" Height="96" Source="{Binding Icon}" Stretch="Uniform" /> <TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords" TextAlignment="Center"
FontSize="12" />
</ToggleButton> </ToggleButton>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" /> <TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE" 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" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
@@ -95,15 +100,7 @@
<DataTemplate> <DataTemplate>
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8" <Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
CornerRadius="3"> CornerRadius="3">
<Grid ColumnSpacing="8"> <TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
<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>
</Border> </Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>

View File

@@ -1,11 +1,10 @@
using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI; using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI; using Microsoft.UI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using ReactorMaintenance.Simulation; using ReactorMaintenance.Simulation;
using System.Globalization; using System.Globalization;
using Windows.Foundation; using Windows.Foundation;
@@ -21,23 +20,17 @@ public sealed partial class MainWindow
{ {
private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY) 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); return new(OriginX + position.X * CellSize, OriginY + position.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);
} }
} }
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 EEditorTool Tool { get; } = tool;
public BitmapImage? Icon { get; } = icon;
public string Label { get; } = label; public string Label { get; } = label;
public bool IsSelected { get; set; } public bool IsSelected { get; set; }
} }
@@ -47,7 +40,7 @@ public sealed partial class MainWindow
InitializeComponent(); InitializeComponent();
m_Level = BuildStarterLevel(); 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; ToolPicker.ItemsSource = m_EditorTools;
RefreshInspector(); RefreshInspector();
} }
@@ -63,18 +56,6 @@ public sealed partial class MainWindow
m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png"); m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png"); m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.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) 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) private void ToolToggle_Checked(object sender, RoutedEventArgs e)
{ {
if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool) if ((sender as FrameworkElement)?.DataContext is not EditorToolViewModel tool)
{ return;
m_SelectedTool = tool.Tool;
foreach (var editorTool in m_EditorTools) m_SelectedTool = tool.Tool;
editorTool.IsSelected = editorTool == tool; foreach (var editorTool in m_EditorTools)
} editorTool.IsSelected = editorTool == tool;
} }
private void New_Click(object sender, RoutedEventArgs e) private void New_Click(object sender, RoutedEventArgs e)
@@ -115,8 +96,8 @@ public sealed partial class MainWindow
return; return;
var json = await FileIO.ReadTextAsync(file); var json = await FileIO.ReadTextAsync(file);
m_Level = LevelSerializer.Deserialize(json); var loaded = LevelSerializer.Deserialize(json);
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; m_Level = loaded with { Forecasts = m_Simulation.Forecast(loaded) };
m_CurrentFile = file; m_CurrentFile = file;
m_SelectedCell = null; m_SelectedCell = null;
RefreshInspector(); RefreshInspector();
@@ -133,6 +114,10 @@ public sealed partial class MainWindow
{ {
try try
{ {
var report = new LevelValidator().Validate(m_Level);
if (!report.IsValid)
throw new InvalidOperationException(report.Errors[0].Message);
var file = m_CurrentFile; var file = m_CurrentFile;
if (file is null) 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(); RefreshInspector();
LevelCanvas.Invalidate(); LevelCanvas.Invalidate();
} }
@@ -175,43 +174,42 @@ public sealed partial class MainWindow
var point = e.GetCurrentPoint(LevelCanvas); var point = e.GetCurrentPoint(LevelCanvas);
if (point.Properties.IsRightButtonPressed) if (point.Properties.IsRightButtonPressed)
{ {
RemovePropAt(point.Position); ClearAt(point.Position);
e.Handled = true; e.Handled = true;
return; return;
} }
if (point.Properties.IsLeftButtonPressed) if (!point.Properties.IsLeftButtonPressed)
{ return;
_ = LevelCanvas.CapturePointer(e.Pointer);
m_LeftPointerDown = true; _ = LevelCanvas.CapturePointer(e.Pointer);
m_LeftPointerDownPoint = point.Position; m_LeftPointerDown = true;
m_LastPanPoint = point.Position; m_LeftPointerDownPoint = point.Position;
m_DragExceededClickThreshold = false; m_LastPanPoint = point.Position;
e.Handled = true; m_DragExceededClickThreshold = false;
} e.Handled = true;
} }
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{ {
var point = e.GetCurrentPoint(LevelCanvas); if (!m_LeftPointerDown)
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;
return; 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) private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
@@ -226,6 +224,12 @@ public sealed partial class MainWindow
e.Handled = true; 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) private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{ {
var point = e.GetCurrentPoint(LevelCanvas); var point = e.GetCurrentPoint(LevelCanvas);
@@ -237,59 +241,34 @@ public sealed partial class MainWindow
e.Handled = true; 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) 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)) if (!TryGetGridPosition(point, out var position))
return; return;
m_SelectedCell = position; 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(); RefreshInspector();
LevelCanvas.Invalidate(); LevelCanvas.Invalidate();
} }
private void RemovePropAt(Point point) private void ClearAt(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)
{ {
if (!TryGetGridPosition(point, out var position)) if (!TryGetGridPosition(point, out var position))
return; return;
m_SelectedCell = position; 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) }; m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
RefreshInspector(); RefreshInspector();
LevelCanvas.Invalidate(); LevelCanvas.Invalidate();
@@ -302,212 +281,125 @@ public sealed partial class MainWindow
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21)); drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
DrawTerrain(drawing, layout); DrawTerrain(drawing, layout);
DrawCellOverlays(drawing, layout); DrawUnderground(drawing, layout);
//DrawGrid(drawing, layout); DrawSurface(drawing, layout);
DrawDoors(drawing, layout);
DrawProps(drawing, layout);
DrawLeaks(drawing, layout);
DrawRobot(drawing, layout); DrawRobot(drawing, layout);
DrawGrid(drawing, layout);
} }
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout 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++) 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);
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y)); 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 rect = Inset(layout.CellRect(position), 0.18);
{ DrawUndergroundCell(drawing, rect, position, ECarrierType.Fuel, c_FuelColor);
var position = new GridPosition(x, y); DrawUndergroundCell(drawing, Inset(rect, -0.08), position, ECarrierType.Coolant, c_CoolantColor);
var cell = m_Level.GetCell(position); DrawUndergroundCell(drawing, Inset(rect, -0.16), position, ECarrierType.Electricity, c_ElectricityColor);
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);
}
} }
} }
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; return;
var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe)); drawing.DrawRectangle(rect, color, cell.State == EUndergroundState.Leaking ? 4 : 2);
drawing.DrawImage(tilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic); 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; foreach (var position in AllPositions().Where(m_Level.IsFloor))
if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium)) {
mask |= c_NorthConnection; var surface = m_Level.GetSurface(position);
var rect = layout.CellRect(position);
if (HasMatchingPipe(position with { X = position.X + 1 }, medium)) FillHazard(drawing, rect, surface.Fuel, c_FuelColor, 0.08);
mask |= c_EastConnection; FillHazard(drawing, rect, surface.Coolant, c_CoolantColor, 0.18);
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28);
if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium)) if (surface.Heat > 0)
mask |= c_SouthConnection; DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f));
}
if (HasMatchingPipe(position with { X = position.X - 1 }, medium))
mask |= c_WestConnection;
return mask;
} }
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; if (amount <= 0)
}
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)
return; return;
var wallMask = c_AllCorners ^ floorMask; var alpha = (byte)Math.Clamp(40 + amount / Balancing.Current.MaxValue * 130, 40, 170);
var sourceRect = TilemapSourceRect(wallMask); drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
drawing.DrawImage(m_TerrainTilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
} }
private static Rect TilemapSourceRect(int wallMask) private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
{ {
var tilePosition = wallMask switch { foreach (var door in m_Level.Doors)
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++)
{ {
var xPos = (float)(layout.OriginX + (x * layout.CellSize)); var centerA = Center(layout.CellRect(door.A));
drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1); 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)); var prop = m_Level.GetProp(position);
drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1); 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) private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
{ {
var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y); DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04));
DrawImage(drawing, m_RobotSprite, rect); }
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) 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 availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); 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() private void ClampPan()
{ {
var cellSize = GetBaseCellSize() * m_Zoom; var cellSize = GetBaseCellSize() * m_Zoom;
var contentWidth = cellSize * m_Level.Width; m_PanX = ClampAxisPan(m_PanX, cellSize * m_Level.Width, Math.Max(1, LevelCanvas.ActualWidth));
var contentHeight = cellSize * m_Level.Height; m_PanY = ClampAxisPan(m_PanY, cellSize * m_Level.Height, Math.Max(1, LevelCanvas.ActualHeight));
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);
} }
private static double ClampAxisPan(double pan, double contentSize, double availableSize) 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); 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() private void RefreshInspector()
{ {
LevelNameText.Text = m_Level.Name; LevelNameText.Text = m_Level.Name;
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture); TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
StatusText.Text = m_Level.Global.Status; StatusText.Text = $"{m_Level.Global.LevelState}: {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"; 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)) 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();
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();
} }
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 { return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}";
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.")
};
} }
private static BitmapImage? EditorToolIcon(EEditorTool tool) private static string Format(float value)
{ {
return tool switch { return value.ToString("0.0", CultureInfo.InvariantCulture);
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])));
} }
private static LevelState BuildStarterLevel() private static LevelState BuildStarterLevel()
{ {
var level = LevelState.Create("Cooling Sector B", 16, 12); var level = LevelState.Create("Cooling Sector B", 16, 12);
level = level.SetCell(new(3, 5), new() { level = AddNetwork(level, ECarrierType.Fuel, new(2, 3), new(5, 3));
Prop = ECellProp.CoolingPump, level = AddNetwork(level, ECarrierType.Coolant, new(2, 5), new(5, 5));
Pipe = EPipeMedium.Coolant, level = AddNetwork(level, ECarrierType.Electricity, new(2, 7), new(5, 7));
Flow = 5, level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
Pressure = 5, level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
Powered = true 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.SetCell(new(4, 5), new() { level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
Pipe = EPipeMedium.Coolant, level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
Flow = 5, level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
Pressure = 7 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.SetCell(new(5, 5), new() { level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
Pipe = EPipeMedium.Coolant, Leaks = [new LeakState { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
Flow = 3, Doors = [new DoorState { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }],
Pressure = 8, Robot = new() { Position = new(10, 5) },
LeakRate = 2, Reactors = [
Integrity = 4 new ReactorBinding {
}); ReactorId = 1,
level = level.SetCell(new(6, 5), new() { ControlPosition = new(10, 5),
Pipe = EPipeMedium.Coolant, FuelConsumerPosition = new(5, 3),
Flow = 3, CoolantConsumerPosition = new(5, 5),
Pressure = 7 ElectricityConsumerPosition = new(5, 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
});
return level with { Forecasts = new SimulationEngine().Forecast(level) }; return level with { Forecasts = new SimulationEngine().Forecast(level) };
} }
private const int c_TilemapTileSize = 512; private static LevelState AddNetwork(LevelState level, ECarrierType carrier, GridPosition start, GridPosition end)
private const int c_PipeTilemapTileSize = 256; {
private const int c_PipeTilemapColumns = 4; var minX = Math.Min(start.X, end.X);
private const int c_TopLeftCorner = 1; var maxX = Math.Max(start.X, end.X);
private const int c_TopRightCorner = 2; var minY = Math.Min(start.Y, end.Y);
private const int c_BottomLeftCorner = 4; var maxY = Math.Max(start.Y, end.Y);
private const int c_BottomRightCorner = 8; for (var y = minY; y <= maxY; y++)
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner; {
private const int c_NorthConnection = 1; for (var x = minX; x <= maxX; x++)
private const int c_EastConnection = 2; level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
private const int c_SouthConnection = 4; }
private const int c_WestConnection = 8;
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_MinZoom = 0.5;
private const double c_MaxZoom = 4; private const double c_MaxZoom = 4;
private const double c_ZoomStep = 1.15; private const double c_ZoomStep = 1.15;
private const double c_ClickPixelThreshold = 10; 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 SimulationEngine m_Simulation = new();
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
private readonly Dictionary<EPipeMedium, CanvasBitmap> m_PipeTilemaps = [];
private StorageFile? m_CurrentFile; private StorageFile? m_CurrentFile;
private LevelState m_Level; private LevelState m_Level;
private IReadOnlyList<EditorToolViewModel> m_EditorTools = []; private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
@@ -725,5 +718,4 @@ public sealed partial class MainWindow
private CanvasBitmap? m_RobotSprite; private CanvasBitmap? m_RobotSprite;
private CanvasBitmap? m_LeakSprite; private CanvasBitmap? m_LeakSprite;
private CanvasBitmap? m_HeatSprite; private CanvasBitmap? m_HeatSprite;
private CanvasBitmap? m_FireSprite;
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <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> <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace> <RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>