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

1415 lines
55 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.Controls;
using Microsoft.UI.Xaml.Input;
using ReactorMaintenance.Simulation;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.System;
using Windows.UI;
using Windows.UI.Popups;
using WinRT.Interop;
namespace ReactorMaintenance.Win2D;
public sealed partial class MainWindow
{
private enum EEditorLayer
{
Surface,
Electricity,
Fuel,
Water
}
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 record InspectorItemViewModel(string Label, string Value);
private sealed record NetworkInspectionViewModel(string Carrier, string State, string Amount, string Intensity, string Integrity);
private sealed class EditorToolViewModel(EditorToolCommand command, string label) : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
public EditorToolCommand Command { get; } = command;
public string Label { get; } = label;
public bool IsSelected
{
get => m_IsSelected;
set
{
if (m_IsSelected == value)
return;
m_IsSelected = value;
OnPropertyChanged();
}
}
private bool m_IsSelected;
}
public MainWindow()
{
InitializeComponent();
m_Level = BuildStarterLevel();
m_EditorTools = BuildEditorTools();
m_SimulationTimer.Interval = TimeSpan.FromSeconds(1.0 / c_SimulationStepsPerSecond);
m_SimulationTimer.Tick += SimulationTimer_Tick;
LayerPicker.ItemsSource = Enum.GetValues<EEditorLayer>();
LayerPicker.SelectedItem = EEditorLayer.Surface;
RefreshToolPicker();
RefreshInspector();
UpdatePlayPauseButton();
}
private void LevelCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
{
args.TrackAsyncAction(LoadImagesAsync(sender).AsAsyncAction());
}
private async Task LoadImagesAsync(CanvasControl sender)
{
await m_Images.LoadAsync(sender);
m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
m_LeakSprite = m_Images.Get("leak");
m_HeatSprite = m_Images.Get("heat");
}
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;
SelectTool(tool);
RefreshInspector();
}
private void LayerPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LayerPicker.SelectedItem is EEditorLayer layer)
m_ActiveLayer = layer;
if (!IsToolAvailableOnActiveLayer(m_SelectedTool))
m_SelectedTool = new() { Tool = EEditorTool.Cursor };
RefreshToolPicker();
RefreshInspector();
LevelCanvas.Invalidate();
}
private void SelectTool(EditorToolViewModel tool)
{
m_SelectedTool = tool.Command;
foreach (var editorTool in m_EditorTools)
editorTool.IsSelected = editorTool == tool;
}
private void RefreshToolPicker()
{
var visibleTools = m_EditorTools.Where(tool => IsToolAvailableOnActiveLayer(tool.Command)).ToArray();
foreach (var tool in m_EditorTools)
tool.IsSelected = tool.Command == m_SelectedTool;
ToolPicker.ItemsSource = visibleTools;
}
private void New_Click(object sender, RoutedEventArgs e)
{
StopSimulationTimer();
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;
StopSimulationTimer();
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;
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 DebugPulse_Click(object sender, RoutedEventArgs e)
{
RunDebugPulse();
}
private void PlayPause_Click(object sender, RoutedEventArgs e)
{
if (m_SimulationTimer.IsEnabled)
StopSimulationTimer();
else
StartSimulationTimer();
}
private void SimulationTimer_Tick(object? sender, object e)
{
RunDebugPulse();
}
private void StartSimulationTimer()
{
m_SimulationTimer.Start();
UpdatePlayPauseButton();
}
private void StopSimulationTimer()
{
m_SimulationTimer.Stop();
UpdatePlayPauseButton();
}
private void UpdatePlayPauseButton()
{
var isPlaying = m_SimulationTimer.IsEnabled;
PlayPauseButton.Icon = new SymbolIcon(isPlaying ? Symbol.Pause : Symbol.Play);
PlayPauseButton.Label = isPlaying ? "Pause" : "Play";
}
private void RunDebugPulse()
{
m_Level = m_Simulation.AdvancePulseForDebug(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;
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
m_CursorDragStartCell = null;
m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_InvalidDragCell = null;
m_EditorFeedback = string.Empty;
m_LastPaintedCell = null;
if (!m_IsPanning && m_SelectedTool.Tool == EEditorTool.Cursor && TryGetGridPosition(point.Position, out var position))
m_CursorDragStartCell = position;
else if (!m_IsPanning && IsDragPaintTool(m_SelectedTool.Tool) && TryGetGridPosition(point.Position, out position))
{
m_LastPaintedCell = position;
PaintDraggedCell(position);
}
e.Handled = true;
}
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
UpdateHoverCell(point.Position);
if (!m_LeftPointerDown)
return;
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;
if (m_IsPanning)
{
m_PanX += deltaX;
m_PanY += deltaY;
ClampPan();
LevelCanvas.Invalidate();
}
else if (IsDragPaintTool(m_SelectedTool.Tool) && TryGetGridPosition(point.Position, out var paintPosition) && paintPosition != m_LastPaintedCell)
{
m_LastPaintedCell = paintPosition;
PaintDraggedCell(paintPosition);
}
else if (m_CursorDragStartCell is not null && m_DragExceededClickThreshold && TryGetGridPosition(point.Position, out var destination))
{
UpdateCursorDragPreview(destination);
LevelCanvas.Invalidate();
}
e.Handled = true;
}
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(LevelCanvas);
if (m_LeftPointerDown)
{
if (m_IsPanning)
{
LevelCanvas.Invalidate();
}
else if (m_CursorDragStartCell is { } source && m_DragExceededClickThreshold && !m_CursorDragStartRejected && TryGetGridPosition(point.Position, out var destination))
{
var result = LevelEditor.TryMoveOccupant(m_Level, source, destination);
m_Level = result.Level;
m_SelectedCell = result.Success ? destination : source;
m_InvalidDragCell = result.Success ? null : destination;
m_EditorFeedback = result.Reason;
if (result.Success)
{
RefreshForecasts();
}
RefreshInspector();
LevelCanvas.Invalidate();
}
else if (!m_DragExceededClickThreshold && !m_CursorDragStartRejected && !IsDragPaintTool(m_SelectedTool.Tool))
{
SelectOrPaintAt(point.Position);
}
}
var clearRejectedDragFeedback = m_CursorDragStartRejected;
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_LastPaintedCell = null;
m_DragExceededClickThreshold = false;
if (clearRejectedDragFeedback)
{
ClearDragFeedback();
RefreshInspector();
LevelCanvas.Invalidate();
}
LevelCanvas.ReleasePointerCapture(e.Pointer);
e.Handled = true;
}
private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
{
m_LeftPointerDown = false;
m_IsPanning = false;
m_CursorDragStartCell = null;
m_CursorDragStartRejected = false;
m_DragPreviewDestination = null;
m_LastPaintedCell = null;
m_DragExceededClickThreshold = false;
ClearDragFeedback();
RefreshInspector();
LevelCanvas.Invalidate();
}
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);
}
RefreshInspector();
LevelCanvas.Invalidate();
}
private void PaintDraggedCell(GridPosition position)
{
m_SelectedCell = position;
ApplySelectedTool(position);
RefreshInspector();
LevelCanvas.Invalidate();
}
private void UpdateHoverCell(Point point)
{
var nextHover = TryGetGridPosition(point, out var position) ? position : null;
if (nextHover == HoverCell)
return;
HoverCell = nextHover;
LevelCanvas.Invalidate();
RefreshInspector();
}
private static bool IsDragPaintTool(EEditorTool tool)
{
return tool is EEditorTool.Floor or EEditorTool.Wall or EEditorTool.Underground;
}
private void UpdateCursorDragPreview(GridPosition destination)
{
if (m_CursorDragStartCell is not { } source)
return;
if (m_CursorDragStartRejected)
return;
if (!HasMovableAt(source))
{
m_CursorDragStartRejected = true;
m_DragPreviewDestination = null;
m_InvalidDragCell = source;
m_EditorFeedback = "No movable robot, prop, source, or leak starts here.";
RefreshInspector();
return;
}
m_DragPreviewDestination = destination;
m_InvalidDragCell = null;
m_EditorFeedback = string.Empty;
RefreshInspector();
}
private void ClearDragFeedback()
{
m_InvalidDragCell = null;
m_EditorFeedback = string.Empty;
}
private void ClearAt(Point point)
{
if (!TryGetGridPosition(point, out var position))
return;
m_SelectedCell = position;
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != 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, SurfaceOpacity());
DrawUnderground(drawing, layout);
DrawSurface(drawing, layout, SurfaceOpacity());
DrawDoors(drawing, layout, SurfaceOpacity());
DrawProps(drawing, layout, SurfaceOpacity());
DrawLeaks(drawing, layout, SurfaceOpacity());
DrawRobot(drawing, layout, SurfaceOpacity());
DrawGrid(drawing, layout);
DrawEditorOverlays(drawing, layout);
}
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
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), opacity);
}
}
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask, float opacity)
{
if (m_TerrainTilemap is null)
{
DrawFallbackTerrainTile(drawing, rect, floorMask, opacity);
return;
}
var wallMask = c_AllCorners ^ floorMask;
drawing.DrawImage(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), opacity, CanvasImageInterpolation.HighQualityCubic);
}
private static void DrawFallbackTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask, float opacity)
{
var alpha = (byte)Math.Clamp(opacity * 255, 0, 255);
var color = floorMask == c_AllCorners ? ColorHelper.FromArgb(alpha, 32, 38, 42) : ColorHelper.FromArgb(alpha, 41, 47, 52);
drawing.FillRectangle(rect, color);
}
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.GetTerrain(position) : ECellTerrain.Wall;
}
private bool IsWall(GridPosition position)
{
return m_Level.InBounds(position) && m_Level.GetTerrain(position) == ECellTerrain.Wall;
}
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
{
foreach (var carrier in OrderedUndergroundLayers())
DrawUndergroundLayer(drawing, layout, carrier, CarrierColor(carrier), UndergroundOpacity(carrier));
}
private IEnumerable<ECarrierType> OrderedUndergroundLayers()
{
var carriers = new[] { ECarrierType.Fuel, ECarrierType.Water, ECarrierType.Electricity };
var activeCarrier = LayerCarrier(m_ActiveLayer);
return activeCarrier is null ? carriers : carriers.Where(carrier => carrier != activeCarrier).Append(activeCarrier.Value);
}
private void DrawUndergroundLayer(CanvasDrawingSession drawing, CanvasLayout layout, ECarrierType carrier, Color color, float opacity)
{
var layerColor = WithOpacity(color, opacity);
var lineWidth = (float)Math.Max(4, layout.CellSize * 0.16);
var cellDotRadius = (float)Math.Max(2, layout.CellSize * 0.08);
var sourceDotRadius = (float)Math.Max(5, layout.CellSize * 0.22);
foreach (var position in AllPositions())
{
var cell = m_Level.GetUnderground(position, carrier);
if (!cell.IsPresent)
continue;
var center = Center(layout.CellRect(position));
DrawNetworkConnection(drawing, layout, carrier, position, new(position.X + 1, position.Y), layerColor, lineWidth);
DrawNetworkConnection(drawing, layout, carrier, position, new(position.X, position.Y + 1), layerColor, lineWidth);
drawing.FillCircle((float)center.X, (float)center.Y, cellDotRadius, layerColor);
if (cell.State == EUndergroundState.Leaking)
drawing.DrawCircle((float)center.X, (float)center.Y, sourceDotRadius * 0.7f, Colors.OrangeRed, Math.Max(2, lineWidth * 0.25f));
var prop = m_Level.GetProp(position);
if (prop is { Type: EPropType.Flow } && prop.Carrier == carrier)
drawing.FillCircle((float)center.X, (float)center.Y, sourceDotRadius, layerColor);
}
}
private void DrawNetworkConnection(
CanvasDrawingSession drawing,
CanvasLayout layout,
ECarrierType carrier,
GridPosition position,
GridPosition neighbor,
Color color,
float lineWidth)
{
if (!m_Level.InBounds(neighbor) || !m_Level.GetUnderground(neighbor, carrier).IsPresent)
return;
var from = Center(layout.CellRect(position));
var to = Center(layout.CellRect(neighbor));
drawing.DrawLine((float)from.X, (float)from.Y, (float)to.X, (float)to.Y, color, lineWidth);
}
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
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, opacity, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
FillHazard(drawing, rect, surface.Water, c_WaterColor, 0.18, opacity, Balancing.Current.WaterCaution, Balancing.Current.WaterCritical);
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28, opacity, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
var heatOpacity = SurfaceOverlayOpacity(surface.Heat, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
if (heatOpacity > 0)
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), heatOpacity * opacity);
}
}
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset, float opacity, float caution, float critical)
{
var overlayOpacity = SurfaceOverlayOpacity(amount, caution, critical);
if (overlayOpacity <= 0)
return;
var alpha = (byte)Math.Clamp(170 * overlayOpacity * opacity, 0, 170);
drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
}
private static float SurfaceOverlayOpacity(float amount, float caution, float critical)
{
if (amount < caution)
return 0;
if (amount >= critical)
return 0.9f;
var cautionRange = Math.Max(0.001f, critical - caution);
var t = (amount - caution) / cautionRange;
return 0.3f + (t * 0.35f);
}
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var position in AllPositions())
{
var prop = m_Level.GetProp(position);
if (prop.Type != EPropType.Door)
continue;
var rect = layout.CellRect(position);
var center = Center(rect);
var color = WithOpacity(prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, opacity);
if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1)))
drawing.DrawLine((float)center.X, (float)rect.Top, (float)center.X, (float)rect.Bottom, color, 5);
else if (IsWall(new(position.X - 1, position.Y)) && IsWall(new(position.X + 1, position.Y)))
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
else
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
}
}
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
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);
DrawBadge(drawing, rect, ImageKey(prop), PropColor(prop), PropLabel(prop), opacity);
}
}
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
foreach (var leak in m_Level.Leaks.Where(leak => !leak.Repaired))
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1), opacity);
}
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
{
DrawBadge(drawing, Inset(layout.CellRect(m_Level.Robot.Position), 0.04), "robot", Colors.White, "BOT", opacity);
}
private void DrawEditorOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
{
if (m_DragPreviewDestination is { } destination)
{
var rect = Inset(layout.CellRect(destination), 0.12);
var key = m_CursorDragStartCell is { } source ? MovableImageKey(source) : null;
DrawBadge(drawing, rect, key, ColorHelper.FromArgb(255, 104, 168, 222), "MOVE", 0.55f);
}
if (m_InvalidDragCell is { } invalidCell)
{
var rect = layout.CellRect(invalidCell);
drawing.DrawRectangle(rect, Colors.OrangeRed, 4);
DrawStatusBadge(drawing, rect, m_EditorFeedback);
}
if (HoverCell is { } hover && m_SelectedTool.Tool != EEditorTool.Cursor)
{
var rect = layout.CellRect(hover);
var size = Math.Max(26, rect.Width * 0.42);
var badgeRect = new Rect(rect.Right - size - 4, rect.Top + 4, size, size);
DrawBadge(drawing, badgeRect, ImageKey(m_SelectedTool), ToolColor(m_SelectedTool), ToolShortLabel(m_SelectedTool), 0.9f);
}
}
private static void DrawStatusBadge(CanvasDrawingSession drawing, Rect cellRect, string text)
{
if (string.IsNullOrWhiteSpace(text))
return;
var rect = new Rect(cellRect.Left, cellRect.Bottom + 4, Math.Max(cellRect.Width * 2.6, 160), 28);
drawing.FillRoundedRectangle(rect, 4, 4, ColorHelper.FromArgb(230, 96, 32, 32));
DrawCenteredText(drawing, text, rect, Colors.White, 11);
}
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)Math.Floor((point.X - layout.OriginX) / layout.CellSize);
var y = (int)Math.Floor((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.Pulse.ToString(CultureInfo.InvariantCulture);
StatusText.Text = string.IsNullOrWhiteSpace(m_EditorFeedback)
? $"{m_Level.Global.LevelState}: {m_Level.Global.Status}"
: $"{m_Level.Global.LevelState}: {m_EditorFeedback}";
InventoryGrid.ItemsSource = new[] {
new InspectorItemViewModel("Fuel", $"{m_Level.Robot.FuelNeutralizers}"),
new InspectorItemViewModel("Water", $"{m_Level.Robot.WaterNeutralizers}"),
new InspectorItemViewModel("Electricity", $"{m_Level.Robot.ElectricityNeutralizers}"),
new InspectorItemViewModel("Heat", $"{m_Level.Robot.HeatShields} ({m_Level.Robot.HeatImmunitySteps.ToString(CultureInfo.InvariantCulture)} steps)")
};
RequiredGrid.ItemsSource = new[] {
new InspectorItemViewModel("Fuel", m_Level.RequiredFuelConsumers.ToString(CultureInfo.InvariantCulture)),
new InspectorItemViewModel("Water", m_Level.RequiredWaterConsumers.ToString(CultureInfo.InvariantCulture)),
new InspectorItemViewModel("Electric", m_Level.RequiredElectricityConsumers.ToString(CultureInfo.InvariantCulture))
};
if (m_SelectedCell is { } position && m_Level.InBounds(position))
{
SelectedCellText.Text = $"Selected cell: {position.X}, {position.Y}";
TerrainText.Text = m_Level.GetTerrain(position).ToString();
var prop = m_Level.GetProp(position);
PropText.Text = prop.Type != EPropType.None ? $"{prop.Type} {prop.SwitchState}" : "(none)";
ServicesGrid.ItemsSource = new[] {
new InspectorItemViewModel("Fuel", prop.FuelServiceState.ToString()),
new InspectorItemViewModel("Water", prop.WaterServiceState.ToString()),
new InspectorItemViewModel("Electricity", prop.ElectricityServiceState.ToString())
};
var surface = m_Level.GetSurface(position);
ConsumersGrid.ItemsSource = new[] {
new InspectorItemViewModel("Fuel", surface.FuelBlockTurns.ToString()),
new InspectorItemViewModel("Water", surface.WaterBlockTurns.ToString()),
new InspectorItemViewModel("Electricity", surface.ElectricityBlockTurns.ToString())
};
LeaksGrid.ItemsSource = m_Level.Leaks.Select(leak => new InspectorItemViewModel(leak.Carrier.ToString(), $"{leak.UndergroundPosition.X}, {leak.UndergroundPosition.Y}"));
SurfaceGrid.ItemsSource = SurfaceInspectionItems(position);
NetworkGrid.ItemsSource = NetworkInspectionItems(position);
}
else
{
SelectedCellText.Text = "Selected cell: (none)";
TerrainText.Text = string.Empty;
PropText.Text = string.Empty;
ServicesGrid.ItemsSource = null;
ConsumersGrid.ItemsSource = null;
LeaksGrid.ItemsSource = null;
SurfaceGrid.ItemsSource = null;
NetworkGrid.ItemsSource = null;
}
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
}
private void ApplySelectedTool(GridPosition position)
{
switch (m_SelectedTool.Tool)
{
case EEditorTool.Door:
ApplyDoorTool(position);
break;
case EEditorTool.Leak when m_SelectedTool.Carrier == ECarrierType.Electricity:
m_Level = LevelEditor.CycleElectricityLeakAccess(m_Level, position);
RefreshForecasts();
break;
default:
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
RefreshForecasts();
break;
}
}
private void ApplyDoorTool(GridPosition position)
{
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
RefreshForecasts();
}
private InspectorItemViewModel[] SurfaceInspectionItems(GridPosition position)
{
var surface = m_Level.GetSurface(position);
return [
new("Fuel", Format(surface.Fuel)),
new("Water", Format(surface.Water)),
new("Electric", Format(surface.Electricity)),
new("Heat", Format(surface.Heat))
];
}
private NetworkInspectionViewModel[] NetworkInspectionItems(GridPosition position)
{
return [
NetworkInspectionItem("Fuel", m_Level.GetUnderground(position, ECarrierType.Fuel)),
NetworkInspectionItem("Water", m_Level.GetUnderground(position, ECarrierType.Water)),
NetworkInspectionItem("Electric", m_Level.GetUnderground(position, ECarrierType.Electricity))
];
}
private static NetworkInspectionViewModel NetworkInspectionItem(string carrier, UndergroundCell cell)
{
return new(carrier, cell.State.ToString(), Format(cell.Amount), Format(cell.Intensity), cell.StructuralIntegrity.ToString(CultureInfo.InvariantCulture));
}
private string SelectedCellTitle(GridPosition position)
{
var prop = m_Level.GetProp(position);
if (prop.Type != EPropType.None)
return $"{prop.Type} {prop.SwitchState}";
if (m_Level.Robot.Position == position)
return "Robot";
var leak = m_Level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == position || leak.UndergroundPosition == position));
if (leak is not null)
return $"{leak.Carrier} Leak";
var surface = m_Level.GetSurface(position);
if (surface.Fuel > 0 || surface.Water > 0 || surface.Electricity > 0)
return "Surface Hazard";
if (surface.Heat > 0)
return "Heat";
return m_Level.GetTerrain(position).ToString();
}
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.Water, 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.Water });
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer });
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer });
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer });
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.Water, new() { State = EUndergroundState.Leaking }) with {
Leaks = [new() { Carrier = ECarrierType.Water, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
Robot = new() { Position = new(10, 5) },
Reactors = [
new() {
ReactorId = 1,
ControlPosition = new(10, 5)
}
]
};
level = level.SetTerrain(new(8, 4), ECellTerrain.Wall);
level = level.SetTerrain(new(8, 6), ECellTerrain.Wall);
level = level.SetProp(new(8, 5), new() { Type = EPropType.Door });
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 void DrawBadge(CanvasDrawingSession drawing, Rect rect, string? imageKey, Color fallbackColor, string fallbackLabel, float opacity)
{
var image = imageKey is null ? null : m_Images.Get(imageKey);
if (image is not null)
{
DrawImage(drawing, image, rect, opacity);
return;
}
drawing.FillRoundedRectangle(rect, 4, 4, WithOpacity(fallbackColor, opacity));
DrawCenteredText(drawing, fallbackLabel, rect, WithOpacity(Colors.White, opacity), Math.Max(9, (float)(rect.Height * 0.24)));
}
private string? MovableImageKey(GridPosition source)
{
var prop = m_Level.GetProp(source);
if (prop.Type != EPropType.None)
return ImageKey(prop);
var leak = m_Level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == source || leak.UndergroundPosition == source));
if (leak is not null)
return $"leak-{leak.Carrier.ToString().ToLowerInvariant()}";
return m_Level.Robot.Position == source ? "robot" : null;
}
private bool HasMovableAt(GridPosition source)
{
return MovableImageKey(source) is not null;
}
private float SurfaceOpacity()
{
return m_ActiveLayer == EEditorLayer.Surface ? 1.0f : 0.5f;
}
private float UndergroundOpacity(ECarrierType carrier)
{
if (m_ActiveLayer == EEditorLayer.Surface)
return 0.25f;
return LayerCarrier(m_ActiveLayer) == carrier ? 1.0f : 0.25f;
}
private static ECarrierType? LayerCarrier(EEditorLayer layer)
{
return layer switch {
EEditorLayer.Electricity => ECarrierType.Electricity,
EEditorLayer.Fuel => ECarrierType.Fuel,
EEditorLayer.Water => ECarrierType.Water,
_ => null
};
}
private static Color WithOpacity(Color color, float opacity)
{
return ColorHelper.FromArgb((byte)Math.Clamp(color.A * opacity, 0, 255), color.R, color.G, color.B);
}
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.IsolationValve => CarrierColor(prop.Carrier),
EPropType.SprinklerControl => ColorHelper.FromArgb(255, 54, 150, 186),
EPropType.SprinklerValve => ColorHelper.FromArgb(255, 38, 112, 150),
EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73),
_ => Colors.Gray
};
}
private static Color ToolColor(EditorToolCommand command)
{
return command.Tool switch {
EEditorTool.Floor => ColorHelper.FromArgb(255, 62, 76, 67),
EEditorTool.Wall => ColorHelper.FromArgb(255, 78, 86, 94),
EEditorTool.Underground or EEditorTool.Flow or EEditorTool.Leak or EEditorTool.SurfaceHazard => CarrierColor(command.Carrier),
EEditorTool.Heat => ColorHelper.FromArgb(255, 221, 120, 55),
EEditorTool.Robot => ColorHelper.FromArgb(255, 215, 219, 224),
_ => PropColor(new() { Type = ToolPropType(command.Tool), Carrier = command.Carrier, RemedyType = command.RemedyType })
};
}
private static EPropType ToolPropType(EEditorTool tool)
{
return tool switch {
EEditorTool.Flow => EPropType.Flow,
EEditorTool.Consumer => EPropType.Consumer,
EEditorTool.Junction => EPropType.Junction,
EEditorTool.Door => EPropType.Door,
EEditorTool.AllSeeingEyeTerminal => EPropType.AllSeeingEyeTerminal,
EEditorTool.IsolationValve => EPropType.IsolationValve,
EEditorTool.SprinklerControl => EPropType.SprinklerControl,
EEditorTool.SprinklerValve => EPropType.SprinklerValve,
EEditorTool.RemedySupply => EPropType.RemedySupply,
EEditorTool.ReactorControl => EPropType.ReactorControl,
_ => EPropType.None
};
}
private static string ImageKey(PropState prop)
{
return prop.Type switch {
EPropType.Flow => $"carrier-{prop.Carrier.ToString().ToLowerInvariant()}-source",
EPropType.Consumer => "prop-consumer",
EPropType.Junction => "prop-junction",
EPropType.Door => "prop-door",
EPropType.AllSeeingEyeTerminal => "prop-eye-terminal",
EPropType.IsolationValve => "prop-isolation-valve",
EPropType.SprinklerControl => "prop-sprinkler-control",
EPropType.SprinklerValve => "prop-sprinkler-valve",
EPropType.RemedySupply => "prop-remedy",
EPropType.ReactorControl => "prop-reactor",
_ => "prop"
};
}
private static string ImageKey(EditorToolCommand command)
{
return command.Tool switch {
EEditorTool.Floor => "tool-floor",
EEditorTool.Wall => "tool-wall",
EEditorTool.Underground => $"carrier-{command.Carrier.ToString().ToLowerInvariant()}",
EEditorTool.Flow => $"carrier-{command.Carrier.ToString().ToLowerInvariant()}-source",
EEditorTool.Leak => $"leak-{command.Carrier.ToString().ToLowerInvariant()}",
EEditorTool.SurfaceHazard => $"hazard-{command.Carrier.ToString().ToLowerInvariant()}",
EEditorTool.Heat => "hazard-heat",
EEditorTool.Robot => "robot",
_ => ImageKey(new PropState { Type = ToolPropType(command.Tool), Carrier = command.Carrier, RemedyType = command.RemedyType })
};
}
private static Color CarrierColor(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => c_FuelColor,
ECarrierType.Water => c_WaterColor,
ECarrierType.Electricity => c_ElectricityColor,
_ => Colors.White
};
}
private static string PropLabel(PropState prop)
{
return prop.Type switch {
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
EPropType.Consumer => "CON",
EPropType.Junction => $"J {prop.JunctionMode}",
EPropType.Door => "DOOR",
EPropType.AllSeeingEyeTerminal => "EYE",
EPropType.IsolationValve => prop.IsOpen ? "V OPEN" : "V CLOSED",
EPropType.SprinklerControl => prop.IsEnabled ? "SPR ON" : "SPR OFF",
EPropType.SprinklerValve => "SPR",
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
EPropType.ReactorControl => "REACT",
_ => string.Empty
};
}
private static string ToolShortLabel(EditorToolCommand command)
{
return command.Tool switch {
EEditorTool.Floor => "FLR",
EEditorTool.Wall => "WALL",
EEditorTool.Underground => $"{CarrierShort(command.Carrier)} NET",
EEditorTool.Flow => $"{CarrierShort(command.Carrier)} SRC",
EEditorTool.Leak => $"{CarrierShort(command.Carrier)} LEAK",
EEditorTool.SurfaceHazard => $"{CarrierShort(command.Carrier)} HAZ",
EEditorTool.Heat => "HEAT",
EEditorTool.Robot => "BOT",
_ => PropLabel(new() { Type = ToolPropType(command.Tool), Carrier = command.Carrier, RemedyType = command.RemedyType })
};
}
private static string CarrierShort(ECarrierType carrier)
{
return carrier switch {
ECarrierType.Fuel => "F",
ECarrierType.Water => "C",
ECarrierType.Electricity => "E",
_ => "?"
};
}
private static string RemedyShort(ERemedyType remedy)
{
return remedy switch {
ERemedyType.FuelNeutralizer => "F REM",
ERemedyType.WaterNeutralizer => "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.Water, "Water Net"),
CarrierTool(EEditorTool.Underground, ECarrierType.Electricity, "Electric Net"),
CarrierTool(EEditorTool.Flow, ECarrierType.Fuel, "Fuel Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Water, "Water Source"),
CarrierTool(EEditorTool.Flow, ECarrierType.Electricity, "Electric Source"),
CarrierTool(EEditorTool.IsolationValve, ECarrierType.Fuel, "Fuel Valve"),
CarrierTool(EEditorTool.IsolationValve, ECarrierType.Water, "Water Valve"),
CarrierTool(EEditorTool.IsolationValve, ECarrierType.Electricity, "Electric Valve"),
Tool(EEditorTool.Consumer, "Consumer"),
Tool(EEditorTool.Junction, "Junction"),
Tool(EEditorTool.Door, "Door"),
Tool(EEditorTool.AllSeeingEyeTerminal, "Eye Terminal"),
Tool(EEditorTool.SprinklerControl, "Sprinkler Control"),
Tool(EEditorTool.SprinklerValve, "Sprinkler Valve"),
RemedyTool(ERemedyType.FuelNeutralizer, "Fuel Remedy"),
RemedyTool(ERemedyType.WaterNeutralizer, "Water 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.Water, "Water Leak"),
CarrierTool(EEditorTool.Leak, ECarrierType.Electricity, "Electric Leak"),
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Fuel, "Fuel Hazard"),
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Water, "Water Hazard"),
CarrierTool(EEditorTool.SurfaceHazard, ECarrierType.Electricity, "Electric Hazard"),
Tool(EEditorTool.Heat, "Heat"),
Tool(EEditorTool.Robot, "Robot")
];
}
private bool IsToolAvailableOnActiveLayer(EditorToolCommand command)
{
if (command.Tool == EEditorTool.Cursor)
return true;
if (m_ActiveLayer == EEditorLayer.Surface)
{
return command.Tool is EEditorTool.Floor
or EEditorTool.Wall
or EEditorTool.Consumer
or EEditorTool.Junction
or EEditorTool.Door
or EEditorTool.AllSeeingEyeTerminal
or EEditorTool.IsolationValve
or EEditorTool.SprinklerControl
or EEditorTool.SprinklerValve
or EEditorTool.RemedySupply
or EEditorTool.ReactorControl
or EEditorTool.SurfaceHazard
or EEditorTool.Heat
or EEditorTool.Robot;
}
var activeCarrier = LayerCarrier(m_ActiveLayer);
return command.Carrier == activeCarrier && command.Tool is EEditorTool.Underground or EEditorTool.Flow or EEditorTool.Leak or EEditorTool.IsolationValve;
}
private void RefreshForecasts()
{
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
}
public GridPosition? HoverCell { get; private set; }
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_SimulationStepsPerSecond = 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, 65, 65);
private static readonly Color c_WaterColor = 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 EditorImageRegistry m_Images = new();
private readonly SimulationEngine m_Simulation = new();
private readonly DispatcherTimer m_SimulationTimer = new();
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
private StorageFile? m_CurrentFile;
private GridPosition? m_CursorDragStartCell;
private bool m_CursorDragStartRejected;
private bool m_DragExceededClickThreshold;
private GridPosition? m_DragPreviewDestination;
private string m_EditorFeedback = string.Empty;
private CanvasBitmap? m_HeatSprite;
private GridPosition? m_InvalidDragCell;
private bool m_IsPanning;
private GridPosition? m_LastPaintedCell;
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 GridPosition? m_SelectedCell;
private EditorToolCommand m_SelectedTool = new() { Tool = EEditorTool.Cursor };
private CanvasBitmap? m_TerrainTilemap;
private double m_Zoom = 1;
}