using System.Numerics; using Windows.Foundation; using Windows.Storage; using Windows.Storage.Pickers; using Windows.UI; using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Geometry; using Microsoft.Graphics.Canvas.Text; using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using ReactorMaintenance.Simulation; using System.Globalization; 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); } } public MainWindow() { InitializeComponent(); m_Level = BuildStarterLevel(); ToolPicker.ItemsSource = Enum.GetValues(); ToolPicker.SelectedItem = m_SelectedTool; RefreshInspector(); } private void ToolPicker_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (ToolPicker.SelectedItem is EEditorTool tool) m_SelectedTool = 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) { m_Painting = true; _ = LevelCanvas.CapturePointer(e.Pointer); PaintAt(e.GetCurrentPoint(LevelCanvas).Position); } private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) { if (m_Painting) PaintAt(e.GetCurrentPoint(LevelCanvas).Position); } private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e) { m_Painting = false; LevelCanvas.ReleasePointerCapture(e.Pointer); } 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); if (cell.HasPipe) { var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); var pipeColor = cell.Pipe switch { EPipeMedium.Coolant => Colors.DeepSkyBlue, EPipeMedium.Fuel => Colors.Goldenrod, EPipeMedium.Pressure => Colors.LightSteelBlue, _ => Colors.Transparent }; drawing.DrawLine(center with { X = (float)rect.X + 6 }, center with { X = (float)(rect.X + rect.Width - 6) }, pipeColor, Math.Max(3, (float)rect.Width / 7)); drawing.DrawLine(center with { Y = (float)rect.Y + 6 }, center with { Y = (float)(rect.Y + rect.Height - 6) }, pipeColor, Math.Max(3, (float)rect.Width / 7)); } if (cell.LeakRate > 0) drawing.DrawCircle(new((float)(rect.X + rect.Width - 10), (float)(rect.Y + 10)), 5, Colors.OrangeRed, 2); if (cell.Hazards.Fire) drawing.FillCircle(new((float)(rect.X + rect.Width * 0.5), (float)(rect.Y + rect.Height * 0.5)), (float)rect.Width * 0.24f, Colors.OrangeRed); if (m_SelectedCell == position) drawing.DrawRectangle(rect, Colors.White, 3); DrawCellProp(drawing, cell, rect); } } private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask) { var wallColor = ColorHelper.FromArgb(255, 54, 61, 68); var floorColor = ColorHelper.FromArgb(255, 31, 36, 40); if (floorMask == 0) { drawing.FillRectangle(rect, wallColor); return; } if (floorMask == c_AllFloorCorners) { drawing.FillRectangle(rect, floorColor); return; } drawing.FillRectangle(rect, floorColor); var wallMask = c_AllFloorCorners ^ floorMask; if ((wallMask & c_TopLeftFloor) != 0) DrawTerrainCorner(drawing, rect, wallColor, c_TopLeftFloor); if ((wallMask & c_TopRightFloor) != 0) DrawTerrainCorner(drawing, rect, wallColor, c_TopRightFloor); if ((wallMask & c_BottomLeftFloor) != 0) DrawTerrainCorner(drawing, rect, wallColor, c_BottomLeftFloor); if ((wallMask & c_BottomRightFloor) != 0) DrawTerrainCorner(drawing, rect, wallColor, c_BottomRightFloor); } private static void DrawTerrainCorner(CanvasDrawingSession drawing, Rect rect, Color color, int corner) { var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); var radiusX = (float)rect.Width / 2; var radiusY = (float)rect.Height / 2; var start = corner switch { c_TopLeftFloor => new Vector2(center.X, (float)rect.Y), c_TopRightFloor => new Vector2((float)(rect.X + rect.Width), center.Y), c_BottomRightFloor => new Vector2(center.X, (float)(rect.Y + rect.Height)), c_BottomLeftFloor => new Vector2((float)rect.X, center.Y), _ => center }; var end = corner switch { c_TopLeftFloor => new Vector2((float)rect.X, center.Y), c_TopRightFloor => new Vector2(center.X, (float)rect.Y), c_BottomRightFloor => new Vector2((float)(rect.X + rect.Width), center.Y), c_BottomLeftFloor => new Vector2(center.X, (float)(rect.Y + rect.Height)), _ => center }; using var builder = new CanvasPathBuilder(drawing); builder.BeginFigure(center); builder.AddLine(start); builder.AddArc(end, radiusX, radiusY, 0, CanvasSweepDirection.CounterClockwise, CanvasArcSize.Small); builder.EndFigure(CanvasFigureLoop.Closed); using var geometry = CanvasGeometry.CreatePath(builder); drawing.FillGeometry(geometry, color); } 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.34f); format.HorizontalAlignment = CanvasHorizontalAlignment.Center; format.VerticalAlignment = CanvasVerticalAlignment.Center; drawing.DrawText(text, propRect, Colors.White, format); } 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); var center = new Vector2((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2)); drawing.FillCircle(center, (float)rect.Width * 0.28f, Colors.White); drawing.DrawCircle(center, (float)rect.Width * 0.28f, Colors.Black, 2); } 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() { 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)); size = Math.Max(20, size); var originX = Math.Max(0, (availableWidth - size * m_Level.Width) / 2); var originY = Math.Max(0, (availableHeight - size * m_Level.Height) / 2); return new(size, originX, originY); } private static Color PropColor(ECellProp prop) { 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 }; } 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; } 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 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; private GridPosition? m_SelectedCell; private EEditorTool m_SelectedTool = EEditorTool.Floor; }