Add editor image badges and drag moves
This commit is contained in:
@@ -2,26 +2,106 @@
|
||||
|
||||
public static class LevelEditor
|
||||
{
|
||||
public sealed record MoveOccupantResult(bool Success, LevelState Level, string Reason)
|
||||
{
|
||||
public static MoveOccupantResult Succeeded(LevelState level)
|
||||
{
|
||||
return new(true, level, string.Empty);
|
||||
}
|
||||
|
||||
public static MoveOccupantResult Failed(LevelState level, string reason)
|
||||
{
|
||||
return new(false, level, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelState MoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
if (!level.InBounds(source) || !level.IsFloor(destination) || source == destination)
|
||||
return level;
|
||||
return TryMoveOccupant(level, source, destination).Level;
|
||||
}
|
||||
|
||||
public static MoveOccupantResult TryMoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
if (!level.InBounds(source))
|
||||
return MoveOccupantResult.Failed(level, "Drag start is outside the level.");
|
||||
|
||||
if (!level.InBounds(destination))
|
||||
return MoveOccupantResult.Failed(level, "Drop target is outside the level.");
|
||||
|
||||
if (source == destination)
|
||||
return MoveOccupantResult.Failed(level, "Drop target is the same cell.");
|
||||
|
||||
var prop = level.GetProp(source);
|
||||
if (prop.Type == EPropType.None)
|
||||
return level.Robot.Position == source ? level with { Robot = level.Robot with { Position = destination } } : level;
|
||||
if (prop.Type != EPropType.None)
|
||||
return TryMoveProp(level, source, destination, prop);
|
||||
|
||||
var leak = level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == source || leak.UndergroundPosition == source));
|
||||
if (leak is not null)
|
||||
return TryMoveLeak(level, leak, destination);
|
||||
|
||||
return level.Robot.Position == source
|
||||
? TryMoveRobot(level, destination)
|
||||
: MoveOccupantResult.Failed(level, "No movable robot, prop, source, or leak starts here.");
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Robot destination must be a floor cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(level with { Robot = level.Robot with { Position = destination } });
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveProp(LevelState level, GridPosition source, GridPosition destination, PropState prop)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Prop destination must be a floor cell.");
|
||||
|
||||
if (level.GetProp(destination).Type != EPropType.None)
|
||||
return level;
|
||||
return MoveOccupantResult.Failed(level, "Prop destination is already occupied.");
|
||||
|
||||
var next = level.SetProp(source, new()).SetProp(destination, prop);
|
||||
if (prop.Type != EPropType.ReactorControl)
|
||||
return next;
|
||||
return MoveOccupantResult.Succeeded(next);
|
||||
|
||||
return next with {
|
||||
return MoveOccupantResult.Succeeded(next with {
|
||||
Reactors = next.Reactors
|
||||
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveLeak(LevelState level, LeakState leak, GridPosition destination)
|
||||
{
|
||||
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Fuel and coolant leaks must move to a floor cell.");
|
||||
|
||||
var next = ClearLeak(level, leak)
|
||||
.SetUnderground(leak.UndergroundPosition, leak.Carrier, new());
|
||||
return MoveOccupantResult.Succeeded(SetLeak(next, destination, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
if (leak.Carrier == ECarrierType.Electricity)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must be an adjacent floor access cell.");
|
||||
|
||||
var undergroundPosition = leak.UndergroundPosition;
|
||||
if (undergroundPosition.ManhattanDistance(destination) != 1)
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must stay adjacent to its underground wall cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(SetLeak(ClearLeak(level, leak), undergroundPosition, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
return MoveOccupantResult.Failed(level, "Unsupported leak carrier.");
|
||||
}
|
||||
|
||||
private static LevelState ClearLeak(LevelState level, LeakState leak)
|
||||
{
|
||||
return level with {
|
||||
Leaks = level.Leaks.Where(candidate => candidate != leak).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +141,7 @@ public static class LevelEditor
|
||||
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||
EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }),
|
||||
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
||||
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
||||
EEditorTool.Door => ToggleOrSetDoor(level, position),
|
||||
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||
@@ -129,6 +209,21 @@ public static class LevelEditor
|
||||
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
|
||||
}
|
||||
|
||||
private static LevelState ToggleOrSetDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Door)
|
||||
{
|
||||
var nextState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||
return level.SetProp(position, prop with { DoorState = nextState });
|
||||
}
|
||||
|
||||
return level.SetProp(position, new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
|
||||
}
|
||||
|
||||
private static LevelState SetReactorControl(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
|
||||
@@ -103,10 +103,7 @@ public static class LevelStateExtensions
|
||||
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.SetSurface(position, new())
|
||||
.SetProp(position, new())
|
||||
.SetUnderground(position, ECarrierType.Fuel, new())
|
||||
.SetUnderground(position, ECarrierType.Coolant, new())
|
||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
||||
.SetProp(position, new());
|
||||
}
|
||||
|
||||
private static bool DoorBlocksEdge(LevelState level, GridPosition doorPosition, GridPosition neighbor)
|
||||
|
||||
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
|
||||
namespace ReactorMaintenance.Win2D;
|
||||
|
||||
public sealed class EditorImageRegistry
|
||||
{
|
||||
public async Task LoadAsync(CanvasControl sender)
|
||||
{
|
||||
m_Images.Clear();
|
||||
await LoadFolderAsync(sender, "Images", "Props");
|
||||
await LoadFolderAsync(sender, "Images", "Pipes");
|
||||
await LoadFolderAsync(sender, "Images", "Badges");
|
||||
await LoadFolderAsync(sender, "Images", "Elements");
|
||||
AddAlias("tool-floor", "floor");
|
||||
AddAlias("tool-wall", "wall");
|
||||
AddAlias("tool-heat", "heat");
|
||||
AddAlias("tool-robot", "robot");
|
||||
AddAlias("robot", "robot");
|
||||
AddAlias("prop-reactor", "reactor");
|
||||
AddAlias("prop-consumer", "generator");
|
||||
AddAlias("prop-flow", "generator");
|
||||
AddAlias("carrier-fuel-source", "generator");
|
||||
AddAlias("carrier-coolant-source", "cooling-pump");
|
||||
AddAlias("carrier-electricity-source", "generator");
|
||||
AddAlias("prop-junction", "pressure-regulator");
|
||||
AddAlias("prop-door", "wall");
|
||||
AddAlias("prop-eye-terminal", "diagnostic-terminal");
|
||||
AddAlias("prop-remedy", "repair");
|
||||
AddAlias("leak-fuel", "leak");
|
||||
AddAlias("leak-coolant", "leak");
|
||||
AddAlias("leak-electricity", "leak");
|
||||
AddAlias("hazard-heat", "heat");
|
||||
AddAlias("hazard-fuel", "fire");
|
||||
AddAlias("hazard-coolant", "leak");
|
||||
AddAlias("hazard-electricity", "heat");
|
||||
}
|
||||
|
||||
public CanvasBitmap? Get(string key)
|
||||
{
|
||||
return m_Images.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
private async Task LoadFolderAsync(CanvasControl sender, params string[] pathParts)
|
||||
{
|
||||
var folder = Path.Combine([AppContext.BaseDirectory, .. pathParts]);
|
||||
if (!Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(folder, "*.png"))
|
||||
{
|
||||
var key = Path.GetFileNameWithoutExtension(path);
|
||||
m_Images[key] = await CanvasBitmap.LoadAsync(sender, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAlias(string alias, string key)
|
||||
{
|
||||
if (!m_Images.ContainsKey(alias) && m_Images.TryGetValue(key, out var image))
|
||||
m_Images[alias] = image;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, CanvasBitmap> m_Images = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -94,10 +94,52 @@
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<ItemsControl x:Name="GlobalGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsWrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#3C4650" BorderThickness="1" Padding="8" Margin="0,0,8,8"
|
||||
Width="126" CornerRadius="3">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding Value}" Foreground="#F4F1E8" FontSize="15"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock Text="{Binding Description}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<ItemsControl x:Name="CellGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsWrapGrid Orientation="Horizontal" MaximumRowsOrColumns="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#3C4650" BorderThickness="1" Padding="8" Margin="0,0,8,8"
|
||||
Width="126" CornerRadius="3">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding Value}" Foreground="#F4F1E8" FontSize="14"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock Text="{Binding Description}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ForecastList">
|
||||
|
||||
@@ -44,6 +44,7 @@ public sealed partial class MainWindow
|
||||
}
|
||||
|
||||
private sealed record ForecastViewModel(string Message);
|
||||
private sealed record InspectorItemViewModel(string Label, string Value, string Description);
|
||||
|
||||
private sealed class EditorToolViewModel(EditorToolCommand command, string label) : INotifyPropertyChanged
|
||||
{
|
||||
@@ -92,10 +93,10 @@ public sealed partial class MainWindow
|
||||
|
||||
private async Task LoadImagesAsync(CanvasControl sender)
|
||||
{
|
||||
await m_Images.LoadAsync(sender);
|
||||
m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
|
||||
m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
|
||||
m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
|
||||
m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
|
||||
m_LeakSprite = m_Images.Get("leak");
|
||||
m_HeatSprite = m_Images.Get("heat");
|
||||
}
|
||||
|
||||
private static async Task<CanvasBitmap> LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
|
||||
@@ -259,18 +260,28 @@ public sealed partial class MainWindow
|
||||
m_DragExceededClickThreshold = false;
|
||||
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
|
||||
m_CursorDragStartCell = null;
|
||||
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 point = e.GetCurrentPoint(LevelCanvas);
|
||||
var deltaX = point.Position.X - m_LastPanPoint.X;
|
||||
var deltaY = point.Position.Y - m_LastPanPoint.Y;
|
||||
m_LastPanPoint = point.Position;
|
||||
@@ -287,6 +298,16 @@ public sealed partial class MainWindow
|
||||
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))
|
||||
{
|
||||
m_DragPreviewDestination = destination;
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
@@ -302,14 +323,21 @@ public sealed partial class MainWindow
|
||||
}
|
||||
else if (m_CursorDragStartCell is { } source && m_DragExceededClickThreshold && TryGetGridPosition(point.Position, out var destination))
|
||||
{
|
||||
m_Level = LevelEditor.MoveOccupant(m_Level, source, destination);
|
||||
m_SelectedCell = destination;
|
||||
SelectReactorFromCell(destination);
|
||||
RefreshForecasts();
|
||||
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)
|
||||
{
|
||||
SelectReactorFromCell(destination);
|
||||
RefreshForecasts();
|
||||
}
|
||||
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
else if (!m_DragExceededClickThreshold)
|
||||
else if (!m_DragExceededClickThreshold && !IsDragPaintTool(m_SelectedTool.Tool))
|
||||
{
|
||||
SelectOrPaintAt(point.Position);
|
||||
}
|
||||
@@ -318,6 +346,8 @@ public sealed partial class MainWindow
|
||||
m_LeftPointerDown = false;
|
||||
m_IsPanning = false;
|
||||
m_CursorDragStartCell = null;
|
||||
m_DragPreviewDestination = null;
|
||||
m_LastPaintedCell = null;
|
||||
m_DragExceededClickThreshold = false;
|
||||
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
||||
e.Handled = true;
|
||||
@@ -328,6 +358,8 @@ public sealed partial class MainWindow
|
||||
m_LeftPointerDown = false;
|
||||
m_IsPanning = false;
|
||||
m_CursorDragStartCell = null;
|
||||
m_DragPreviewDestination = null;
|
||||
m_LastPaintedCell = null;
|
||||
m_DragExceededClickThreshold = false;
|
||||
}
|
||||
|
||||
@@ -361,6 +393,29 @@ public sealed partial class MainWindow
|
||||
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 == m_HoverCell)
|
||||
return;
|
||||
|
||||
m_HoverCell = nextHover;
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private static bool IsDragPaintTool(EEditorTool tool)
|
||||
{
|
||||
return tool is EEditorTool.Floor or EEditorTool.Wall;
|
||||
}
|
||||
|
||||
private void ClearAt(Point point)
|
||||
{
|
||||
if (!TryGetGridPosition(point, out var position))
|
||||
@@ -390,6 +445,7 @@ public sealed partial class MainWindow
|
||||
DrawLeaks(drawing, layout, SurfaceOpacity());
|
||||
DrawRobot(drawing, layout, SurfaceOpacity());
|
||||
DrawGrid(drawing, layout);
|
||||
DrawEditorOverlays(drawing, layout);
|
||||
}
|
||||
|
||||
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
|
||||
@@ -569,9 +625,11 @@ public sealed partial class MainWindow
|
||||
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)center.X, (float)rect.Top, (float)center.X, (float)rect.Bottom, color, 5);
|
||||
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,8 +642,7 @@ public sealed partial class MainWindow
|
||||
continue;
|
||||
|
||||
var rect = Inset(layout.CellRect(position), 0.18);
|
||||
drawing.FillRoundedRectangle(rect, 4, 4, WithOpacity(PropColor(prop), opacity));
|
||||
DrawCenteredText(drawing, PropLabel(prop), rect, WithOpacity(Colors.White, opacity), Math.Max(10, (float)(layout.CellSize * 0.22)));
|
||||
DrawBadge(drawing, rect, ImageKey(prop), PropColor(prop), PropLabel(prop), opacity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +654,42 @@ public sealed partial class MainWindow
|
||||
|
||||
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
|
||||
{
|
||||
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04), 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 (m_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)
|
||||
@@ -625,8 +717,8 @@ public sealed partial class MainWindow
|
||||
private bool TryGetGridPosition(Point point, out GridPosition position)
|
||||
{
|
||||
var layout = GetLayout();
|
||||
var x = (int)((point.X - layout.OriginX) / layout.CellSize);
|
||||
var y = (int)((point.Y - layout.OriginY) / layout.CellSize);
|
||||
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);
|
||||
}
|
||||
@@ -689,14 +781,20 @@ public sealed partial class MainWindow
|
||||
{
|
||||
LevelNameText.Text = m_Level.Name;
|
||||
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
||||
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
|
||||
StatusText.Text = string.IsNullOrWhiteSpace(m_EditorFeedback)
|
||||
? $"{m_Level.Global.LevelState}: {m_Level.Global.Status}"
|
||||
: $"{m_Level.Global.LevelState}: {m_EditorFeedback}";
|
||||
var doorCount = m_Level.Props.Count(prop => prop.Type == EPropType.Door);
|
||||
GlobalText.Text = $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
|
||||
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
||||
+ $"Required consumers F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}\n"
|
||||
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {doorCount}";
|
||||
GlobalGrid.ItemsSource = new[] {
|
||||
new InspectorItemViewModel("Inventory", $"F {m_Level.Robot.FuelNeutralizers} / C {m_Level.Robot.CoolantNeutralizers} / E {m_Level.Robot.ElectricityNeutralizers} / H {m_Level.Robot.HeatShields}", "Remedies carried by the robot."),
|
||||
new InspectorItemViewModel("Heat Shield", m_Level.Robot.HeatImmunitySteps.ToString(CultureInfo.InvariantCulture), "Remaining protected movement steps."),
|
||||
new InspectorItemViewModel("Required F/C/E", $"{m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}", "Producing consumers needed for readiness."),
|
||||
new InspectorItemViewModel("Objects", $"R {m_Level.Reactors.Count} / L {m_Level.Leaks.Count} / D {doorCount}", "Reactors, active leaks, and doors.")
|
||||
};
|
||||
|
||||
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
|
||||
CellGrid.ItemsSource = m_SelectedCell is { } position && m_Level.InBounds(position)
|
||||
? CellInspectionItems(position)
|
||||
: new[] { new InspectorItemViewModel("Selection", "None", "Select a grid cell to inspect it.") };
|
||||
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
||||
}
|
||||
|
||||
@@ -725,22 +823,24 @@ public sealed partial class MainWindow
|
||||
RefreshForecasts();
|
||||
}
|
||||
|
||||
private string CellInspectionText(GridPosition position)
|
||||
private InspectorItemViewModel[] CellInspectionItems(GridPosition position)
|
||||
{
|
||||
var prop = m_Level.GetProp(position);
|
||||
var surface = m_Level.GetSurface(position);
|
||||
var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
|
||||
var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
|
||||
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
||||
return $"Position: {position.X},{position.Y}\n"
|
||||
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
||||
+ $"Prop: {prop.Type} {prop.SwitchState} {prop.ServiceState}\n"
|
||||
+ $"Consumer F/C/E: {prop.FuelServiceState} / {prop.CoolantServiceState} / {prop.ElectricityServiceState}\n"
|
||||
+ $"Fuel: {UndergroundText(fuel)}\n"
|
||||
+ $"Coolant: {UndergroundText(coolant)}\n"
|
||||
+ $"Electricity: {UndergroundText(electricity)}\n"
|
||||
+ $"Surface fuel/coolant/electricity/heat: {Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}\n"
|
||||
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
|
||||
return [
|
||||
new("Position", $"{position.X},{position.Y}", "Grid coordinate."),
|
||||
new("Terrain", m_Level.GetTerrain(position).ToString(), "Static surface cell type."),
|
||||
new("Prop", $"{prop.Type} {prop.SwitchState}", "Placed surface object and switch state."),
|
||||
new("Services F/C/E", $"{prop.FuelServiceState}/{prop.CoolantServiceState}/{prop.ElectricityServiceState}", "Consumer service status."),
|
||||
new("Fuel Net", UndergroundText(fuel), "Amount, intensity, and integrity."),
|
||||
new("Coolant Net", UndergroundText(coolant), "Amount, intensity, and integrity."),
|
||||
new("Electric Net", UndergroundText(electricity), "Amount, intensity, and integrity."),
|
||||
new("Surface F/C/E/H", $"{Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}", "Visible hazards and heat."),
|
||||
new("Blocks F/C/E", $"{surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}", "Temporary remedy entry blocks.")
|
||||
];
|
||||
}
|
||||
|
||||
private static string UndergroundText(UndergroundCell cell)
|
||||
@@ -831,6 +931,32 @@ public sealed partial class MainWindow
|
||||
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 float SurfaceOpacity()
|
||||
{
|
||||
return m_ActiveLayer == EEditorLayer.Surface ? 1.0f : 0.5f;
|
||||
@@ -873,6 +999,61 @@ public sealed partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
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.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.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 {
|
||||
@@ -897,6 +1078,21 @@ public sealed partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1015,14 +1211,20 @@ public sealed partial class MainWindow
|
||||
private static readonly Color c_CoolantColor = ColorHelper.FromArgb(255, 57, 174, 196);
|
||||
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
|
||||
private readonly IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
||||
private readonly EditorImageRegistry m_Images = new();
|
||||
|
||||
private readonly SimulationEngine m_Simulation = new();
|
||||
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
|
||||
private StorageFile? m_CurrentFile;
|
||||
private GridPosition? m_CursorDragStartCell;
|
||||
private bool m_DragExceededClickThreshold;
|
||||
private GridPosition? m_DragPreviewDestination;
|
||||
private string m_EditorFeedback = string.Empty;
|
||||
private CanvasBitmap? m_HeatSprite;
|
||||
private GridPosition? m_HoverCell;
|
||||
private GridPosition? m_InvalidDragCell;
|
||||
private bool m_IsPanning;
|
||||
private GridPosition? m_LastPaintedCell;
|
||||
private Point m_LastPanPoint;
|
||||
private CanvasBitmap? m_LeakSprite;
|
||||
private bool m_LeftPointerDown;
|
||||
@@ -1030,7 +1232,6 @@ public sealed partial class MainWindow
|
||||
private LevelState m_Level;
|
||||
private double m_PanX;
|
||||
private double m_PanY;
|
||||
private CanvasBitmap? m_RobotSprite;
|
||||
private GridPosition? m_SelectedCell;
|
||||
private int? m_SelectedReactorId = 1;
|
||||
private EditorToolCommand m_SelectedTool = new() { Tool = EEditorTool.Cursor };
|
||||
|
||||
Reference in New Issue
Block a user