739 lines
28 KiB
C#
739 lines
28 KiB
C#
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<EEditorTool>().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<CanvasBitmap> 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<ECellProp, CanvasBitmap> m_PropSprites = [];
|
|
private readonly Dictionary<EPipeMedium, CanvasBitmap> m_PipeTilemaps = [];
|
|
private StorageFile? m_CurrentFile;
|
|
private LevelState m_Level;
|
|
private IReadOnlyList<EditorToolViewModel> 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;
|
|
}
|