1016 lines
39 KiB
C#
1016 lines
39 KiB
C#
using Microsoft.Graphics.Canvas;
|
|
using Microsoft.Graphics.Canvas.Text;
|
|
using Microsoft.Graphics.Canvas.UI;
|
|
using Microsoft.Graphics.Canvas.UI.Xaml;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Input;
|
|
using ReactorMaintenance.Simulation;
|
|
using System.Globalization;
|
|
using Windows.Foundation;
|
|
using Windows.Storage;
|
|
using Windows.Storage.Pickers;
|
|
using Windows.UI;
|
|
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(GridPosition position)
|
|
{
|
|
return new(OriginX + (position.X * CellSize), OriginY + (position.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(string Message);
|
|
|
|
private sealed class EditorToolViewModel(EditorToolCommand command, string label)
|
|
{
|
|
public EditorToolCommand Command { get; } = command;
|
|
public string Label { get; } = label;
|
|
public bool IsSelected { get; set; }
|
|
}
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
|
|
m_Level = BuildStarterLevel();
|
|
m_EditorTools = BuildEditorTools();
|
|
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");
|
|
}
|
|
|
|
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 ToolToggle_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
if ((sender as FrameworkElement)?.DataContext is not EditorToolViewModel tool)
|
|
return;
|
|
|
|
m_SelectedTool = tool.Command;
|
|
ClearPendingEditorOperation();
|
|
foreach (var editorTool in m_EditorTools)
|
|
editorTool.IsSelected = editorTool == tool;
|
|
|
|
RefreshInspector();
|
|
}
|
|
|
|
private void New_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
m_Level = BuildStarterLevel();
|
|
m_CurrentFile = null;
|
|
m_SelectedCell = null;
|
|
ClearPendingEditorOperation();
|
|
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);
|
|
var loaded = LevelSerializer.Deserialize(json);
|
|
m_Level = loaded with { Forecasts = m_Simulation.Forecast(loaded) };
|
|
m_CurrentFile = file;
|
|
m_SelectedCell = null;
|
|
m_SelectedReactorId = m_Level.Reactors.FirstOrDefault()?.ReactorId;
|
|
ClearPendingEditorOperation();
|
|
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 report = new LevelValidator().Validate(m_Level);
|
|
if (!report.IsValid)
|
|
throw new InvalidOperationException(report.Errors[0].Message);
|
|
|
|
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 EndTurn_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
m_Level = m_Simulation.EndTurn(m_Level);
|
|
RefreshInspector();
|
|
LevelCanvas.Invalidate();
|
|
}
|
|
|
|
private void Interact_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
m_Level = m_Simulation.InteractProp(m_Level);
|
|
RefreshInspector();
|
|
LevelCanvas.Invalidate();
|
|
}
|
|
|
|
private void HeatShield_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
m_Level = m_Simulation.ApplyHeatShield(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.IsRightButtonPressed)
|
|
{
|
|
ClearAt(point.Position);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
if (!point.Properties.IsLeftButtonPressed)
|
|
return;
|
|
|
|
_ = LevelCanvas.CapturePointer(e.Pointer);
|
|
m_LeftPointerDown = true;
|
|
m_LeftPointerDownPoint = point.Position;
|
|
m_LastPanPoint = point.Position;
|
|
m_DragExceededClickThreshold = false;
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
if (!m_LeftPointerDown)
|
|
return;
|
|
|
|
var point = e.GetCurrentPoint(LevelCanvas);
|
|
var deltaX = point.Position.X - m_LastPanPoint.X;
|
|
var deltaY = point.Position.Y - m_LastPanPoint.Y;
|
|
m_LastPanPoint = point.Position;
|
|
|
|
var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
|
|
var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
|
|
if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
|
|
m_DragExceededClickThreshold = true;
|
|
|
|
m_PanX += deltaX;
|
|
m_PanY += deltaY;
|
|
ClampPan();
|
|
LevelCanvas.Invalidate();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
var point = e.GetCurrentPoint(LevelCanvas);
|
|
if (m_LeftPointerDown && !m_DragExceededClickThreshold)
|
|
SelectOrPaintAt(point.Position);
|
|
|
|
m_LeftPointerDown = false;
|
|
m_DragExceededClickThreshold = false;
|
|
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
|
|
{
|
|
m_LeftPointerDown = false;
|
|
m_DragExceededClickThreshold = false;
|
|
}
|
|
|
|
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 void SelectOrPaintAt(Point point)
|
|
{
|
|
if (!TryGetGridPosition(point, out var position))
|
|
return;
|
|
|
|
m_SelectedCell = position;
|
|
if (m_SelectedTool.Tool != EEditorTool.Cursor)
|
|
{
|
|
ApplySelectedTool(position);
|
|
}
|
|
else
|
|
{
|
|
SelectReactorFromCell(position);
|
|
}
|
|
|
|
RefreshInspector();
|
|
LevelCanvas.Invalidate();
|
|
}
|
|
|
|
private void ClearAt(Point point)
|
|
{
|
|
if (!TryGetGridPosition(point, out var position))
|
|
return;
|
|
|
|
m_SelectedCell = position;
|
|
ClearPendingEditorOperation();
|
|
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
|
|
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
|
|
Doors = m_Level.Doors.Where(door => door.A != position && door.B != position).ToArray(),
|
|
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
|
|
};
|
|
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);
|
|
DrawUnderground(drawing, layout);
|
|
DrawSurface(drawing, layout);
|
|
DrawDoors(drawing, layout);
|
|
DrawProps(drawing, layout);
|
|
DrawLeaks(drawing, layout);
|
|
DrawRobot(drawing, layout);
|
|
DrawGrid(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 DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
|
|
{
|
|
if (m_TerrainTilemap is null)
|
|
{
|
|
DrawFallbackTerrainTile(drawing, rect, floorMask);
|
|
return;
|
|
}
|
|
|
|
var wallMask = c_AllCorners ^ floorMask;
|
|
drawing.DrawImage(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), 1.0f, CanvasImageInterpolation.HighQualityCubic);
|
|
}
|
|
|
|
private static void DrawFallbackTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
|
|
{
|
|
var color = floorMask == c_AllCorners ? ColorHelper.FromArgb(255, 32, 38, 42) : ColorHelper.FromArgb(255, 41, 47, 52);
|
|
drawing.FillRectangle(rect, color);
|
|
}
|
|
|
|
private static Rect TilemapSourceRect(int wallMask)
|
|
{
|
|
var tilePosition = wallMask switch {
|
|
c_BottomLeftCorner => new GridPosition(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.GetTerrain(position) : ECellTerrain.Wall;
|
|
}
|
|
|
|
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var position in AllPositions())
|
|
{
|
|
var rect = Inset(layout.CellRect(position), 0.18);
|
|
DrawUndergroundCell(drawing, rect, position, ECarrierType.Fuel, c_FuelColor);
|
|
DrawUndergroundCell(drawing, Inset(rect, -0.08), position, ECarrierType.Coolant, c_CoolantColor);
|
|
DrawUndergroundCell(drawing, Inset(rect, -0.16), position, ECarrierType.Electricity, c_ElectricityColor);
|
|
}
|
|
}
|
|
|
|
private void DrawUndergroundCell(CanvasDrawingSession drawing, Rect rect, GridPosition position, ECarrierType carrier, Color color)
|
|
{
|
|
var cell = m_Level.GetUnderground(position, carrier);
|
|
if (!cell.IsPresent)
|
|
return;
|
|
|
|
drawing.DrawRectangle(rect, color, cell.State == EUndergroundState.Leaking ? 4 : 2);
|
|
if (cell.Amount > 0 || cell.Intensity > 0)
|
|
drawing.FillCircle((float)(rect.X + (rect.Width / 2)), (float)(rect.Y + (rect.Height / 2)), (float)Math.Max(2, rect.Width * 0.08), color);
|
|
}
|
|
|
|
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var position in AllPositions().Where(m_Level.IsFloor))
|
|
{
|
|
var surface = m_Level.GetSurface(position);
|
|
var rect = layout.CellRect(position);
|
|
FillHazard(drawing, rect, surface.Fuel, c_FuelColor, 0.08);
|
|
FillHazard(drawing, rect, surface.Coolant, c_CoolantColor, 0.18);
|
|
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28);
|
|
if (surface.Heat > 0)
|
|
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f));
|
|
}
|
|
}
|
|
|
|
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset)
|
|
{
|
|
if (amount <= 0)
|
|
return;
|
|
|
|
var alpha = (byte)Math.Clamp(40 + (amount / Balancing.Current.MaxValue * 130), 40, 170);
|
|
drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
|
|
}
|
|
|
|
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var door in m_Level.Doors)
|
|
{
|
|
var centerA = Center(layout.CellRect(door.A));
|
|
var centerB = Center(layout.CellRect(door.B));
|
|
drawing.DrawLine((float)centerA.X, (float)centerA.Y, (float)centerB.X, (float)centerB.Y, door.State == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, 5);
|
|
}
|
|
}
|
|
|
|
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var position in AllPositions())
|
|
{
|
|
var prop = m_Level.GetProp(position);
|
|
if (prop.Type == EPropType.None)
|
|
continue;
|
|
|
|
var rect = Inset(layout.CellRect(position), 0.18);
|
|
drawing.FillRoundedRectangle(rect, 4, 4, PropColor(prop));
|
|
DrawCenteredText(drawing, PropLabel(prop), rect, Colors.White, Math.Max(10, (float)(layout.CellSize * 0.22)));
|
|
}
|
|
}
|
|
|
|
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var leak in m_Level.Leaks.Where(leak => !leak.Repaired))
|
|
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1));
|
|
}
|
|
|
|
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04));
|
|
}
|
|
|
|
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
|
|
{
|
|
foreach (var position in AllPositions())
|
|
{
|
|
var rect = layout.CellRect(position);
|
|
drawing.DrawRectangle(rect, ColorHelper.FromArgb(90, 91, 104, 115), 1);
|
|
if (m_SelectedCell == position)
|
|
drawing.DrawRectangle(rect, Colors.White, 3);
|
|
}
|
|
}
|
|
|
|
private static void DrawCenteredText(CanvasDrawingSession drawing, string text, Rect rect, Color color, float fontSize)
|
|
{
|
|
using var format = new CanvasTextFormat {
|
|
FontSize = fontSize,
|
|
HorizontalAlignment = CanvasHorizontalAlignment.Center,
|
|
VerticalAlignment = CanvasVerticalAlignment.Center,
|
|
WordWrapping = CanvasWordWrapping.NoWrap
|
|
};
|
|
drawing.DrawText(text, rect, color, format);
|
|
}
|
|
|
|
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;
|
|
m_PanX = ClampAxisPan(m_PanX, cellSize * m_Level.Width, Math.Max(1, LevelCanvas.ActualWidth));
|
|
m_PanY = ClampAxisPan(m_PanY, cellSize * m_Level.Height, Math.Max(1, LevelCanvas.ActualHeight));
|
|
}
|
|
|
|
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 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 RefreshInspector()
|
|
{
|
|
LevelNameText.Text = m_Level.Name;
|
|
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
|
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
|
|
GlobalText.Text = $"Actions: {m_Level.Global.ActionsRemaining}/{Balancing.Current.ActionsPerTurn}\n"
|
|
+ $"All-seeing-eye: {(m_Level.Global.AllSeeingEyeUnlocked ? "unlocked" : "locked")}\n"
|
|
+ $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
|
|
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
|
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {m_Level.Doors.Count}";
|
|
|
|
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
|
|
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
|
WorkflowText.Text = WorkflowInspectionText();
|
|
ReactorBindingText.Text = ReactorBindingInspectionText();
|
|
RuleEventText.Text = RuleEventInspectionText();
|
|
}
|
|
|
|
private void ApplySelectedTool(GridPosition position)
|
|
{
|
|
switch (m_SelectedTool.Tool)
|
|
{
|
|
case EEditorTool.Door:
|
|
ApplyDoorTool(position);
|
|
break;
|
|
case EEditorTool.Leak when m_SelectedTool.Carrier == ECarrierType.Electricity:
|
|
ApplyElectricityLeakTool(position);
|
|
break;
|
|
default:
|
|
ClearPendingEditorOperation();
|
|
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
|
SelectReactorFromCell(position);
|
|
RefreshForecasts();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ApplyDoorTool(GridPosition position)
|
|
{
|
|
if (!m_Level.IsFloor(position))
|
|
return;
|
|
|
|
if (m_PendingDoorCell is not { } pending)
|
|
{
|
|
m_PendingDoorCell = position;
|
|
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
|
RefreshForecasts();
|
|
return;
|
|
}
|
|
|
|
m_Level = LevelEditor.SetDoorEdge(m_Level, pending, position);
|
|
m_PendingDoorCell = null;
|
|
RefreshForecasts();
|
|
}
|
|
|
|
private void ApplyElectricityLeakTool(GridPosition position)
|
|
{
|
|
if (m_Level.GetTerrain(position) == ECellTerrain.Wall)
|
|
{
|
|
m_PendingElectricityLeakCell = position;
|
|
return;
|
|
}
|
|
|
|
if (m_PendingElectricityLeakCell is { } undergroundPosition)
|
|
{
|
|
m_Level = LevelEditor.SetLeak(m_Level, undergroundPosition, position, ECarrierType.Electricity);
|
|
m_PendingElectricityLeakCell = null;
|
|
RefreshForecasts();
|
|
return;
|
|
}
|
|
|
|
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
|
RefreshForecasts();
|
|
}
|
|
|
|
private void BindFuel_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
BindSelectedConsumer(ECarrierType.Fuel);
|
|
}
|
|
|
|
private void BindCoolant_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
BindSelectedConsumer(ECarrierType.Coolant);
|
|
}
|
|
|
|
private void BindElectricity_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
BindSelectedConsumer(ECarrierType.Electricity);
|
|
}
|
|
|
|
private void AddWarningRule_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var turn = m_Level.Global.Turn + 1;
|
|
m_Level = LevelEditor.AddRuleEvent(m_Level, new() {
|
|
Phase = ERuleEventPhase.EndOfTurn,
|
|
ForecastText = $"Warning on turn {turn}",
|
|
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }],
|
|
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = $"Authored warning on turn {turn}" }]
|
|
});
|
|
RefreshForecasts();
|
|
RefreshInspector();
|
|
}
|
|
|
|
private void AddLeakRule_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (m_SelectedCell is not { } position || !TryGetAuthoredLeakCarrier(position, out var carrier))
|
|
return;
|
|
|
|
var turn = m_Level.Global.Turn + 1;
|
|
m_Level = LevelEditor.AddRuleEvent(m_Level, new() {
|
|
Phase = ERuleEventPhase.EndOfTurn,
|
|
ForecastText = $"{carrier} leak starts on turn {turn}",
|
|
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }],
|
|
Effects = [new() { Kind = ERuleEffectKind.StartLeak, Position = position, AccessPosition = position, Carrier = carrier }]
|
|
});
|
|
RefreshForecasts();
|
|
RefreshInspector();
|
|
}
|
|
|
|
private void RemoveLastRule_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var ruleEvent = m_Level.RuleEvents.LastOrDefault();
|
|
if (ruleEvent is null)
|
|
return;
|
|
|
|
m_Level = LevelEditor.RemoveRuleEvent(m_Level, ruleEvent.Id);
|
|
RefreshForecasts();
|
|
RefreshInspector();
|
|
}
|
|
|
|
private void BindSelectedConsumer(ECarrierType carrier)
|
|
{
|
|
if (m_SelectedCell is not { } position || m_SelectedReactorId is not { } reactorId)
|
|
return;
|
|
|
|
m_Level = LevelEditor.BindReactorConsumer(m_Level, reactorId, carrier, position);
|
|
RefreshForecasts();
|
|
RefreshInspector();
|
|
}
|
|
|
|
private string CellInspectionText(GridPosition position)
|
|
{
|
|
var prop = m_Level.GetProp(position);
|
|
var surface = m_Level.GetSurface(position);
|
|
var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
|
|
var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
|
|
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
|
return $"Position: {position.X},{position.Y}\n"
|
|
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
|
+ $"Prop: {prop.Type} {prop.Carrier} {prop.SwitchState} {prop.ServiceState}\n"
|
|
+ $"Fuel: {UndergroundText(fuel)}\n"
|
|
+ $"Coolant: {UndergroundText(coolant)}\n"
|
|
+ $"Electricity: {UndergroundText(electricity)}\n"
|
|
+ $"Surface fuel/coolant/electricity/heat: {Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}\n"
|
|
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
|
|
}
|
|
|
|
private string WorkflowInspectionText()
|
|
{
|
|
if (m_PendingDoorCell is { } door)
|
|
return $"Door edge: select an adjacent floor for {door.X},{door.Y}.";
|
|
|
|
if (m_PendingElectricityLeakCell is { } leak)
|
|
return $"Electricity leak face: select adjacent floor access for {leak.X},{leak.Y}.";
|
|
|
|
return "No pending editor operation.";
|
|
}
|
|
|
|
private string ReactorBindingInspectionText()
|
|
{
|
|
var reactor = m_SelectedReactorId is { } reactorId ? m_Level.Reactors.FirstOrDefault(candidate => candidate.ReactorId == reactorId) : null;
|
|
if (reactor is null)
|
|
return "Select or place a reactor control.";
|
|
|
|
return $"Reactor {reactor.ReactorId}\n"
|
|
+ $"Fuel: {PositionText(reactor.FuelConsumerPosition)}\n"
|
|
+ $"Coolant: {PositionText(reactor.CoolantConsumerPosition)}\n"
|
|
+ $"Electric: {PositionText(reactor.ElectricityConsumerPosition)}";
|
|
}
|
|
|
|
private string RuleEventInspectionText()
|
|
{
|
|
if (m_Level.RuleEvents.Count == 0)
|
|
return "No authored rule events.";
|
|
|
|
var last = m_Level.RuleEvents[^1];
|
|
return $"{m_Level.RuleEvents.Count} authored. Last: {last.Id} ({last.Phase}).";
|
|
}
|
|
|
|
private static string PositionText(GridPosition position)
|
|
{
|
|
return $"{position.X},{position.Y}";
|
|
}
|
|
|
|
private static string UndergroundText(UndergroundCell cell)
|
|
{
|
|
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}";
|
|
}
|
|
|
|
private static string Format(float value)
|
|
{
|
|
return value.ToString("0.0", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static LevelState BuildStarterLevel()
|
|
{
|
|
var level = LevelState.Create("Cooling Sector B", 16, 12);
|
|
level = AddNetwork(level, ECarrierType.Fuel, new(2, 3), new(5, 3));
|
|
level = AddNetwork(level, ECarrierType.Coolant, new(2, 5), new(5, 5));
|
|
level = AddNetwork(level, ECarrierType.Electricity, new(2, 7), new(5, 7));
|
|
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
|
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
|
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
|
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
|
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
|
level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
|
level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal });
|
|
level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield });
|
|
level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
|
|
Leaks = [new() { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
|
|
Doors = [new() { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }],
|
|
Robot = new() { Position = new(10, 5) },
|
|
Reactors = [
|
|
new() {
|
|
ReactorId = 1,
|
|
ControlPosition = new(10, 5),
|
|
FuelConsumerPosition = new(5, 3),
|
|
CoolantConsumerPosition = new(5, 5),
|
|
ElectricityConsumerPosition = new(5, 7)
|
|
}
|
|
]
|
|
};
|
|
|
|
return level with { Forecasts = new SimulationEngine().Forecast(level) };
|
|
}
|
|
|
|
private static LevelState AddNetwork(LevelState level, ECarrierType carrier, GridPosition start, GridPosition end)
|
|
{
|
|
var minX = Math.Min(start.X, end.X);
|
|
var maxX = Math.Max(start.X, end.X);
|
|
var minY = Math.Min(start.Y, end.Y);
|
|
var maxY = Math.Max(start.Y, end.Y);
|
|
for (var y = minY; y <= maxY; y++)
|
|
{
|
|
for (var x = minX; x <= maxX; x++)
|
|
level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
private IEnumerable<GridPosition> AllPositions()
|
|
{
|
|
return AllPositions(m_Level);
|
|
}
|
|
|
|
private static IEnumerable<GridPosition> AllPositions(LevelState level)
|
|
{
|
|
for (var y = 0; y < level.Height; y++)
|
|
{
|
|
for (var x = 0; x < level.Width; x++)
|
|
yield return new(x, y);
|
|
}
|
|
}
|
|
|
|
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 static Point Center(Rect rect)
|
|
{
|
|
return new(rect.X + (rect.Width / 2), rect.Y + (rect.Height / 2));
|
|
}
|
|
|
|
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 Color PropColor(PropState prop)
|
|
{
|
|
return prop.Type switch {
|
|
EPropType.Flow => CarrierColor(prop.Carrier),
|
|
EPropType.Consumer => ColorHelper.FromArgb(255, 93, 123, 170),
|
|
EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178),
|
|
EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55),
|
|
EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156),
|
|
EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
|
|
EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73),
|
|
_ => Colors.Gray
|
|
};
|
|
}
|
|
|
|
private static Color CarrierColor(ECarrierType carrier)
|
|
{
|
|
return carrier switch {
|
|
ECarrierType.Fuel => c_FuelColor,
|
|
ECarrierType.Coolant => c_CoolantColor,
|
|
ECarrierType.Electricity => c_ElectricityColor,
|
|
_ => Colors.White
|
|
};
|
|
}
|
|
|
|
private static string PropLabel(PropState prop)
|
|
{
|
|
return prop.Type switch {
|
|
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
|
|
EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON",
|
|
EPropType.Junction => $"J {prop.JunctionMode}",
|
|
EPropType.Door => "DOOR",
|
|
EPropType.AllSeeingEyeTerminal => "EYE",
|
|
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
|
|
EPropType.ReactorControl => "REACT",
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
private static string CarrierShort(ECarrierType carrier)
|
|
{
|
|
return carrier switch {
|
|
ECarrierType.Fuel => "F",
|
|
ECarrierType.Coolant => "C",
|
|
ECarrierType.Electricity => "E",
|
|
_ => "?"
|
|
};
|
|
}
|
|
|
|
private static string RemedyShort(ERemedyType remedy)
|
|
{
|
|
return remedy switch {
|
|
ERemedyType.FuelNeutralizer => "F REM",
|
|
ERemedyType.CoolantNeutralizer => "C REM",
|
|
ERemedyType.ElectricityNeutralizer => "E REM",
|
|
ERemedyType.HeatShield => "H SHD",
|
|
_ => "REM"
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyList<EditorToolViewModel> BuildEditorTools()
|
|
{
|
|
EditorToolViewModel Tool(EEditorTool tool, string label)
|
|
{
|
|
return new(new() { Tool = tool }, label) { IsSelected = tool == EEditorTool.Cursor };
|
|
}
|
|
|
|
EditorToolViewModel CarrierTool(EEditorTool tool, ECarrierType carrier, string label)
|
|
{
|
|
return new(new() { Tool = tool, Carrier = carrier }, label);
|
|
}
|
|
|
|
EditorToolViewModel RemedyTool(ERemedyType remedy, string label)
|
|
{
|
|
return new(new() { Tool = EEditorTool.RemedySupply, RemedyType = remedy }, label);
|
|
}
|
|
|
|
return [
|
|
Tool(EEditorTool.Cursor, "Cursor"),
|
|
Tool(EEditorTool.Floor, "Floor"),
|
|
Tool(EEditorTool.Wall, "Wall"),
|
|
CarrierTool(EEditorTool.Underground, ECarrierType.Fuel, "Fuel Net"),
|
|
CarrierTool(EEditorTool.Underground, ECarrierType.Coolant, "Coolant Net"),
|
|
CarrierTool(EEditorTool.Underground, ECarrierType.Electricity, "Electric Net"),
|
|
CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"),
|
|
CarrierTool(EEditorTool.Flow, ECarrierType.Coolant, "Coolant Source"),
|
|
CarrierTool(EEditorTool.Flow, ECarrierType.Electricity, "Electric Source"),
|
|
CarrierTool(EEditorTool.Consumer, ECarrierType.Fuel, "Fuel Consumer"),
|
|
CarrierTool(EEditorTool.Consumer, ECarrierType.Coolant, "Coolant Consumer"),
|
|
CarrierTool(EEditorTool.Consumer, ECarrierType.Electricity, "Electric Consumer"),
|
|
Tool(EEditorTool.Junction, "Junction"),
|
|
Tool(EEditorTool.Door, "Door"),
|
|
Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"),
|
|
RemedyTool(ERemedyType.FuelNeutralizer, "Fuel Remedy"),
|
|
RemedyTool(ERemedyType.CoolantNeutralizer, "Coolant Remedy"),
|
|
RemedyTool(ERemedyType.ElectricityNeutralizer, "Electric Remedy"),
|
|
RemedyTool(ERemedyType.HeatShield, "Heat Shield"),
|
|
Tool(EEditorTool.ReactorControl, "Reactor"),
|
|
CarrierTool(EEditorTool.Leak, ECarrierType.Fuel, "Fuel Leak"),
|
|
CarrierTool(EEditorTool.Leak, ECarrierType.Coolant, "Coolant Leak"),
|
|
CarrierTool(EEditorTool.Leak, ECarrierType.Electricity, "Electric Leak"),
|
|
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Fuel, "Fuel Hazard"),
|
|
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Coolant, "Coolant Hazard"),
|
|
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Electricity, "Electric Hazard"),
|
|
Tool(EEditorTool.Heat, "Heat"),
|
|
Tool(EEditorTool.Robot, "Robot")
|
|
];
|
|
}
|
|
|
|
private void SelectReactorFromCell(GridPosition position)
|
|
{
|
|
var prop = m_Level.GetProp(position);
|
|
if (prop is { Type: EPropType.ReactorControl, ReactorId: > 0 })
|
|
m_SelectedReactorId = prop.ReactorId;
|
|
}
|
|
|
|
private bool TryGetAuthoredLeakCarrier(GridPosition position, out ECarrierType carrier)
|
|
{
|
|
if (!m_Level.IsFloor(position))
|
|
{
|
|
carrier = default;
|
|
return false;
|
|
}
|
|
|
|
foreach (var candidate in Enum.GetValues<ECarrierType>())
|
|
{
|
|
if (m_Level.GetUnderground(position, candidate).IsPresent)
|
|
{
|
|
carrier = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
carrier = default;
|
|
return false;
|
|
}
|
|
|
|
private void RefreshForecasts()
|
|
{
|
|
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
|
}
|
|
|
|
private void ClearPendingEditorOperation()
|
|
{
|
|
m_PendingDoorCell = null;
|
|
m_PendingElectricityLeakCell = null;
|
|
}
|
|
|
|
private const double c_MinZoom = 0.5;
|
|
private const double c_MaxZoom = 4;
|
|
private const double c_ZoomStep = 1.15;
|
|
private const double c_ClickPixelThreshold = 10;
|
|
private const int c_TilemapTileSize = 512;
|
|
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 static readonly Color c_FuelColor = ColorHelper.FromArgb(255, 220, 170, 68);
|
|
private static readonly Color c_CoolantColor = ColorHelper.FromArgb(255, 57, 174, 196);
|
|
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
|
|
private readonly IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
|
|
|
private readonly SimulationEngine m_Simulation = new();
|
|
private StorageFile? m_CurrentFile;
|
|
private bool m_DragExceededClickThreshold;
|
|
private CanvasBitmap? m_HeatSprite;
|
|
private Point m_LastPanPoint;
|
|
private CanvasBitmap? m_LeakSprite;
|
|
private bool m_LeftPointerDown;
|
|
private Point m_LeftPointerDownPoint;
|
|
private LevelState m_Level;
|
|
private double m_PanX;
|
|
private double m_PanY;
|
|
private CanvasBitmap? m_RobotSprite;
|
|
private GridPosition? m_SelectedCell;
|
|
private int? m_SelectedReactorId = 1;
|
|
private GridPosition? m_PendingDoorCell;
|
|
private GridPosition? m_PendingElectricityLeakCell;
|
|
private EditorToolCommand m_SelectedTool = new() { Tool = EEditorTool.Cursor };
|
|
private CanvasBitmap? m_TerrainTilemap;
|
|
private double m_Zoom = 1;
|
|
} |