using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.UI; using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.UI; using Microsoft.UI.Input; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media.Imaging; using ReactorMaintenance.Simulation; using System.Globalization; using Windows.Foundation; using Windows.Storage; using Windows.Storage.Pickers; using Windows.System; using Windows.UI; using Windows.UI.Core; using Windows.UI.Popups; using WinRT.Interop; namespace ReactorMaintenance.Win2D; public sealed partial class MainWindow { private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY) { public Rect CellRect(int x, int y) { 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); } } private sealed record ForecastViewModel(BitmapImage Icon, string Message); private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label) { public EEditorTool Tool { get; } = tool; public BitmapImage? Icon { get; } = icon; public string Label { get; } = label; public bool IsSelected { get; set; } } public MainWindow() { InitializeComponent(); m_Level = BuildStarterLevel(); m_EditorTools = Enum.GetValues().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray(); ToolPicker.ItemsSource = m_EditorTools; RefreshInspector(); } private void LevelCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args) { args.TrackAsyncAction(LoadImagesAsync(sender).AsAsyncAction()); } private async Task LoadImagesAsync(CanvasControl sender) { m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png"); m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png"); m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png"); m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png"); m_FireSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "fire.png"); m_PropSprites[ECellProp.Reactor] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "reactor.png"); m_PropSprites[ECellProp.CoolingPump] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "cooling-pump.png"); m_PropSprites[ECellProp.Generator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "generator.png"); m_PropSprites[ECellProp.PressureRegulator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "pressure-regulator.png"); m_PropSprites[ECellProp.DiagnosticTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "diagnostic-terminal.png"); m_PropSprites[ECellProp.ControlTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "control-terminal.png"); m_PipeTilemaps[EPipeMedium.Pressure] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-pressure-tilemap.png"); m_PipeTilemaps[EPipeMedium.Coolant] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-coolant-tilemap.png"); m_PipeTilemaps[EPipeMedium.Fuel] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-fuel-tilemap.png"); } private static async Task LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts) { var path = Path.Combine([AppContext.BaseDirectory, .. pathParts]); return await CanvasBitmap.LoadAsync(sender, path); } private void ToolRadio_Checked(object sender, RoutedEventArgs e) { if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool) { m_SelectedTool = tool.Tool; foreach (var editorTool in m_EditorTools) editorTool.IsSelected = editorTool == tool; } } private void New_Click(object sender, RoutedEventArgs e) { m_Level = BuildStarterLevel(); m_CurrentFile = null; m_SelectedCell = null; RefreshInspector(); LevelCanvas.Invalidate(); } private async void Open_Click(object sender, RoutedEventArgs args) { try { var picker = new FileOpenPicker(); InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this)); picker.FileTypeFilter.Add(".json"); var file = await picker.PickSingleFileAsync(); if (file is null) return; var json = await FileIO.ReadTextAsync(file); m_Level = LevelSerializer.Deserialize(json); m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; m_CurrentFile = file; m_SelectedCell = null; RefreshInspector(); LevelCanvas.Invalidate(); } catch (Exception e) { var messageDialog = new MessageDialog(e.Message); _ = await messageDialog.ShowAsync(); } } private async void Save_Click(object sender, RoutedEventArgs args) { try { var file = m_CurrentFile; if (file is null) { var picker = new FileSavePicker(); InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this)); picker.SuggestedFileName = m_Level.Name.Replace(' ', '-').ToLowerInvariant(); picker.FileTypeChoices.Add("Reactor level", [".json"]); file = await picker.PickSaveFileAsync(); } if (file is null) return; await FileIO.WriteTextAsync(file, LevelSerializer.Serialize(m_Level)); m_CurrentFile = file; } catch (Exception e) { var messageDialog = new MessageDialog(e.Message); _ = await messageDialog.ShowAsync(); } } private void Simulate_Click(object sender, RoutedEventArgs e) { m_Level = m_Simulation.AdvanceTurn(m_Level); RefreshInspector(); LevelCanvas.Invalidate(); } private void Activate_Click(object sender, RoutedEventArgs e) { m_Level = m_Simulation.ActivateReactor(m_Level); RefreshInspector(); LevelCanvas.Invalidate(); } private void LevelCanvas_PointerPressed(object sender, PointerRoutedEventArgs e) { var point = e.GetCurrentPoint(LevelCanvas); if (point.Properties.IsLeftButtonPressed && IsShiftDown()) { m_Panning = true; m_LastPanPoint = point.Position; _ = LevelCanvas.CapturePointer(e.Pointer); e.Handled = true; return; } if (point.Properties.IsRightButtonPressed) { RemovePropAt(point.Position); e.Handled = true; return; } if (point.Properties.IsLeftButtonPressed) { _ = LevelCanvas.CapturePointer(e.Pointer); SelectOrPaintAt(point.Position); m_Painting = m_SelectedTool != EEditorTool.Cursor; e.Handled = true; } } private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) { var point = e.GetCurrentPoint(LevelCanvas); if (m_Panning) { var deltaX = point.Position.X - m_LastPanPoint.X; var deltaY = point.Position.Y - m_LastPanPoint.Y; m_LastPanPoint = point.Position; m_PanX += deltaX; m_PanY += deltaY; ClampPan(); LevelCanvas.Invalidate(); e.Handled = true; return; } if (m_Painting) { PaintAt(point.Position); e.Handled = true; } } private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e) { m_Painting = false; m_Panning = false; LevelCanvas.ReleasePointerCapture(e.Pointer); e.Handled = true; } private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e) { var point = e.GetCurrentPoint(LevelCanvas); var wheelDelta = point.Properties.MouseWheelDelta; if (wheelDelta == 0) return; ZoomAt(point.Position, wheelDelta > 0 ? c_ZoomStep : 1 / c_ZoomStep); e.Handled = true; } private static bool IsShiftDown() { return InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down); } private void ZoomAt(Point point, double zoomFactor) { var oldLayout = GetLayout(); var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize; var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize; m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom); var newCellSize = GetBaseCellSize() * m_Zoom; var originWithoutPan = GetCenteredOrigin(newCellSize); m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize); m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize); ClampPan(); LevelCanvas.Invalidate(); } private void SelectOrPaintAt(Point point) { if (m_SelectedTool == EEditorTool.Cursor) SelectAt(point); else PaintAt(point); } private void SelectAt(Point point) { if (!TryGetGridPosition(point, out var position)) return; m_SelectedCell = position; RefreshInspector(); LevelCanvas.Invalidate(); } private void RemovePropAt(Point point) { if (!TryGetGridPosition(point, out var position)) return; var cell = m_Level.GetCell(position); m_SelectedCell = position; m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None }); m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; RefreshInspector(); LevelCanvas.Invalidate(); } private void PaintAt(Point point) { if (!TryGetGridPosition(point, out var position)) return; m_SelectedCell = position; m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool); m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) }; RefreshInspector(); LevelCanvas.Invalidate(); } private void LevelCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args) { var drawing = args.DrawingSession; var layout = GetLayout(); drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21)); DrawTerrain(drawing, layout); DrawCellOverlays(drawing, layout); DrawGrid(drawing, layout); DrawRobot(drawing, 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++) { var position = new GridPosition(x, y); var cell = m_Level.GetCell(position); var rect = layout.CellRect(x, y); DrawPipe(drawing, position, cell, rect); if (cell.LeakRate > 0) DrawImage(drawing, m_LeakSprite, Inset(rect, 0.12)); if (cell.Hazards.Heat > 0) DrawImage(drawing, m_HeatSprite, Inset(rect, 0.08), Math.Clamp(cell.Hazards.Heat / 10.0f, 0.35f, 0.9f)); if (cell.Hazards.Fire) DrawImage(drawing, m_FireSprite, Inset(rect, 0.08)); if (m_SelectedCell == position) drawing.DrawRectangle(rect, Colors.White, 3); DrawCellProp(drawing, cell, rect); } } } private void DrawPipe(CanvasDrawingSession drawing, GridPosition position, CellState cell, Rect rect) { if (!cell.HasPipe || !m_PipeTilemaps.TryGetValue(cell.Pipe, out var tilemap)) return; var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe)); drawing.DrawImage(tilemap, rect, sourceRect); } private int GetPipeConnectionMask(GridPosition position, EPipeMedium medium) { var mask = 0; if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium)) mask |= c_NorthConnection; if (HasMatchingPipe(position with { X = position.X + 1 }, medium)) mask |= c_EastConnection; if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium)) mask |= c_SouthConnection; if (HasMatchingPipe(position with { X = position.X - 1 }, medium)) mask |= c_WestConnection; return mask; } private bool HasMatchingPipe(GridPosition position, EPipeMedium medium) { return m_Level.InBounds(position) && m_Level.GetCell(position).Pipe == medium; } private static Rect PipeTileSourceRect(int connectionMask) { var tileIndex = connectionMask switch { 0 => 0, c_NorthConnection => 1, c_EastConnection => 2, c_SouthConnection => 3, c_WestConnection => 4, c_NorthConnection | c_EastConnection => 5, c_EastConnection | c_SouthConnection => 6, c_SouthConnection | c_WestConnection => 7, c_WestConnection | c_NorthConnection => 8, c_NorthConnection | c_SouthConnection => 9, c_EastConnection | c_WestConnection => 10, c_NorthConnection | c_EastConnection | c_SouthConnection => 11, c_EastConnection | c_SouthConnection | c_WestConnection => 12, c_SouthConnection | c_WestConnection | c_NorthConnection => 13, c_WestConnection | c_NorthConnection | c_EastConnection => 14, c_NorthConnection | c_EastConnection | c_SouthConnection | c_WestConnection => 15, _ => throw new ArgumentOutOfRangeException(nameof(connectionMask), connectionMask, "Unsupported pipe connection mask.") }; return new( tileIndex % c_PipeTilemapColumns * c_PipeTilemapTileSize, tileIndex / c_PipeTilemapColumns * c_PipeTilemapTileSize, c_PipeTilemapTileSize, c_PipeTilemapTileSize); } private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1) { if (image is not null) drawing.DrawImage(image, rect, image.Bounds, opacity); } 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; var wallMask = c_AllCorners ^ floorMask; var sourceRect = TilemapSourceRect(wallMask); drawing.DrawImage(m_TerrainTilemap, rect, sourceRect); } private static Rect TilemapSourceRect(int wallMask) { var tilePosition = wallMask switch { c_BottomLeftCorner => new(0, 0), c_TopRightCorner | c_BottomRightCorner => new(1, 0), c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0), c_BottomLeftCorner | c_BottomRightCorner => new(3, 0), c_TopLeftCorner | c_BottomRightCorner => new(0, 1), c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1), c_AllCorners => new(2, 1), c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1), c_TopRightCorner => new(0, 2), c_TopLeftCorner | c_TopRightCorner => new(1, 2), c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2), c_BottomLeftCorner | c_TopLeftCorner => new(3, 2), 0 => new(0, 3), c_BottomRightCorner => new(1, 3), c_BottomLeftCorner | c_TopRightCorner => new(2, 3), c_TopLeftCorner => new GridPosition(3, 3), _ => throw new ArgumentOutOfRangeException(nameof(wallMask), wallMask, "Unsupported tile mask.") }; return new( tilePosition.X * c_TilemapTileSize, tilePosition.Y * c_TilemapTileSize, c_TilemapTileSize, c_TilemapTileSize); } private int GetDualTileMask(int x, int y) { var mask = 0; if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor) mask |= c_TopLeftCorner; if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor) mask |= c_TopRightCorner; if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor) mask |= c_BottomLeftCorner; if (GetTerrainOrWall(x, y) == ECellTerrain.Floor) mask |= c_BottomRightCorner; return mask; } private ECellTerrain GetTerrainOrWall(int x, int y) { var position = new GridPosition(x, y); return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall; } private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect) { if (m_PropSprites.TryGetValue(cell.Prop, out var sprite)) drawing.DrawImage(sprite, rect, sprite.Bounds); } private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout) { for (var x = 0; x <= m_Level.Width; x++) { var xPos = (float)(layout.OriginX + (x * layout.CellSize)); drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1); } for (var y = 0; y <= m_Level.Height; y++) { var yPos = (float)(layout.OriginY + (y * layout.CellSize)); drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1); } } private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout) { var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y); DrawImage(drawing, m_RobotSprite, rect); } private bool TryGetGridPosition(Point point, out GridPosition position) { var layout = GetLayout(); var x = (int)((point.X - layout.OriginX) / layout.CellSize); var y = (int)((point.Y - layout.OriginY) / layout.CellSize); position = new(x, y); return m_Level.InBounds(position); } private CanvasLayout GetLayout() { ClampPan(); var cellSize = GetBaseCellSize() * m_Zoom; var centeredOrigin = GetCenteredOrigin(cellSize); return new(cellSize, centeredOrigin.X + m_PanX, centeredOrigin.Y + m_PanY); } private double GetBaseCellSize() { var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); var size = Math.Floor(Math.Min(availableWidth / m_Level.Width, availableHeight / m_Level.Height)); return Math.Max(20, size); } private Point GetCenteredOrigin(double cellSize) { var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2); } private void ClampPan() { var cellSize = GetBaseCellSize() * m_Zoom; var contentWidth = cellSize * m_Level.Width; var contentHeight = cellSize * m_Level.Height; var availableWidth = Math.Max(1, LevelCanvas.ActualWidth); var availableHeight = Math.Max(1, LevelCanvas.ActualHeight); m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth); m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight); } private static double ClampAxisPan(double pan, double contentSize, double availableSize) { if (contentSize <= availableSize) return 0; var maxPan = (contentSize - availableSize) / 2; return Math.Clamp(pan, -maxPan, maxPan); } private void RefreshInspector() { LevelNameText.Text = m_Level.Name; TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture); StatusText.Text = m_Level.Global.Status; GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10"; if (m_SelectedCell is { } position && m_Level.InBounds(position)) { var cell = m_Level.GetCell(position); CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}"; } else CellText.Text = "No cell selected."; ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel(FailureIcon(forecast.Kind), forecast.Message)).ToArray(); } private static BitmapImage FailureIcon(EFailureKind kind) { return ImageFromOutputPath("Images", "Failures", FailureIconFileName(kind)); } private static string FailureIconFileName(EFailureKind kind) { return kind switch { EFailureKind.PipeBurst => "failure-pipe-burst.png", EFailureKind.Ignition => "failure-ignition.png", EFailureKind.Meltdown => "failure-meltdown.png", EFailureKind.StabilityCollapse => "failure-stability-collapse.png", EFailureKind.ReactorReady => "failure-reactor-ready.png", _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported failure kind.") }; } private static BitmapImage? EditorToolIcon(EEditorTool tool) { return tool switch { EEditorTool.Cursor => PropImage("cursor.png"), EEditorTool.Floor => PropImage("floor.png"), EEditorTool.Wall => PropImage("wall.png"), EEditorTool.Reactor => PropImage("reactor.png"), EEditorTool.CoolingPump => PropImage("cooling-pump.png"), EEditorTool.Generator => PropImage("generator.png"), EEditorTool.PressureRegulator => PropImage("pressure-regulator.png"), EEditorTool.DiagnosticTerminal => PropImage("diagnostic-terminal.png"), EEditorTool.ControlTerminal => PropImage("control-terminal.png"), EEditorTool.CoolantPipe => PipeImage("pipe-coolant-tilemap.png"), EEditorTool.FuelPipe => PipeImage("pipe-fuel-tilemap.png"), EEditorTool.PressurePipe => PipeImage("pipe-pressure-tilemap.png"), EEditorTool.Leak => PropImage("leak.png"), EEditorTool.Repair => PropImage("repair.png"), EEditorTool.Heat => PropImage("heat.png"), EEditorTool.Fire => PropImage("fire.png"), EEditorTool.Robot => PropImage("robot.png"), _ => throw new ArgumentOutOfRangeException(nameof(tool), tool, "Unsupported editor tool.") }; } private static BitmapImage PropImage(string fileName) { return ImageFromOutputPath("Images", "Props", fileName); } private static BitmapImage PipeImage(string fileName) { return ImageFromOutputPath("Images", "Pipes", fileName); } private static BitmapImage ImageFromOutputPath(params string[] pathParts) { return new(new(Path.Combine([AppContext.BaseDirectory, .. pathParts]))); } private static LevelState BuildStarterLevel() { var level = LevelState.Create("Cooling Sector B", 16, 12); level = level.SetCell(new(3, 5), new() { Prop = ECellProp.CoolingPump, Pipe = EPipeMedium.Coolant, Flow = 5, Pressure = 5, Powered = true }); level = level.SetCell(new(4, 5), new() { Pipe = EPipeMedium.Coolant, Flow = 5, Pressure = 7 }); level = level.SetCell(new(5, 5), new() { Pipe = EPipeMedium.Coolant, Flow = 3, Pressure = 8, LeakRate = 2, Integrity = 4 }); level = level.SetCell(new(6, 5), new() { Pipe = EPipeMedium.Coolant, Flow = 3, Pressure = 7 }); level = level.SetCell(new(8, 5), new() { Prop = ECellProp.Reactor, Hazards = new() { Heat = 6, Stability = 8 } }); level = level.SetCell(new(2, 8), new() { Prop = ECellProp.Generator, Pipe = EPipeMedium.Fuel, Flow = 4, Pressure = 6, Powered = true }); level = level.SetCell(new(11, 4), new() { Prop = ECellProp.DiagnosticTerminal, Powered = true }); level = level.SetCell(new(12, 8), new() { Prop = ECellProp.ControlTerminal, Powered = true }); return level with { Forecasts = new SimulationEngine().Forecast(level) }; } private const int c_TilemapTileSize = 512; private const int c_PipeTilemapTileSize = 256; private const int c_PipeTilemapColumns = 4; private const int c_TopLeftCorner = 1; private const int c_TopRightCorner = 2; private const int c_BottomLeftCorner = 4; private const int c_BottomRightCorner = 8; private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner; private const int c_NorthConnection = 1; private const int c_EastConnection = 2; private const int c_SouthConnection = 4; private const int c_WestConnection = 8; private const double c_MinZoom = 0.5; private const double c_MaxZoom = 4; private const double c_ZoomStep = 1.15; private readonly SimulationEngine m_Simulation = new(); private readonly Dictionary m_PropSprites = []; private readonly Dictionary m_PipeTilemaps = []; private StorageFile? m_CurrentFile; private LevelState m_Level; private IReadOnlyList m_EditorTools = []; private bool m_Painting; private bool m_Panning; private Point m_LastPanPoint; private GridPosition? m_SelectedCell; private EEditorTool m_SelectedTool = EEditorTool.Cursor; private double m_Zoom = 1; private double m_PanX; private double m_PanY; private CanvasBitmap? m_TerrainTilemap; private CanvasBitmap? m_RobotSprite; private CanvasBitmap? m_LeakSprite; private CanvasBitmap? m_HeatSprite; private CanvasBitmap? m_FireSprite; }