diff --git a/README.md b/README.md index 555ba01..489d10b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ C# WinUI 3 + Win2D level editor for the deterministic grid simulation described ## Projects -- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, JSON serialization, and swappable difficulty balancing profiles. -- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting square grid cells, loading/saving levels, advancing simulation turns, and activating the reactor. +- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles. +- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor. - `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior. ## Commands diff --git a/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs index 99be29f..283e7e0 100644 --- a/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs +++ b/src/ReactorMaintenance.Simulation/Effects/FireAndElectricalHazardEffect.cs @@ -11,7 +11,7 @@ public sealed class FireAndElectricalHazardEffect : ISimulationEffect hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease }; var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold; - var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Kind: ECellKind.Generator, Powered: true }; + var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true }; if ((hasFuel && hasIgnition) || hazards.Fire) { hazards = hazards with { diff --git a/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs index 97389a8..5a070dc 100644 --- a/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs +++ b/src/ReactorMaintenance.Simulation/Effects/MachineEffect.cs @@ -6,10 +6,10 @@ public sealed class MachineEffect : ISimulationEffect { public CellState Apply(CellState cell) { - var hazards = cell.Kind switch { - ECellKind.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease }, - ECellKind.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction }, - ECellKind.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease }, + var hazards = cell.Prop switch { + ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease }, + ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction }, + ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease }, _ => cell.Hazards }; diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index 8d60e9a..4a1e63f 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -32,28 +32,37 @@ public static class LevelEditor var cell = level.GetCell(position); cell = tool switch { - EEditorTool.Floor => cell with { Kind = ECellKind.Floor }, + EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor }, EEditorTool.Wall => cell with { - Kind = ECellKind.Wall, + Terrain = ECellTerrain.Wall, + Prop = ECellProp.None, Pipe = EPipeMedium.None, + Flow = Balancing.Current.MinHazardValue, + Pressure = Balancing.Current.MinHazardValue, + LeakRate = Balancing.Current.MinHazardValue, + PipeOpen = false, Powered = false }, - EEditorTool.Reactor => cell with { Kind = ECellKind.Reactor }, + EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor }, EEditorTool.CoolingPump => cell with { - Kind = ECellKind.CoolingPump, + Terrain = ECellTerrain.Floor, + Prop = ECellProp.CoolingPump, Powered = true }, EEditorTool.Generator => cell with { - Kind = ECellKind.Generator, + Terrain = ECellTerrain.Floor, + Prop = ECellProp.Generator, Powered = true }, - EEditorTool.PressureRegulator => cell with { Kind = ECellKind.PressureRegulator }, + EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator }, EEditorTool.DiagnosticTerminal => cell with { - Kind = ECellKind.DiagnosticTerminal, + Terrain = ECellTerrain.Floor, + Prop = ECellProp.DiagnosticTerminal, Powered = true }, EEditorTool.ControlTerminal => cell with { - Kind = ECellKind.ControlTerminal, + Terrain = ECellTerrain.Floor, + Prop = ECellProp.ControlTerminal, Powered = true }, EEditorTool.CoolantPipe => cell with { @@ -100,7 +109,7 @@ public static class LevelEditor _ => cell }; - if (cell.Kind == ECellKind.Wall) + if (cell.Terrain == ECellTerrain.Wall) cell = cell with { Hazards = new() }; return level.SetCell(position, cell); diff --git a/src/ReactorMaintenance.Simulation/LevelSerializer.cs b/src/ReactorMaintenance.Simulation/LevelSerializer.cs index d03aed4..99b8fb4 100644 --- a/src/ReactorMaintenance.Simulation/LevelSerializer.cs +++ b/src/ReactorMaintenance.Simulation/LevelSerializer.cs @@ -1,18 +1,28 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; namespace ReactorMaintenance.Simulation; public static class LevelSerializer { + private const int c_CurrentVersion = 1; + public static string Serialize(LevelState level) { - return JsonSerializer.Serialize(level, Options); + return JsonSerializer.Serialize(new LevelFile { + Version = c_CurrentVersion, + Level = level + }, Options); } public static LevelState Deserialize(string json) { - var level = JsonSerializer.Deserialize(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level."); + var file = JsonSerializer.Deserialize(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level."); + var level = file.Version switch { + c_CurrentVersion => file.Level, + _ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.") + }; + return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level; } @@ -20,4 +30,10 @@ public static class LevelSerializer WriteIndented = true, Converters = { new JsonStringEnumConverter() } }; -} \ No newline at end of file + + private sealed record LevelFile + { + public int Version { get; init; } + public LevelState Level { get; init; } = new(); + } +} diff --git a/src/ReactorMaintenance.Simulation/Models.cs b/src/ReactorMaintenance.Simulation/Models.cs index 1a3e5d6..63ef131 100644 --- a/src/ReactorMaintenance.Simulation/Models.cs +++ b/src/ReactorMaintenance.Simulation/Models.cs @@ -1,10 +1,14 @@ namespace ReactorMaintenance.Simulation; -public enum ECellKind +public enum ECellTerrain { - Empty, Floor, - Wall, + Wall +} + +public enum ECellProp +{ + None, Reactor, CoolingPump, Generator, @@ -68,7 +72,8 @@ public sealed record HazardState public sealed record CellState { - public ECellKind Kind { get; init; } = ECellKind.Floor; + public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor; + public ECellProp Prop { get; init; } public EPipeMedium Pipe { get; init; } public int Flow { get; init; } public int Pressure { get; init; } @@ -78,7 +83,7 @@ public sealed record CellState public bool Powered { get; init; } public bool DoorLocked { get; init; } public HazardState Hazards { get; init; } = new(); - public bool IsWalkable => Kind != ECellKind.Wall && Kind != ECellKind.Empty; + public bool IsWalkable => Terrain != ECellTerrain.Wall; public bool HasPipe => Pipe != EPipeMedium.None; } @@ -110,7 +115,7 @@ public sealed record LevelState for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++) { if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance) - cells[y * width + x] = cells[y * width + x] with { Kind = ECellKind.Wall }; + cells[y * width + x] = cells[y * width + x] with { Terrain = ECellTerrain.Wall }; } } diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index 1bca146..d23eae9 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -116,10 +116,10 @@ public sealed class SimulationEngine(IEnumerable effects, IEn private static GlobalState UpdateGlobal(LevelState level, CellState[] cells) { - var reactorHeat = cells.Where(c => c.Kind == ECellKind.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max(); - var poweredGenerators = cells.Count(c => c is { Kind: ECellKind.Generator, Powered: true, Hazards.Fire: false }); - var poweredPumps = cells.Count(c => c is { Kind: ECellKind.CoolingPump, Powered: true, Hazards.Fire: false }); - var damagedCriticalCells = cells.Count(c => c.Kind is ECellKind.Reactor or ECellKind.Generator or ECellKind.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold); + var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max(); + var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false }); + var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false }); + var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold); var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells); var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold; var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS"; @@ -137,9 +137,9 @@ public sealed class SimulationEngine(IEnumerable effects, IEn private static bool IsReactorReady(LevelState level) { - var hasReactor = level.Cells.Any(c => c.Kind == ECellKind.Reactor); - var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Kind: ECellKind.Generator, Powered: true, Hazards.Fire: false }); - var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Kind: ECellKind.CoolingPump, Powered: true, Hazards.Fire: false }); + var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor); + var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false }); + var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false }); var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold; return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost; } diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs index 753cc01..28d7f9a 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs @@ -25,6 +25,11 @@ public sealed partial class MainWindow { return new(OriginX + x * CellSize, OriginY + y * CellSize, CellSize, CellSize); } + + public Rect DualTileRect(int x, int y) + { + return new(OriginX + (x - 0.5) * CellSize, OriginY + (y - 0.5) * CellSize, CellSize, CellSize); + } } public MainWindow() @@ -157,12 +162,20 @@ public sealed partial class MainWindow var layout = GetLayout(); drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21)); - DrawCells(drawing, layout); + DrawTerrain(drawing, layout); + DrawCellOverlays(drawing, layout); DrawGrid(drawing, layout); DrawRobot(drawing, layout); } - private void DrawCells(CanvasDrawingSession drawing, CanvasLayout layout) + private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout) + { + for (var y = 0; y <= m_Level.Height; y++) + for (var x = 0; x <= m_Level.Width; x++) + DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y)); + } + + private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout) { for (var y = 0; y < m_Level.Height; y++) for (var x = 0; x < m_Level.Width; x++) @@ -171,8 +184,6 @@ public sealed partial class MainWindow var cell = m_Level.GetCell(position); var rect = layout.CellRect(x, y); - drawing.FillRectangle(rect, CellColor(cell)); - if (cell.HasPipe) { var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); @@ -195,31 +206,93 @@ public sealed partial class MainWindow if (m_SelectedCell == position) drawing.DrawRectangle(rect, Colors.White, 3); - DrawCellGlyph(drawing, cell, rect); + DrawCellProp(drawing, cell, rect); } } - private void DrawCellGlyph(CanvasDrawingSession drawing, CellState cell, Rect rect) + private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask) { - var text = cell.Kind switch { - ECellKind.Reactor => "R", - ECellKind.CoolingPump => "C", - ECellKind.Generator => "G", - ECellKind.PressureRegulator => "P", - ECellKind.DiagnosticTerminal => "D", - ECellKind.ControlTerminal => "T", + var wallColor = ColorHelper.FromArgb(255, 54, 61, 68); + var floorColor = ColorHelper.FromArgb(255, 31, 36, 40); + drawing.FillRectangle(rect, wallColor); + + if (floorMask == 0) + return; + + if (floorMask == c_AllFloorCorners) + { + drawing.FillRectangle(rect, floorColor); + return; + } + + var halfWidth = rect.Width / 2; + var halfHeight = rect.Height / 2; + if ((floorMask & c_TopLeftFloor) != 0) + drawing.FillRectangle(new(rect.X, rect.Y, halfWidth, halfHeight), floorColor); + + if ((floorMask & c_TopRightFloor) != 0) + drawing.FillRectangle(new(rect.X + halfWidth, rect.Y, halfWidth, halfHeight), floorColor); + + if ((floorMask & c_BottomLeftFloor) != 0) + drawing.FillRectangle(new(rect.X, rect.Y + halfHeight, halfWidth, halfHeight), floorColor); + + if ((floorMask & c_BottomRightFloor) != 0) + drawing.FillRectangle(new(rect.X + halfWidth, rect.Y + halfHeight, halfWidth, halfHeight), floorColor); + + var center = new Vector2((float)(rect.X + halfWidth), (float)(rect.Y + halfHeight)); + if (floorMask is c_TopLeftFloor or c_TopRightFloor or c_BottomLeftFloor or c_BottomRightFloor) + drawing.FillCircle(center, (float)Math.Min(rect.Width, rect.Height) * 0.18f, floorColor); + } + + private int GetDualTileMask(int x, int y) + { + var mask = 0; + if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor) + mask |= c_TopLeftFloor; + + if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor) + mask |= c_TopRightFloor; + + if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor) + mask |= c_BottomLeftFloor; + + if (GetTerrainOrWall(x, y) == ECellTerrain.Floor) + mask |= c_BottomRightFloor; + + 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) + { + var text = cell.Prop switch { + ECellProp.Reactor => "R", + ECellProp.CoolingPump => "C", + ECellProp.Generator => "G", + ECellProp.PressureRegulator => "P", + ECellProp.DiagnosticTerminal => "D", + ECellProp.ControlTerminal => "T", _ => string.Empty }; if (string.IsNullOrEmpty(text)) return; + var propRect = new Rect(rect.X + rect.Width * 0.18, rect.Y + rect.Height * 0.18, rect.Width * 0.64, rect.Height * 0.64); + drawing.FillRoundedRectangle(propRect, 4, 4, PropColor(cell.Prop)); + drawing.DrawRoundedRectangle(propRect, 4, 4, ColorHelper.FromArgb(210, 12, 14, 16), 2); + using var format = new CanvasTextFormat(); - format.FontSize = Math.Max(14, (float)rect.Width * 0.42f); + format.FontSize = Math.Max(14, (float)rect.Width * 0.34f); format.HorizontalAlignment = CanvasHorizontalAlignment.Center; format.VerticalAlignment = CanvasVerticalAlignment.Center; - drawing.DrawText(text, rect, Colors.White, format); + drawing.DrawText(text, propRect, Colors.White, format); } private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout) @@ -265,22 +338,16 @@ public sealed partial class MainWindow return new(size, originX, originY); } - private static Color CellColor(CellState cell) + private static Color PropColor(ECellProp prop) { - if (cell.Kind == ECellKind.Wall) - return ColorHelper.FromArgb(255, 54, 61, 68); - - if (cell.Hazards.Fire) - return ColorHelper.FromArgb(255, 91, 39, 30); - - return cell.Kind switch { - ECellKind.Reactor => ColorHelper.FromArgb(255, 61, 76, 82), - ECellKind.CoolingPump => ColorHelper.FromArgb(255, 25, 79, 96), - ECellKind.Generator => ColorHelper.FromArgb(255, 86, 75, 35), - ECellKind.PressureRegulator => ColorHelper.FromArgb(255, 70, 78, 98), - ECellKind.DiagnosticTerminal => ColorHelper.FromArgb(255, 39, 84, 62), - ECellKind.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91), - _ => ColorHelper.FromArgb(255, 31, 36, 40) + return prop switch { + ECellProp.Reactor => ColorHelper.FromArgb(255, 61, 76, 82), + ECellProp.CoolingPump => ColorHelper.FromArgb(255, 25, 79, 96), + ECellProp.Generator => ColorHelper.FromArgb(255, 86, 75, 35), + ECellProp.PressureRegulator => ColorHelper.FromArgb(255, 70, 78, 98), + ECellProp.DiagnosticTerminal => ColorHelper.FromArgb(255, 39, 84, 62), + ECellProp.ControlTerminal => ColorHelper.FromArgb(255, 80, 61, 91), + _ => Colors.Transparent }; } @@ -294,7 +361,7 @@ public sealed partial class MainWindow if (m_SelectedCell is { } position && m_Level.InBounds(position)) { var cell = m_Level.GetCell(position); - CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Kind: {cell.Kind}\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}"; + 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."; @@ -306,7 +373,7 @@ public sealed partial class MainWindow { var level = LevelState.Create("Cooling Sector B", 16, 12); level = level.SetCell(new(3, 5), new() { - Kind = ECellKind.CoolingPump, + Prop = ECellProp.CoolingPump, Pipe = EPipeMedium.Coolant, Flow = 5, Pressure = 5, @@ -330,31 +397,36 @@ public sealed partial class MainWindow Pressure = 7 }); level = level.SetCell(new(8, 5), new() { - Kind = ECellKind.Reactor, + Prop = ECellProp.Reactor, Hazards = new() { Heat = 6, Stability = 8 } }); level = level.SetCell(new(2, 8), new() { - Kind = ECellKind.Generator, + Prop = ECellProp.Generator, Pipe = EPipeMedium.Fuel, Flow = 4, Pressure = 6, Powered = true }); level = level.SetCell(new(11, 4), new() { - Kind = ECellKind.DiagnosticTerminal, + Prop = ECellProp.DiagnosticTerminal, Powered = true }); level = level.SetCell(new(12, 8), new() { - Kind = ECellKind.ControlTerminal, + Prop = ECellProp.ControlTerminal, Powered = true }); return level with { Forecasts = new SimulationEngine().Forecast(level) }; } private readonly SimulationEngine m_Simulation = new(); + private const int c_TopLeftFloor = 1; + private const int c_TopRightFloor = 2; + private const int c_BottomLeftFloor = 4; + private const int c_BottomRightFloor = 8; + private const int c_AllFloorCorners = c_TopLeftFloor | c_TopRightFloor | c_BottomLeftFloor | c_BottomRightFloor; private StorageFile? m_CurrentFile; private LevelState m_Level; private bool m_Painting; diff --git a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs index df7ecfd..6a2cacb 100644 --- a/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs +++ b/tests/ReactorMaintenance.Simulation.Tests/SimulationEngineTests.cs @@ -11,7 +11,7 @@ public sealed class SimulationEngineTests { var level = LevelState.Create("Fuel leak", 6, 6) .SetCell(new(2, 2), new() { - Kind = ECellKind.Generator, + Prop = ECellProp.Generator, Pipe = EPipeMedium.Fuel, LeakRate = Balancing.Current.FuelVaporFireThreshold, Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance, @@ -132,7 +132,7 @@ public sealed class SimulationEngineTests { var level = LevelState.Create("Meltdown", 6, 6) .SetCell(new(2, 2), new() { - Kind = ECellKind.Reactor, + Prop = ECellProp.Reactor, Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease } }); @@ -162,7 +162,7 @@ public sealed class SimulationEngineTests { var level = LevelState.Create("Collapse", 6, 6) .SetCell(new(2, 2), new() { - Kind = ECellKind.Generator, + Prop = ECellProp.Generator, Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold } }) with { Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage } @@ -178,15 +178,15 @@ public sealed class SimulationEngineTests { var level = LevelState.Create("Ready", 8, 6) .SetCell(new(2, 2), new() { - Kind = ECellKind.Reactor, + Prop = ECellProp.Reactor, Hazards = new() { Heat = 3 } }) .SetCell(new(3, 2), new() { - Kind = ECellKind.Generator, + Prop = ECellProp.Generator, Powered = true }) .SetCell(new(4, 2), new() { - Kind = ECellKind.CoolingPump, + Prop = ECellProp.CoolingPump, Powered = true }); @@ -208,12 +208,59 @@ public sealed class SimulationEngineTests var json = LevelSerializer.Serialize(level); var loaded = LevelSerializer.Deserialize(json); + Assert.Contains("\"Version\": 1", json); Assert.Equal(level.Name, loaded.Name); - Assert.Equal(ECellKind.Reactor, loaded.GetCell(new(2, 2)).Kind); + Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop); Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe); Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate); } + [Fact] + public void LevelSerializationRejectsUnsupportedVersion() + { + var json = """ + { + "Version": 999, + "Level": {} + } + """; + + var exception = Assert.Throws(() => LevelSerializer.Deserialize(json)); + + Assert.Contains("Unsupported level file version 999", exception.Message); + } + + [Fact] + public void WallToolClearsCellPropsPipesAndHazards() + { + var level = LevelState.Create("Wall", 5, 5); + level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator); + level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe); + level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire); + + var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall); + var cell = edited.GetCell(new(2, 2)); + + Assert.Equal(ECellTerrain.Wall, cell.Terrain); + Assert.Equal(ECellProp.None, cell.Prop); + Assert.Equal(EPipeMedium.None, cell.Pipe); + Assert.False(cell.Powered); + Assert.False(cell.Hazards.Fire); + } + + [Fact] + public void PropToolsKeepFloorTerrain() + { + var level = LevelState.Create("Prop", 5, 5); + level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall); + + var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor); + var cell = edited.GetCell(new(1, 1)); + + Assert.Equal(ECellTerrain.Floor, cell.Terrain); + Assert.Equal(ECellProp.Reactor, cell.Prop); + } + private readonly SimulationEngine m_Engine = new(); private sealed class StepCountingHazard : Hazard