Rework Win2D editor for design model
This commit is contained in:
12
README.md
12
README.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
17
TASKS.md
17
TASKS.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user