Add editor image badges and drag moves

This commit is contained in:
2026-05-11 23:03:29 +02:00
parent 884cc4503f
commit dfe0cb3b6a
7 changed files with 538 additions and 46 deletions

View File

@@ -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))

View File

@@ -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)

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

View File

@@ -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">

View File

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