Add editor image badges and drag moves
This commit is contained in:
@@ -455,6 +455,8 @@ The editor includes layer selection for Surface, Electricity, Fuel, and Coolant:
|
|||||||
- Networks render as thick lines connecting adjacent cell centers; sources render as large centered dots.
|
- Networks render as thick lines connecting adjacent cell centers; sources render as large centered dots.
|
||||||
- Tools are layer-aware. Cursor is always available. Surface terrain, props, consumers, hazards, doors, and heat tools are available only on Surface. Network painting and sources are available only on their matching underground layer.
|
- Tools are layer-aware. Cursor is always available. Surface terrain, props, consumers, hazards, doors, and heat tools are available only on Surface. Network painting and sources are available only on their matching underground layer.
|
||||||
|
|
||||||
|
Editor tool badges and drag previews use stable semantic image keys when assets are available. Assets may be added under `Images/Badges` or `Images/Elements` with filenames such as `tool-door.png`, `prop-reactor.png`, `carrier-fuel-source.png`, `leak-electricity.png`, or `robot.png`; missing assets fall back to compact procedural badges and text labels.
|
||||||
|
|
||||||
The serialized level schema stores level metadata, dimensions, terrain, underground layers including structural integrity, props and prop state, required reactor consumer counts, leaks, robot state, inventory, forecasts, and dynamic state when saving active play.
|
The serialized level schema stores level metadata, dimensions, terrain, underground layers including structural integrity, props and prop state, required reactor consumer counts, leaks, robot state, inventory, forecasts, and dynamic state when saving active play.
|
||||||
|
|
||||||
The loader accepts only schema-valid level data and returns clear errors for malformed data.
|
The loader accepts only schema-valid level data and returns clear errors for malformed data.
|
||||||
|
|||||||
@@ -2,26 +2,106 @@
|
|||||||
|
|
||||||
public static class LevelEditor
|
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)
|
public static LevelState MoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||||
{
|
{
|
||||||
if (!level.InBounds(source) || !level.IsFloor(destination) || source == destination)
|
return TryMoveOccupant(level, source, destination).Level;
|
||||||
return 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);
|
var prop = level.GetProp(source);
|
||||||
if (prop.Type == EPropType.None)
|
if (prop.Type != EPropType.None)
|
||||||
return level.Robot.Position == source ? level with { Robot = level.Robot with { Position = destination } } : level;
|
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)
|
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);
|
var next = level.SetProp(source, new()).SetProp(destination, prop);
|
||||||
if (prop.Type != EPropType.ReactorControl)
|
if (prop.Type != EPropType.ReactorControl)
|
||||||
return next;
|
return MoveOccupantResult.Succeeded(next);
|
||||||
|
|
||||||
return next with {
|
return MoveOccupantResult.Succeeded(next with {
|
||||||
Reactors = next.Reactors
|
Reactors = next.Reactors
|
||||||
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
|
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
|
||||||
.ToArray()
|
.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.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||||
EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }),
|
EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }),
|
||||||
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
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.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||||
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||||
@@ -129,6 +209,21 @@ public static class LevelEditor
|
|||||||
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
|
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)
|
private static LevelState SetReactorControl(LevelState level, GridPosition position)
|
||||||
{
|
{
|
||||||
if (!level.IsFloor(position))
|
if (!level.IsFloor(position))
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ public static class LevelStateExtensions
|
|||||||
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
|
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
|
||||||
{
|
{
|
||||||
return level.SetSurface(position, new())
|
return level.SetSurface(position, new())
|
||||||
.SetProp(position, new())
|
.SetProp(position, new());
|
||||||
.SetUnderground(position, ECarrierType.Fuel, new())
|
|
||||||
.SetUnderground(position, ECarrierType.Coolant, new())
|
|
||||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool DoorBlocksEdge(LevelState level, GridPosition doorPosition, GridPosition neighbor)
|
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>
|
</Grid>
|
||||||
|
|
||||||
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
<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 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" />
|
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||||
<ItemsControl x:Name="ForecastList">
|
<ItemsControl x:Name="ForecastList">
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public sealed partial class MainWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ForecastViewModel(string Message);
|
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
|
private sealed class EditorToolViewModel(EditorToolCommand command, string label) : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
@@ -92,10 +93,10 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private async Task LoadImagesAsync(CanvasControl sender)
|
private async Task LoadImagesAsync(CanvasControl sender)
|
||||||
{
|
{
|
||||||
|
await m_Images.LoadAsync(sender);
|
||||||
m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
|
m_TerrainTilemap = await LoadCanvasBitmapAsync(sender, "Images", "tilemap.png");
|
||||||
m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
|
m_LeakSprite = m_Images.Get("leak");
|
||||||
m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
|
m_HeatSprite = m_Images.Get("heat");
|
||||||
m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<CanvasBitmap> LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
|
private static async Task<CanvasBitmap> LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
|
||||||
@@ -259,18 +260,28 @@ public sealed partial class MainWindow
|
|||||||
m_DragExceededClickThreshold = false;
|
m_DragExceededClickThreshold = false;
|
||||||
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
|
m_IsPanning = e.KeyModifiers.HasFlag(VirtualKeyModifiers.Shift);
|
||||||
m_CursorDragStartCell = null;
|
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))
|
if (!m_IsPanning && m_SelectedTool.Tool == EEditorTool.Cursor && TryGetGridPosition(point.Position, out var position))
|
||||||
m_CursorDragStartCell = 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;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
|
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
var point = e.GetCurrentPoint(LevelCanvas);
|
||||||
|
UpdateHoverCell(point.Position);
|
||||||
if (!m_LeftPointerDown)
|
if (!m_LeftPointerDown)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var point = e.GetCurrentPoint(LevelCanvas);
|
|
||||||
var deltaX = point.Position.X - m_LastPanPoint.X;
|
var deltaX = point.Position.X - m_LastPanPoint.X;
|
||||||
var deltaY = point.Position.Y - m_LastPanPoint.Y;
|
var deltaY = point.Position.Y - m_LastPanPoint.Y;
|
||||||
m_LastPanPoint = point.Position;
|
m_LastPanPoint = point.Position;
|
||||||
@@ -287,6 +298,16 @@ public sealed partial class MainWindow
|
|||||||
ClampPan();
|
ClampPan();
|
||||||
LevelCanvas.Invalidate();
|
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;
|
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))
|
else if (m_CursorDragStartCell is { } source && m_DragExceededClickThreshold && TryGetGridPosition(point.Position, out var destination))
|
||||||
{
|
{
|
||||||
m_Level = LevelEditor.MoveOccupant(m_Level, source, destination);
|
var result = LevelEditor.TryMoveOccupant(m_Level, source, destination);
|
||||||
m_SelectedCell = 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);
|
SelectReactorFromCell(destination);
|
||||||
RefreshForecasts();
|
RefreshForecasts();
|
||||||
|
}
|
||||||
|
|
||||||
RefreshInspector();
|
RefreshInspector();
|
||||||
LevelCanvas.Invalidate();
|
LevelCanvas.Invalidate();
|
||||||
}
|
}
|
||||||
else if (!m_DragExceededClickThreshold)
|
else if (!m_DragExceededClickThreshold && !IsDragPaintTool(m_SelectedTool.Tool))
|
||||||
{
|
{
|
||||||
SelectOrPaintAt(point.Position);
|
SelectOrPaintAt(point.Position);
|
||||||
}
|
}
|
||||||
@@ -318,6 +346,8 @@ public sealed partial class MainWindow
|
|||||||
m_LeftPointerDown = false;
|
m_LeftPointerDown = false;
|
||||||
m_IsPanning = false;
|
m_IsPanning = false;
|
||||||
m_CursorDragStartCell = null;
|
m_CursorDragStartCell = null;
|
||||||
|
m_DragPreviewDestination = null;
|
||||||
|
m_LastPaintedCell = null;
|
||||||
m_DragExceededClickThreshold = false;
|
m_DragExceededClickThreshold = false;
|
||||||
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
LevelCanvas.ReleasePointerCapture(e.Pointer);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
@@ -328,6 +358,8 @@ public sealed partial class MainWindow
|
|||||||
m_LeftPointerDown = false;
|
m_LeftPointerDown = false;
|
||||||
m_IsPanning = false;
|
m_IsPanning = false;
|
||||||
m_CursorDragStartCell = null;
|
m_CursorDragStartCell = null;
|
||||||
|
m_DragPreviewDestination = null;
|
||||||
|
m_LastPaintedCell = null;
|
||||||
m_DragExceededClickThreshold = false;
|
m_DragExceededClickThreshold = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +393,29 @@ public sealed partial class MainWindow
|
|||||||
LevelCanvas.Invalidate();
|
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)
|
private void ClearAt(Point point)
|
||||||
{
|
{
|
||||||
if (!TryGetGridPosition(point, out var position))
|
if (!TryGetGridPosition(point, out var position))
|
||||||
@@ -390,6 +445,7 @@ public sealed partial class MainWindow
|
|||||||
DrawLeaks(drawing, layout, SurfaceOpacity());
|
DrawLeaks(drawing, layout, SurfaceOpacity());
|
||||||
DrawRobot(drawing, layout, SurfaceOpacity());
|
DrawRobot(drawing, layout, SurfaceOpacity());
|
||||||
DrawGrid(drawing, layout);
|
DrawGrid(drawing, layout);
|
||||||
|
DrawEditorOverlays(drawing, layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
|
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
|
||||||
@@ -569,9 +625,11 @@ public sealed partial class MainWindow
|
|||||||
var center = Center(rect);
|
var center = Center(rect);
|
||||||
var color = WithOpacity(prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, opacity);
|
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)))
|
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);
|
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
|
||||||
else
|
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;
|
continue;
|
||||||
|
|
||||||
var rect = Inset(layout.CellRect(position), 0.18);
|
var rect = Inset(layout.CellRect(position), 0.18);
|
||||||
drawing.FillRoundedRectangle(rect, 4, 4, WithOpacity(PropColor(prop), opacity));
|
DrawBadge(drawing, rect, ImageKey(prop), PropColor(prop), PropLabel(prop), opacity);
|
||||||
DrawCenteredText(drawing, PropLabel(prop), rect, WithOpacity(Colors.White, opacity), Math.Max(10, (float)(layout.CellSize * 0.22)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +654,42 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout, float opacity)
|
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)
|
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
@@ -625,8 +717,8 @@ public sealed partial class MainWindow
|
|||||||
private bool TryGetGridPosition(Point point, out GridPosition position)
|
private bool TryGetGridPosition(Point point, out GridPosition position)
|
||||||
{
|
{
|
||||||
var layout = GetLayout();
|
var layout = GetLayout();
|
||||||
var x = (int)((point.X - layout.OriginX) / layout.CellSize);
|
var x = (int)Math.Floor((point.X - layout.OriginX) / layout.CellSize);
|
||||||
var y = (int)((point.Y - layout.OriginY) / layout.CellSize);
|
var y = (int)Math.Floor((point.Y - layout.OriginY) / layout.CellSize);
|
||||||
position = new(x, y);
|
position = new(x, y);
|
||||||
return m_Level.InBounds(position);
|
return m_Level.InBounds(position);
|
||||||
}
|
}
|
||||||
@@ -689,14 +781,20 @@ public sealed partial class MainWindow
|
|||||||
{
|
{
|
||||||
LevelNameText.Text = m_Level.Name;
|
LevelNameText.Text = m_Level.Name;
|
||||||
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
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);
|
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"
|
GlobalGrid.ItemsSource = new[] {
|
||||||
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
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."),
|
||||||
+ $"Required consumers F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}\n"
|
new InspectorItemViewModel("Heat Shield", m_Level.Robot.HeatImmunitySteps.ToString(CultureInfo.InvariantCulture), "Remaining protected movement steps."),
|
||||||
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {doorCount}";
|
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();
|
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,22 +823,24 @@ public sealed partial class MainWindow
|
|||||||
RefreshForecasts();
|
RefreshForecasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CellInspectionText(GridPosition position)
|
private InspectorItemViewModel[] CellInspectionItems(GridPosition position)
|
||||||
{
|
{
|
||||||
var prop = m_Level.GetProp(position);
|
var prop = m_Level.GetProp(position);
|
||||||
var surface = m_Level.GetSurface(position);
|
var surface = m_Level.GetSurface(position);
|
||||||
var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
|
var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
|
||||||
var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
|
var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
|
||||||
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
||||||
return $"Position: {position.X},{position.Y}\n"
|
return [
|
||||||
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
new("Position", $"{position.X},{position.Y}", "Grid coordinate."),
|
||||||
+ $"Prop: {prop.Type} {prop.SwitchState} {prop.ServiceState}\n"
|
new("Terrain", m_Level.GetTerrain(position).ToString(), "Static surface cell type."),
|
||||||
+ $"Consumer F/C/E: {prop.FuelServiceState} / {prop.CoolantServiceState} / {prop.ElectricityServiceState}\n"
|
new("Prop", $"{prop.Type} {prop.SwitchState}", "Placed surface object and switch state."),
|
||||||
+ $"Fuel: {UndergroundText(fuel)}\n"
|
new("Services F/C/E", $"{prop.FuelServiceState}/{prop.CoolantServiceState}/{prop.ElectricityServiceState}", "Consumer service status."),
|
||||||
+ $"Coolant: {UndergroundText(coolant)}\n"
|
new("Fuel Net", UndergroundText(fuel), "Amount, intensity, and integrity."),
|
||||||
+ $"Electricity: {UndergroundText(electricity)}\n"
|
new("Coolant Net", UndergroundText(coolant), "Amount, intensity, and integrity."),
|
||||||
+ $"Surface fuel/coolant/electricity/heat: {Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}\n"
|
new("Electric Net", UndergroundText(electricity), "Amount, intensity, and integrity."),
|
||||||
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
|
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)
|
private static string UndergroundText(UndergroundCell cell)
|
||||||
@@ -831,6 +931,32 @@ public sealed partial class MainWindow
|
|||||||
drawing.DrawImage(image, rect, image.Bounds, opacity);
|
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()
|
private float SurfaceOpacity()
|
||||||
{
|
{
|
||||||
return m_ActiveLayer == EEditorLayer.Surface ? 1.0f : 0.5f;
|
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)
|
private static Color CarrierColor(ECarrierType carrier)
|
||||||
{
|
{
|
||||||
return carrier switch {
|
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)
|
private static string CarrierShort(ECarrierType carrier)
|
||||||
{
|
{
|
||||||
return carrier switch {
|
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_CoolantColor = ColorHelper.FromArgb(255, 57, 174, 196);
|
||||||
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
|
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
|
||||||
private readonly IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
private readonly IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
||||||
|
private readonly EditorImageRegistry m_Images = new();
|
||||||
|
|
||||||
private readonly SimulationEngine m_Simulation = new();
|
private readonly SimulationEngine m_Simulation = new();
|
||||||
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
|
private EEditorLayer m_ActiveLayer = EEditorLayer.Surface;
|
||||||
private StorageFile? m_CurrentFile;
|
private StorageFile? m_CurrentFile;
|
||||||
private GridPosition? m_CursorDragStartCell;
|
private GridPosition? m_CursorDragStartCell;
|
||||||
private bool m_DragExceededClickThreshold;
|
private bool m_DragExceededClickThreshold;
|
||||||
|
private GridPosition? m_DragPreviewDestination;
|
||||||
|
private string m_EditorFeedback = string.Empty;
|
||||||
private CanvasBitmap? m_HeatSprite;
|
private CanvasBitmap? m_HeatSprite;
|
||||||
|
private GridPosition? m_HoverCell;
|
||||||
|
private GridPosition? m_InvalidDragCell;
|
||||||
private bool m_IsPanning;
|
private bool m_IsPanning;
|
||||||
|
private GridPosition? m_LastPaintedCell;
|
||||||
private Point m_LastPanPoint;
|
private Point m_LastPanPoint;
|
||||||
private CanvasBitmap? m_LeakSprite;
|
private CanvasBitmap? m_LeakSprite;
|
||||||
private bool m_LeftPointerDown;
|
private bool m_LeftPointerDown;
|
||||||
@@ -1030,7 +1232,6 @@ public sealed partial class MainWindow
|
|||||||
private LevelState m_Level;
|
private LevelState m_Level;
|
||||||
private double m_PanX;
|
private double m_PanX;
|
||||||
private double m_PanY;
|
private double m_PanY;
|
||||||
private CanvasBitmap? m_RobotSprite;
|
|
||||||
private GridPosition? m_SelectedCell;
|
private GridPosition? m_SelectedCell;
|
||||||
private int? m_SelectedReactorId = 1;
|
private int? m_SelectedReactorId = 1;
|
||||||
private EditorToolCommand m_SelectedTool = new() { Tool = EEditorTool.Cursor };
|
private EditorToolCommand m_SelectedTool = new() { Tool = EEditorTool.Cursor };
|
||||||
|
|||||||
@@ -13,6 +13,35 @@ public sealed class LevelEditorTests
|
|||||||
Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState);
|
Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoorToolTogglesExistingDoorState()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Door editor", 6, 6);
|
||||||
|
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||||
|
|
||||||
|
var opened = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||||
|
var closed = LevelEditor.Apply(opened, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||||
|
|
||||||
|
Assert.Equal(EDoorState.Open, opened.GetProp(new(2, 2)).DoorState);
|
||||||
|
Assert.Equal(EDoorState.Closed, closed.GetProp(new(2, 2)).DoorState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WallToolPreservesUndergroundNetworks()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Wall editor", 6, 6);
|
||||||
|
var position = new GridPosition(2, 2);
|
||||||
|
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel });
|
||||||
|
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Coolant });
|
||||||
|
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
|
||||||
|
|
||||||
|
var next = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Wall });
|
||||||
|
|
||||||
|
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Fuel).State);
|
||||||
|
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Coolant).State);
|
||||||
|
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Electricity).State);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ConsumerToolPlacesCarrierAgnosticConsumer()
|
public void ConsumerToolPlacesCarrierAgnosticConsumer()
|
||||||
{
|
{
|
||||||
@@ -92,4 +121,66 @@ public sealed class LevelEditorTests
|
|||||||
Assert.Equal(EPropType.ReactorControl, next.GetProp(new(3, 3)).Type);
|
Assert.Equal(EPropType.ReactorControl, next.GetProp(new(3, 3)).Type);
|
||||||
Assert.Equal(new(3, 3), next.Reactors[0].ControlPosition);
|
Assert.Equal(new(3, 3), next.Reactors[0].ControlPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveOccupantMovesSourcesAsProps()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Move editor", 6, 6);
|
||||||
|
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.Flow, Carrier = ECarrierType.Fuel });
|
||||||
|
|
||||||
|
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.Reason);
|
||||||
|
Assert.Equal(EPropType.None, result.Level.GetProp(new(1, 1)).Type);
|
||||||
|
Assert.Equal(EPropType.Flow, result.Level.GetProp(new(3, 3)).Type);
|
||||||
|
Assert.Equal(ECarrierType.Fuel, result.Level.GetProp(new(3, 3)).Carrier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveOccupantMovesFuelLeakToFloorDestination()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Move editor", 6, 6);
|
||||||
|
level = LevelEditor.SetLeak(level, new(1, 1), new(1, 1), ECarrierType.Fuel);
|
||||||
|
|
||||||
|
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.Reason);
|
||||||
|
Assert.Single(result.Level.Leaks);
|
||||||
|
Assert.Equal(new(3, 3), result.Level.Leaks[0].UndergroundPosition);
|
||||||
|
Assert.Equal(new(3, 3), result.Level.Leaks[0].AccessPosition);
|
||||||
|
Assert.Equal(EUndergroundState.Leaking, result.Level.GetUnderground(new(3, 3), ECarrierType.Fuel).State);
|
||||||
|
Assert.Equal(EUndergroundState.Absent, result.Level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveOccupantMovesElectricityLeakAccessFace()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Move editor", 6, 6);
|
||||||
|
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||||
|
level = LevelEditor.SetLeak(level, new(2, 2), new(2, 3), ECarrierType.Electricity);
|
||||||
|
|
||||||
|
var result = LevelEditor.TryMoveOccupant(level, new(2, 3), new(3, 2));
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.Reason);
|
||||||
|
Assert.Single(result.Level.Leaks);
|
||||||
|
Assert.Equal(new(2, 2), result.Level.Leaks[0].UndergroundPosition);
|
||||||
|
Assert.Equal(new(3, 2), result.Level.Leaks[0].AccessPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveOccupantReportsInvalidStartAndDestinationReasons()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Move editor", 6, 6);
|
||||||
|
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
|
||||||
|
level = level.SetProp(new(1, 1), new() { Type = EPropType.Consumer });
|
||||||
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer });
|
||||||
|
|
||||||
|
var invalidStart = LevelEditor.TryMoveOccupant(level, new(4, 4), new(5, 5));
|
||||||
|
var invalidDestination = LevelEditor.TryMoveOccupant(level, new(1, 1), new(2, 2));
|
||||||
|
|
||||||
|
Assert.False(invalidStart.Success);
|
||||||
|
Assert.Contains("No movable", invalidStart.Reason);
|
||||||
|
Assert.False(invalidDestination.Success);
|
||||||
|
Assert.Contains("occupied", invalidDestination.Reason);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user