1415 lines
55 KiB
C#
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;
|
|
} |