Files
zfxaction26_2/src/ReactorMaintenance.Win2D/MainWindow.xaml.cs

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;
}