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

@@ -69,4 +69,4 @@ public abstract class Balancing
public abstract int RemedyBlockTurns { get; } public abstract int RemedyBlockTurns { get; }
public abstract int HeatShieldSteps { get; } public abstract int HeatShieldSteps { get; }
public abstract int InventoryCapacityPerRemedy { get; } public abstract int InventoryCapacityPerRemedy { get; }
} }

View File

@@ -52,4 +52,4 @@ public class NormalBalancing : Balancing
public override int RemedyBlockTurns => 2; public override int RemedyBlockTurns => 2;
public override int HeatShieldSteps => 3; public override int HeatShieldSteps => 3;
public override int InventoryCapacityPerRemedy => 3; public override int InventoryCapacityPerRemedy => 3;
} }

View File

@@ -162,4 +162,4 @@ public static class LevelEditor
] ]
}; };
} }
} }

View File

@@ -39,4 +39,4 @@ public static class LevelSerializer
public int Version { get; init; } public int Version { get; init; }
public LevelState? Level { get; init; } public LevelState? Level { get; init; }
} }
} }

View File

@@ -200,4 +200,4 @@ public sealed class LevelValidator
{ {
return level.InBounds(position) && level.GetProp(position).Type == propType; return level.InBounds(position) && level.GetProp(position).Type == propType;
} }
} }

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
@@ -532,4 +531,4 @@ public sealed record LevelState
public RobotState Robot { get; init; } = new(); public RobotState Robot { get; init; } = new();
public GlobalState Global { get; init; } = new(); public GlobalState Global { get; init; } = new();
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>(); public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
} }

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 };
} }
@@ -732,4 +734,4 @@ public sealed class SimulationEngine
} }
private readonly LevelValidator m_Validator = new(); private readonly LevelValidator m_Validator = new();
} }

View File

@@ -1,19 +1,19 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
namespace ReactorMaintenance.Win2D; namespace ReactorMaintenance.Win2D;
public partial class App public partial class App
{ {
public App() public App()
{ {
InitializeComponent(); InitializeComponent();
} }
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override void OnLaunched(LaunchActivatedEventArgs args)
{ {
m_Window = new MainWindow(); m_Window = new MainWindow();
m_Window.Activate(); m_Window.Activate();
} }
private Window? m_Window; private Window? m_Window;
} }

View File

@@ -15,8 +15,10 @@
<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 Icon="Accept" Label="Activate" Click="Activate_Click" /> <AppBarButton Label="Interact" Click="Interact_Click" />
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
</CommandBar> </CommandBar>
<Grid Grid.Row="1" ColumnSpacing="0"> <Grid Grid.Row="1" ColumnSpacing="0">
@@ -37,19 +39,22 @@
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<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"
</ToggleButton> FontSize="12" />
</DataTemplate> </ToggleButton>
</ItemsControl.ItemTemplate> </DataTemplate>
</ItemsControl> </ItemsControl.ItemTemplate>
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" /> </ItemsControl>
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE" <TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
TextWrapping="Wrap" /> Foreground="#9EA7AE" TextWrapping="Wrap" />
</StackPanel> <TextBlock Text="Door chooses the first adjacent floor edge. Reactor controls auto-bind to the first available consumers."
</ScrollViewer> Foreground="#9EA7AE"
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>
<Grid Grid.Column="1" Background="#101215"> <Grid Grid.Column="1" Background="#101215">
<canvas:CanvasControl <canvas:CanvasControl
@@ -95,18 +100,10 @@
<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> </Border>
<ColumnDefinition Width="32" /> </DataTemplate>
<ColumnDefinition Width="*" /> </ItemsControl.ItemTemplate>
</Grid.ColumnDefinitions>
<Image Width="28" Height="28" Source="{Binding Icon}" />
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="#F4F1E8"
TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

File diff suppressed because it is too large Load Diff

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>
@@ -16,9 +16,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0" /> <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1839" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -200,4 +200,4 @@ public sealed class SimulationEngineTests
} }
private readonly SimulationEngine m_Engine = new(); private readonly SimulationEngine m_Engine = new();
} }