Implement Godot grid viewport rendering
This commit is contained in:
14
TASKS.md
14
TASKS.md
@@ -45,7 +45,7 @@ The Godot frontend (src/ReactorMaintenance.Godot) has a complete UX scaffold (sp
|
||||
|
||||
**Goal:** Replace the grid placeholder with a functional, interactive tile-based viewport.
|
||||
|
||||
- [ ] **Task 2.1: Create GridViewport control**
|
||||
- [x] **Task 2.1: Create GridViewport control**
|
||||
- Location: src/ReactorMaintenance.Godot/Controls/GridViewport.cs
|
||||
- Custom Control that renders the level grid via CanvasItem._draw()
|
||||
- Supports configurable tile size (default 48px)
|
||||
@@ -53,35 +53,35 @@ The Godot frontend (src/ReactorMaintenance.Godot) has a complete UX scaffold (sp
|
||||
- Exposes SelectedCell, RobotPosition, HoveredCell properties
|
||||
- Fires OnCellSelected, OnCellHovered, OnGridClicked signals
|
||||
|
||||
- [ ] **Task 2.2: Implement terrain rendering**
|
||||
- [x] **Task 2.2: Implement terrain rendering**
|
||||
- Draw floor tiles with graphite/steel palette per ART.md
|
||||
- Draw wall tiles with darker steel, reinforced silhouettes
|
||||
- Apply subtle hue shifts between adjacent floor plates (no checkerboard)
|
||||
- Support layer opacity rules (surface full / underground 25% or 50%)
|
||||
|
||||
- [ ] **Task 2.3: Implement prop rendering**
|
||||
- [x] **Task 2.3: Implement prop rendering**
|
||||
- Draw props at cell centers with semantic colors
|
||||
- Show state indicators (enabled/disabled, producing/starved)
|
||||
- Use generated asset textures where available, fall back to procedural badges
|
||||
- Support prop highlight for selected cell
|
||||
|
||||
- [ ] **Task 2.4: Implement robot marker**
|
||||
- [x] **Task 2.4: Implement robot marker**
|
||||
- Draw robot sprite at current RobotPosition
|
||||
- Show direction indicator
|
||||
- Use maintenance_robot.png from Assets/Characters
|
||||
- Animate on position change (optional, low priority)
|
||||
|
||||
- [ ] **Task 2.5: Implement hazard rendering**
|
||||
- [x] **Task 2.5: Implement hazard rendering**
|
||||
- Draw surface hazards on affected floor cells
|
||||
- Use semantic colors: fuel (red slicks), coolant (blue-cyan), electricity (yellow arcs), heat (orange-white)
|
||||
- Show hazard intensity via saturation/opacity
|
||||
|
||||
- [ ] **Task 2.6: Implement cell selection highlight**
|
||||
- [x] **Task 2.6: Implement cell selection highlight**
|
||||
- Draw selection rectangle around selected cell
|
||||
- Draw reachable/actionable hints on valid move targets
|
||||
- Support hover highlight on non-selected cells
|
||||
|
||||
- [ ] **Task 2.7: Implement underground network visualization**
|
||||
- [x] **Task 2.7: Implement underground network visualization**
|
||||
- Draw carrier-colored network lines between adjacent cells with positive flow
|
||||
- Fuel = red, Coolant = blue, Electricity = yellow
|
||||
- Render sources as large centered dots
|
||||
|
||||
BIN
src/ReactorMaintenance.Godot/Assets/Terrain/terrain_tilemap.png
Normal file
BIN
src/ReactorMaintenance.Godot/Assets/Terrain/terrain_tilemap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dlisn0ksvlt2v"
|
||||
path="res://.godot/imported/terrain_tilemap.png-37fc7f48e81d272b617dcdd0ccab8c0e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Assets/Terrain/terrain_tilemap.png"
|
||||
dest_files=["res://.godot/imported/terrain_tilemap.png-37fc7f48e81d272b617dcdd0ccab8c0e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -1,4 +1,4 @@
|
||||
using Godot;
|
||||
using Godot;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
@@ -28,4 +28,5 @@ internal static class FrontendAssets
|
||||
public const string PrimaryButtonAccent = "res://Assets/Ui/primary_button_accent.png";
|
||||
public const string ScannerEyeIcon = "res://Assets/Ui/scanner_eye_icon.png";
|
||||
public const string StateBadgeFrame = "res://Assets/Ui/state_badge_frame.png";
|
||||
public const string TerrainTilemap = "res://Assets/Terrain/terrain_tilemap.png";
|
||||
}
|
||||
633
src/ReactorMaintenance.Godot/Controls/GridViewport.cs
Normal file
633
src/ReactorMaintenance.Godot/Controls/GridViewport.cs
Normal file
@@ -0,0 +1,633 @@
|
||||
using Godot;
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Godot.Controls;
|
||||
|
||||
public partial class GridViewport : Control
|
||||
{
|
||||
private readonly record struct SGridLayout(float CellSize, Vector2 Origin)
|
||||
{
|
||||
public Rect2 CellRect(GridPosition position)
|
||||
{
|
||||
return new(Origin + new Vector2(position.X * CellSize, position.Y * CellSize), new(CellSize, CellSize));
|
||||
}
|
||||
|
||||
public Rect2 DualTileRect(int x, int y)
|
||||
{
|
||||
return new(Origin + new Vector2((x - 0.5f) * CellSize, (y - 0.5f) * CellSize), new(CellSize, CellSize));
|
||||
}
|
||||
|
||||
public Vector2 CellCenter(GridPosition position)
|
||||
{
|
||||
return CellRect(position).GetCenter();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Stop;
|
||||
FocusMode = FocusModeEnum.Click;
|
||||
ClipContents = true;
|
||||
m_TerrainTilemap = FrontendAssets.LoadTexture(FrontendAssets.TerrainTilemap);
|
||||
m_RobotTexture = FrontendAssets.LoadTexture(FrontendAssets.MaintenanceRobot);
|
||||
}
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
DrawRect(new(Vector2.Zero, Size), c_BackgroundColor);
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
var layout = GetLayout();
|
||||
DrawTerrain(layout, SurfaceOpacity());
|
||||
DrawUnderground(layout);
|
||||
DrawSurfaceHazards(layout, SurfaceOpacity());
|
||||
DrawDoors(layout, SurfaceOpacity());
|
||||
DrawProps(layout, SurfaceOpacity());
|
||||
DrawLeaks(layout, SurfaceOpacity());
|
||||
DrawReachableHints(layout);
|
||||
DrawRobot(layout, SurfaceOpacity());
|
||||
DrawGridOverlays(layout);
|
||||
}
|
||||
|
||||
public override void _GuiInput(InputEvent @event)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
if (@event is InputEventMouseMotion motion)
|
||||
{
|
||||
if (m_IsPanning)
|
||||
{
|
||||
PanOffset += motion.Relative;
|
||||
QueueRedraw();
|
||||
return;
|
||||
}
|
||||
|
||||
SetHoveredCell(ScreenToCell(motion.Position));
|
||||
return;
|
||||
}
|
||||
|
||||
if (@event is not InputEventMouseButton mouseButton)
|
||||
return;
|
||||
|
||||
if (mouseButton.ButtonIndex is MouseButton.WheelUp or MouseButton.WheelDown && mouseButton.Pressed)
|
||||
{
|
||||
ZoomAt(mouseButton.Position, mouseButton.ButtonIndex == MouseButton.WheelUp ? 1.12f : 1 / 1.12f);
|
||||
AcceptEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouseButton.ButtonIndex == MouseButton.Middle)
|
||||
{
|
||||
m_IsPanning = mouseButton.Pressed;
|
||||
AcceptEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouseButton.ButtonIndex == MouseButton.Left && mouseButton.Pressed)
|
||||
{
|
||||
if (ScreenToCell(mouseButton.Position) is { } cell)
|
||||
{
|
||||
SelectedCell = cell;
|
||||
EmitSignal(SignalName.OnCellSelected, cell);
|
||||
EmitSignal(SignalName.OnGridClicked, cell);
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
AcceptEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLevelState(LevelState levelState)
|
||||
{
|
||||
m_LevelState = levelState;
|
||||
RobotPosition = ToVector(levelState.Robot.Position);
|
||||
if (!IsValidCell(SelectedCell))
|
||||
SelectedCell = RobotPosition;
|
||||
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
private void DrawTerrain(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
for (var y = 0; y <= m_LevelState.Height; y++)
|
||||
{
|
||||
for (var x = 0; x <= m_LevelState.Width; x++)
|
||||
DrawDualTerrainTile(layout.DualTileRect(x, y), GetDualTileFloorMask(x, y), opacity);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDualTerrainTile(Rect2 rect, int floorMask, float opacity)
|
||||
{
|
||||
if (m_TerrainTilemap is null)
|
||||
{
|
||||
DrawFallbackTerrainTile(rect, floorMask, opacity);
|
||||
return;
|
||||
}
|
||||
|
||||
var wallMask = c_AllCorners ^ floorMask;
|
||||
DrawTextureRectRegion(m_TerrainTilemap, rect, TilemapSourceRect(wallMask), new(1, 1, 1, opacity));
|
||||
}
|
||||
|
||||
private void DrawFallbackTerrainTile(Rect2 rect, int floorMask, float opacity)
|
||||
{
|
||||
var color = floorMask == c_AllCorners ? new Color(0.13f, 0.16f, 0.17f, opacity) : new Color(0.18f, 0.20f, 0.22f, opacity);
|
||||
DrawRect(rect, color);
|
||||
}
|
||||
|
||||
private void DrawUnderground(SGridLayout layout)
|
||||
{
|
||||
foreach (var carrier in OrderedUndergroundLayers())
|
||||
DrawUndergroundLayer(layout, carrier, CarrierColor(carrier), UndergroundOpacity(carrier));
|
||||
}
|
||||
|
||||
private IEnumerable<ECarrierType> OrderedUndergroundLayers()
|
||||
{
|
||||
var carriers = new[] { ECarrierType.Fuel, ECarrierType.Coolant, ECarrierType.Electricity }.Where(IsLayerVisible).ToArray();
|
||||
return ActiveUndergroundLayer is { } activeCarrier && carriers.Contains(activeCarrier)
|
||||
? carriers.Where(carrier => carrier != activeCarrier).Append(activeCarrier)
|
||||
: carriers;
|
||||
}
|
||||
|
||||
private void DrawUndergroundLayer(SGridLayout layout, ECarrierType carrier, Color color, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
var layerColor = WithOpacity(color, opacity);
|
||||
var lineWidth = Math.Max(4, layout.CellSize * 0.16f);
|
||||
var cellDotRadius = Math.Max(2, layout.CellSize * 0.08f);
|
||||
var sourceDotRadius = Math.Max(5, layout.CellSize * 0.22f);
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
var cell = m_LevelState.GetUnderground(position, carrier);
|
||||
if (!cell.IsPresent)
|
||||
continue;
|
||||
|
||||
var center = layout.CellCenter(position);
|
||||
DrawNetworkConnection(layout, carrier, position, new(position.X + 1, position.Y), layerColor, lineWidth);
|
||||
DrawNetworkConnection(layout, carrier, position, new(position.X, position.Y + 1), layerColor, lineWidth);
|
||||
DrawCircle(center, cellDotRadius, layerColor);
|
||||
|
||||
if (cell.State == EUndergroundState.Leaking)
|
||||
DrawArc(center, sourceDotRadius * 0.7f, 0, Mathf.Tau, 32, c_LeakColor, Math.Max(2, lineWidth * 0.25f));
|
||||
|
||||
var prop = m_LevelState.GetProp(position);
|
||||
if (prop is { Type: EPropType.Flow } && prop.Carrier == carrier)
|
||||
DrawCircle(center, sourceDotRadius, layerColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNetworkConnection(SGridLayout layout, ECarrierType carrier, GridPosition position, GridPosition neighbor, Color color, float lineWidth)
|
||||
{
|
||||
if (m_LevelState is null || !m_LevelState.InBounds(neighbor) || !m_LevelState.GetUnderground(neighbor, carrier).IsPresent)
|
||||
return;
|
||||
|
||||
DrawLine(layout.CellCenter(position), layout.CellCenter(neighbor), color, lineWidth);
|
||||
}
|
||||
|
||||
private void DrawSurfaceHazards(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var position in AllPositions().Where(m_LevelState.IsFloor))
|
||||
{
|
||||
var surface = m_LevelState.GetSurface(position);
|
||||
var rect = layout.CellRect(position);
|
||||
FillHazard(rect, surface.Fuel, c_FuelColor, 0.08f, opacity, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
|
||||
FillHazard(rect, surface.Coolant, c_CoolantColor, 0.18f, opacity, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical);
|
||||
FillHazard(rect, surface.Electricity, c_ElectricityColor, 0.28f, opacity, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
|
||||
FillHazard(rect, surface.Heat, c_HeatColor, 0.34f, opacity, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
|
||||
}
|
||||
}
|
||||
|
||||
private void FillHazard(Rect2 rect, float amount, Color color, float inset, float opacity, float caution, float critical)
|
||||
{
|
||||
var overlayOpacity = SurfaceOverlayOpacity(amount, caution, critical);
|
||||
if (overlayOpacity <= 0)
|
||||
return;
|
||||
|
||||
DrawRect(Inset(rect, inset), WithOpacity(color, overlayOpacity * opacity * 0.68f));
|
||||
}
|
||||
|
||||
private void DrawDoors(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
var prop = m_LevelState.GetProp(position);
|
||||
if (prop.Type != EPropType.Door)
|
||||
continue;
|
||||
|
||||
var rect = layout.CellRect(position);
|
||||
var center = layout.CellCenter(position);
|
||||
var color = WithOpacity(prop.DoorState == EDoorState.Open ? c_ReadyColor : c_LeakColor, opacity);
|
||||
var width = Math.Max(3, layout.CellSize * 0.1f);
|
||||
if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1)))
|
||||
DrawLine(new(center.X, rect.Position.Y), new(center.X, rect.End.Y), color, width);
|
||||
else
|
||||
DrawLine(new(rect.Position.X, center.Y), new(rect.End.X, center.Y), color, width);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawProps(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
var prop = m_LevelState.GetProp(position);
|
||||
if (prop.Type == EPropType.None || prop.Type == EPropType.Door)
|
||||
continue;
|
||||
|
||||
DrawPropBadge(Inset(layout.CellRect(position), 0.18f), prop, opacity);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPropBadge(Rect2 rect, PropState prop, float opacity)
|
||||
{
|
||||
var color = WithOpacity(PropColor(prop), opacity);
|
||||
DrawRect(rect, color);
|
||||
DrawRect(rect, WithOpacity(Colors.White, opacity * 0.4f), false, Math.Max(1, rect.Size.X * 0.04f));
|
||||
DrawString(ThemeDB.FallbackFont, rect.GetCenter() + new Vector2(-rect.Size.X * 0.3f, rect.Size.Y * 0.09f), PropLabel(prop), HorizontalAlignment.Center, rect.Size.X * 0.6f, (int)Math.Max(9, rect.Size.Y * 0.24f), WithOpacity(Colors.White, opacity));
|
||||
|
||||
if (prop.Type is EPropType.Flow or EPropType.Consumer)
|
||||
{
|
||||
var indicatorColor = prop.IsEnabled ? c_ReadyColor : c_DisabledColor;
|
||||
DrawCircle(rect.Position + new Vector2(rect.Size.X * 0.82f, rect.Size.Y * 0.18f), Math.Max(3, rect.Size.X * 0.08f), WithOpacity(indicatorColor, opacity));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawLeaks(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var leak in m_LevelState.Leaks.Where(leak => !leak.Repaired))
|
||||
{
|
||||
var rect = Inset(layout.CellRect(leak.AccessPosition), 0.12f);
|
||||
DrawArc(rect.GetCenter(), rect.Size.X * 0.32f, 0, Mathf.Tau, 24, WithOpacity(CarrierColor(leak.Carrier), opacity), Math.Max(3, rect.Size.X * 0.08f));
|
||||
DrawLine(rect.Position + new Vector2(rect.Size.X * 0.25f, rect.Size.Y * 0.75f), rect.Position + new Vector2(rect.Size.X * 0.75f, rect.Size.Y * 0.25f), WithOpacity(c_LeakColor, opacity), Math.Max(3, rect.Size.X * 0.07f));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawReachableHints(SGridLayout layout)
|
||||
{
|
||||
if (m_LevelState is null || !IsValidCell(RobotPosition))
|
||||
return;
|
||||
|
||||
var robot = ToGridPosition(RobotPosition);
|
||||
foreach (var neighbor in robot.Neighbors().Where(m_LevelState.IsFloor))
|
||||
DrawRect(Inset(layout.CellRect(neighbor), 0.29f), c_ReachableColor);
|
||||
}
|
||||
|
||||
private void DrawRobot(SGridLayout layout, float opacity)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
var rect = Inset(layout.CellRect(m_LevelState.Robot.Position), 0.04f);
|
||||
if (m_RobotTexture is not null)
|
||||
{
|
||||
DrawTextureRect(m_RobotTexture, rect, false, new(1, 1, 1, opacity));
|
||||
return;
|
||||
}
|
||||
|
||||
DrawRect(rect, WithOpacity(Colors.White, opacity));
|
||||
DrawString(ThemeDB.FallbackFont, rect.GetCenter() + new Vector2(-rect.Size.X * 0.28f, rect.Size.Y * 0.08f), "BOT", HorizontalAlignment.Center, rect.Size.X * 0.56f, (int)Math.Max(10, rect.Size.Y * 0.25f), c_BackgroundColor);
|
||||
}
|
||||
|
||||
private void DrawGridOverlays(SGridLayout layout)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return;
|
||||
|
||||
foreach (var position in AllPositions())
|
||||
DrawRect(layout.CellRect(position), c_GridLineColor, false, 1);
|
||||
|
||||
if (IsValidCell(HoveredCell) && HoveredCell != SelectedCell)
|
||||
DrawRect(layout.CellRect(ToGridPosition(HoveredCell)), c_HoverColor, false, 2);
|
||||
|
||||
if (IsValidCell(SelectedCell))
|
||||
DrawRect(layout.CellRect(ToGridPosition(SelectedCell)), c_SelectedColor, false, 3);
|
||||
}
|
||||
|
||||
private int GetDualTileFloorMask(int x, int y)
|
||||
{
|
||||
var mask = 0;
|
||||
if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
|
||||
mask |= c_TopLeftCorner;
|
||||
|
||||
if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
|
||||
mask |= c_TopRightCorner;
|
||||
|
||||
if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
|
||||
mask |= c_BottomLeftCorner;
|
||||
|
||||
if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
|
||||
mask |= c_BottomRightCorner;
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
private ECellTerrain GetTerrainOrWall(int x, int y)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return ECellTerrain.Wall;
|
||||
|
||||
var position = new GridPosition(x, y);
|
||||
return m_LevelState.InBounds(position) ? m_LevelState.GetTerrain(position) : ECellTerrain.Wall;
|
||||
}
|
||||
|
||||
private bool IsWall(GridPosition position)
|
||||
{
|
||||
return m_LevelState is not null && m_LevelState.InBounds(position) && m_LevelState.GetTerrain(position) == ECellTerrain.Wall;
|
||||
}
|
||||
|
||||
private Rect2 TilemapSourceRect(int wallMask)
|
||||
{
|
||||
var tilePosition = wallMask switch {
|
||||
c_BottomLeftCorner => new(0, 0),
|
||||
c_TopRightCorner | c_BottomRightCorner => new(1, 0),
|
||||
c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
|
||||
c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
|
||||
c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
|
||||
c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
|
||||
c_AllCorners => new(2, 1),
|
||||
c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
|
||||
c_TopRightCorner => new(0, 2),
|
||||
c_TopLeftCorner | c_TopRightCorner => new(1, 2),
|
||||
c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
|
||||
c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
|
||||
0 => new(0, 3),
|
||||
c_BottomRightCorner => new(1, 3),
|
||||
c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
|
||||
c_TopLeftCorner => new(3, 3),
|
||||
_ => Vector2I.Zero
|
||||
};
|
||||
|
||||
return new(tilePosition * c_TilemapTileSize, new Vector2I(c_TilemapTileSize, c_TilemapTileSize));
|
||||
}
|
||||
|
||||
private SGridLayout GetLayout()
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return new(TileSize * Zoom, Vector2.Zero);
|
||||
|
||||
var cellSize = TileSize * Zoom;
|
||||
var contentSize = new Vector2(m_LevelState.Width * cellSize, m_LevelState.Height * cellSize);
|
||||
var origin = ((Size - contentSize) / 2) + PanOffset;
|
||||
return new(cellSize, origin);
|
||||
}
|
||||
|
||||
private Vector2I? ScreenToCell(Vector2 point)
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
return null;
|
||||
|
||||
var layout = GetLayout();
|
||||
var x = Mathf.FloorToInt((point.X - layout.Origin.X) / layout.CellSize);
|
||||
var y = Mathf.FloorToInt((point.Y - layout.Origin.Y) / layout.CellSize);
|
||||
var cell = new Vector2I(x, y);
|
||||
return IsValidCell(cell) ? cell : null;
|
||||
}
|
||||
|
||||
private void ZoomAt(Vector2 point, float factor)
|
||||
{
|
||||
var oldLayout = GetLayout();
|
||||
var cell = (point - oldLayout.Origin) / oldLayout.CellSize;
|
||||
m_Zoom = Math.Clamp(m_Zoom * factor, c_MinZoom, c_MaxZoom);
|
||||
var newCellSize = TileSize * m_Zoom;
|
||||
var contentSize = m_LevelState is null ? Vector2.Zero : new(m_LevelState.Width * newCellSize, m_LevelState.Height * newCellSize);
|
||||
var centeredOrigin = (Size - contentSize) / 2;
|
||||
m_PanOffset = point - centeredOrigin - (cell * newCellSize);
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
private void SetHoveredCell(Vector2I? cell)
|
||||
{
|
||||
var next = cell ?? c_InvalidCell;
|
||||
if (next == HoveredCell)
|
||||
return;
|
||||
|
||||
HoveredCell = next;
|
||||
EmitSignal(SignalName.OnCellHovered, HoveredCell);
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
private bool IsLayerVisible(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => ShowFuelLayer,
|
||||
ECarrierType.Coolant => ShowCoolantLayer,
|
||||
ECarrierType.Electricity => ShowElectricityLayer,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private float SurfaceOpacity()
|
||||
{
|
||||
return ActiveUndergroundLayer is null ? 1.0f : 0.5f;
|
||||
}
|
||||
|
||||
private float UndergroundOpacity(ECarrierType carrier)
|
||||
{
|
||||
if (ActiveUndergroundLayer is null)
|
||||
return 0.25f;
|
||||
|
||||
return ActiveUndergroundLayer == carrier ? 1.0f : 0.25f;
|
||||
}
|
||||
|
||||
private IEnumerable<GridPosition> AllPositions()
|
||||
{
|
||||
if (m_LevelState is null)
|
||||
yield break;
|
||||
|
||||
for (var y = 0; y < m_LevelState.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < m_LevelState.Width; x++)
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidCell(Vector2I cell)
|
||||
{
|
||||
return m_LevelState is not null && cell.X >= 0 && cell.Y >= 0 && cell.X < m_LevelState.Width && cell.Y < m_LevelState.Height;
|
||||
}
|
||||
|
||||
private static Vector2I ToVector(GridPosition position)
|
||||
{
|
||||
return new(position.X, position.Y);
|
||||
}
|
||||
|
||||
private static GridPosition ToGridPosition(Vector2I cell)
|
||||
{
|
||||
return new(cell.X, cell.Y);
|
||||
}
|
||||
|
||||
private static Rect2 Inset(Rect2 rect, float fraction)
|
||||
{
|
||||
var inset = rect.Size.X * fraction;
|
||||
return new(rect.Position + new Vector2(inset, inset), rect.Size - new Vector2(inset * 2, inset * 2));
|
||||
}
|
||||
|
||||
private static float SurfaceOverlayOpacity(float amount, float caution, float critical)
|
||||
{
|
||||
if (amount < caution)
|
||||
return 0;
|
||||
|
||||
if (amount >= critical)
|
||||
return 0.9f;
|
||||
|
||||
var cautionRange = Math.Max(0.001f, critical - caution);
|
||||
var t = (amount - caution) / cautionRange;
|
||||
return 0.3f + (t * 0.35f);
|
||||
}
|
||||
|
||||
private static Color WithOpacity(Color color, float opacity)
|
||||
{
|
||||
return new(color.R, color.G, color.B, color.A * Math.Clamp(opacity, 0, 1));
|
||||
}
|
||||
|
||||
private static Color CarrierColor(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => c_FuelColor,
|
||||
ECarrierType.Coolant => c_CoolantColor,
|
||||
ECarrierType.Electricity => c_ElectricityColor,
|
||||
_ => Colors.White
|
||||
};
|
||||
}
|
||||
|
||||
private static Color PropColor(PropState prop)
|
||||
{
|
||||
return prop.Type switch {
|
||||
EPropType.Flow => CarrierColor(prop.Carrier),
|
||||
EPropType.Consumer => new(0.36f, 0.48f, 0.67f),
|
||||
EPropType.Junction => new(0.56f, 0.44f, 0.70f),
|
||||
EPropType.AllSeeingEyeTerminal => new(0.33f, 0.59f, 0.61f),
|
||||
EPropType.RemedySupply => new(0.30f, 0.57f, 0.34f),
|
||||
EPropType.ReactorControl => new(0.69f, 0.28f, 0.29f),
|
||||
_ => Colors.Gray
|
||||
};
|
||||
}
|
||||
|
||||
private static string PropLabel(PropState prop)
|
||||
{
|
||||
return prop.Type switch {
|
||||
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
|
||||
EPropType.Consumer => "CON",
|
||||
EPropType.Junction => $"J {prop.JunctionMode}",
|
||||
EPropType.AllSeeingEyeTerminal => "EYE",
|
||||
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
|
||||
EPropType.ReactorControl => "REACT",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string CarrierShort(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => "F",
|
||||
ECarrierType.Coolant => "C",
|
||||
ECarrierType.Electricity => "E",
|
||||
_ => "?"
|
||||
};
|
||||
}
|
||||
|
||||
private static string RemedyShort(ERemedyType remedy)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => "F REM",
|
||||
ERemedyType.CoolantNeutralizer => "C REM",
|
||||
ERemedyType.ElectricityNeutralizer => "E REM",
|
||||
ERemedyType.HeatShield => "H SHD",
|
||||
_ => "REM"
|
||||
};
|
||||
}
|
||||
|
||||
public int TileSize
|
||||
{
|
||||
get => m_TileSize;
|
||||
set
|
||||
{
|
||||
m_TileSize = Math.Max(12, value);
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public float Zoom
|
||||
{
|
||||
get => m_Zoom;
|
||||
set
|
||||
{
|
||||
m_Zoom = Math.Clamp(value, c_MinZoom, c_MaxZoom);
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 PanOffset
|
||||
{
|
||||
get => m_PanOffset;
|
||||
set
|
||||
{
|
||||
m_PanOffset = value;
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2I SelectedCell { get; set; } = c_InvalidCell;
|
||||
public Vector2I HoveredCell { get; private set; } = c_InvalidCell;
|
||||
public Vector2I RobotPosition { get; private set; } = c_InvalidCell;
|
||||
public bool ShowFuelLayer { get; set; } = true;
|
||||
public bool ShowCoolantLayer { get; set; } = true;
|
||||
public bool ShowElectricityLayer { get; set; } = true;
|
||||
public ECarrierType? ActiveUndergroundLayer { get; set; }
|
||||
|
||||
[Signal]
|
||||
public delegate void OnCellHoveredEventHandler(Vector2I cell);
|
||||
|
||||
[Signal]
|
||||
public delegate void OnCellSelectedEventHandler(Vector2I cell);
|
||||
|
||||
[Signal]
|
||||
public delegate void OnGridClickedEventHandler(Vector2I cell);
|
||||
|
||||
private const float c_MinZoom = 0.5f;
|
||||
private const float c_MaxZoom = 3.0f;
|
||||
private const int c_TilemapTileSize = 512;
|
||||
private const int c_TopLeftCorner = 1;
|
||||
private const int c_TopRightCorner = 2;
|
||||
private const int c_BottomLeftCorner = 4;
|
||||
private const int c_BottomRightCorner = 8;
|
||||
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
|
||||
private static readonly Vector2I c_InvalidCell = new(-1, -1);
|
||||
private static readonly Color c_BackgroundColor = new(0.06f, 0.07f, 0.08f);
|
||||
private static readonly Color c_CoolantColor = new(0.20f, 0.78f, 0.92f);
|
||||
private static readonly Color c_DisabledColor = new(0.32f, 0.34f, 0.35f);
|
||||
private static readonly Color c_ElectricityColor = new(0.96f, 0.78f, 0.20f);
|
||||
private static readonly Color c_FuelColor = new(0.86f, 0.20f, 0.18f);
|
||||
private static readonly Color c_GridLineColor = new(0.36f, 0.41f, 0.45f, 0.35f);
|
||||
private static readonly Color c_HeatColor = new(1.0f, 0.42f, 0.12f);
|
||||
private static readonly Color c_HoverColor = new(0.60f, 0.74f, 0.86f, 0.72f);
|
||||
private static readonly Color c_LeakColor = new(1.0f, 0.27f, 0.16f);
|
||||
private static readonly Color c_ReachableColor = new(0.78f, 0.96f, 0.84f, 0.20f);
|
||||
private static readonly Color c_ReadyColor = new(0.46f, 0.95f, 0.52f);
|
||||
private static readonly Color c_SelectedColor = new(1.0f, 1.0f, 1.0f, 0.95f);
|
||||
|
||||
private bool m_IsPanning;
|
||||
private LevelState? m_LevelState;
|
||||
private Vector2 m_PanOffset;
|
||||
private Texture2D? m_RobotTexture;
|
||||
private Texture2D? m_TerrainTilemap;
|
||||
private int m_TileSize = 48;
|
||||
private float m_Zoom = 1;
|
||||
}
|
||||
@@ -18,15 +18,16 @@ public sealed class GameSession
|
||||
|
||||
public bool MoveRobot(GridPosition destination)
|
||||
{
|
||||
if (LevelState.Terrain[(destination.Y * LevelState.Width) + destination.X] == ECellTerrain.Wall)
|
||||
return false;
|
||||
|
||||
if (destination.X < 0 || destination.X >= LevelState.Width ||
|
||||
destination.Y < 0 || destination.Y >= LevelState.Height)
|
||||
if (!LevelState.InBounds(destination) || !LevelState.IsFloor(destination) || LevelState.Robot.Position.ManhattanDistance(destination) != 1)
|
||||
return false;
|
||||
|
||||
var previousPosition = LevelState.Robot.Position;
|
||||
LevelState = m_Engine.MoveRobot(LevelState, destination);
|
||||
if (LevelState.Robot.Position == previousPosition)
|
||||
return false;
|
||||
|
||||
RobotMoved?.Invoke(this);
|
||||
LevelStateChanged?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,13 @@ public partial class LevelScreen : ScreenBase
|
||||
m_App = app;
|
||||
m_Session = session;
|
||||
m_Level = level;
|
||||
m_LevelCount = levelCount;
|
||||
m_LevelNumber = levelNumber;
|
||||
m_OutcomeVisible = false;
|
||||
|
||||
var body = CreatePage(string.Empty);
|
||||
var header = new LevelHeader();
|
||||
m_Header = header;
|
||||
body.AddChild(header);
|
||||
header.SetLevel(level.Name, levelNumber, levelCount, session.LevelState.Global.LevelState);
|
||||
|
||||
@@ -26,8 +29,15 @@ public partial class LevelScreen : ScreenBase
|
||||
var split = new HSplitContainer { SizeFlagsVertical = SizeFlags.ExpandFill };
|
||||
body.AddChild(split);
|
||||
|
||||
m_GridPanel = CreateGridContainer();
|
||||
split.AddChild(m_GridPanel);
|
||||
var gridColumn = new VBoxContainer {
|
||||
CustomMinimumSize = new(560, 360),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill
|
||||
};
|
||||
m_GridViewport = CreateGridViewport();
|
||||
gridColumn.AddChild(m_GridViewport);
|
||||
gridColumn.AddChild(CreateLayerControls());
|
||||
split.AddChild(gridColumn);
|
||||
|
||||
m_Inspector = new();
|
||||
m_ForecastList = new();
|
||||
@@ -45,16 +55,74 @@ public partial class LevelScreen : ScreenBase
|
||||
|
||||
UpdateUI();
|
||||
m_Session.LevelStateChanged += OnLevelStateChanged;
|
||||
m_Session.RobotMoved += OnRobotMoved;
|
||||
m_Session.TurnAdvanced += OnTurnAdvanced;
|
||||
}
|
||||
|
||||
private PanelContainer CreateGridContainer()
|
||||
private GridViewport CreateGridViewport()
|
||||
{
|
||||
return new() {
|
||||
var viewport = new GridViewport {
|
||||
CustomMinimumSize = new(560, 360),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill
|
||||
};
|
||||
viewport.SetLevelState(m_Session!.LevelState);
|
||||
viewport.OnCellSelected += OnCellSelected;
|
||||
viewport.OnCellHovered += OnCellHovered;
|
||||
viewport.OnGridClicked += OnGridClicked;
|
||||
return viewport;
|
||||
}
|
||||
|
||||
private Control CreateLayerControls()
|
||||
{
|
||||
var controls = new HBoxContainer();
|
||||
controls.AddChild(CreateLayerToggle("Fuel", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowFuelLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
controls.AddChild(CreateLayerToggle("Coolant", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowCoolantLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
controls.AddChild(CreateLayerToggle("Electricity", true, pressed => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ShowElectricityLayer = pressed;
|
||||
m_GridViewport.QueueRedraw();
|
||||
}));
|
||||
|
||||
var activeLayer = new OptionButton();
|
||||
activeLayer.AddItem("Surface", 0);
|
||||
activeLayer.AddItem("Fuel", 1);
|
||||
activeLayer.AddItem("Coolant", 2);
|
||||
activeLayer.AddItem("Electricity", 3);
|
||||
activeLayer.ItemSelected += index => {
|
||||
if (m_GridViewport is null) return;
|
||||
|
||||
m_GridViewport.ActiveUndergroundLayer = index switch {
|
||||
1 => ECarrierType.Fuel,
|
||||
2 => ECarrierType.Coolant,
|
||||
3 => ECarrierType.Electricity,
|
||||
_ => null
|
||||
};
|
||||
m_GridViewport.QueueRedraw();
|
||||
};
|
||||
controls.AddChild(activeLayer);
|
||||
return controls;
|
||||
}
|
||||
|
||||
private static CheckBox CreateLayerToggle(string text, bool pressed, Action<bool> toggled)
|
||||
{
|
||||
var toggle = new CheckBox {
|
||||
Text = text,
|
||||
ButtonPressed = pressed
|
||||
};
|
||||
toggle.Toggled += pressedState => toggled(pressedState);
|
||||
return toggle;
|
||||
}
|
||||
|
||||
private HBoxContainer CreateActionBar()
|
||||
@@ -111,20 +179,9 @@ public partial class LevelScreen : ScreenBase
|
||||
return;
|
||||
|
||||
var ls = m_Session.LevelState;
|
||||
var robotPos = m_Session.RobotPosition;
|
||||
var index = (robotPos.Y * ls.Width) + robotPos.X;
|
||||
var surface = ls.Surface[index];
|
||||
var prop = index < ls.Props.Length ? ls.Props[index] : new();
|
||||
|
||||
m_Inspector.SetCellInfo(
|
||||
robotPos,
|
||||
ls.Terrain[index],
|
||||
prop.Type,
|
||||
prop.ServiceState,
|
||||
surface.Fuel,
|
||||
surface.Coolant,
|
||||
surface.Electricity,
|
||||
surface.Heat);
|
||||
m_Header?.SetLevel(m_Level?.Name ?? ls.Name, m_LevelNumber, m_LevelCount, ls.Global.LevelState);
|
||||
m_GridViewport?.SetLevelState(ls);
|
||||
UpdateInspector(m_GridViewport?.SelectedCell ?? new Vector2I(ls.Robot.Position.X, ls.Robot.Position.Y));
|
||||
|
||||
m_ForecastList.SetForecasts(ls.Forecasts);
|
||||
|
||||
@@ -135,11 +192,42 @@ public partial class LevelScreen : ScreenBase
|
||||
m_Session.LevelState.Robot.HeatShields);
|
||||
}
|
||||
|
||||
private void UpdateInspector(Vector2I cell)
|
||||
{
|
||||
if (m_Session is null || m_Inspector is null)
|
||||
return;
|
||||
|
||||
var ls = m_Session.LevelState;
|
||||
var position = new GridPosition(cell.X, cell.Y);
|
||||
if (!ls.InBounds(position))
|
||||
{
|
||||
m_Inspector.SetCellInfo(null, ECellTerrain.Wall, EPropType.None, EConsumerServiceState.Unknown, 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var surface = ls.GetSurface(position);
|
||||
var prop = ls.GetProp(position);
|
||||
m_Inspector.SetCellInfo(
|
||||
position,
|
||||
ls.GetTerrain(position),
|
||||
prop.Type,
|
||||
prop.ServiceState,
|
||||
surface.Fuel,
|
||||
surface.Coolant,
|
||||
surface.Electricity,
|
||||
surface.Heat);
|
||||
}
|
||||
|
||||
private void OnLevelStateChanged(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnRobotMoved(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnTurnAdvanced(GameSession sender)
|
||||
{
|
||||
UpdateUI();
|
||||
@@ -176,12 +264,36 @@ public partial class LevelScreen : ScreenBase
|
||||
m_OverlayLayer.AddChild(center);
|
||||
}
|
||||
|
||||
private void OnCellSelected(Vector2I cell)
|
||||
{
|
||||
UpdateInspector(cell);
|
||||
}
|
||||
|
||||
private void OnCellHovered(Vector2I cell)
|
||||
{
|
||||
if (cell.X >= 0 && cell.Y >= 0)
|
||||
UpdateInspector(cell);
|
||||
}
|
||||
|
||||
private void OnGridClicked(Vector2I cell)
|
||||
{
|
||||
if (m_Session is null)
|
||||
return;
|
||||
|
||||
var destination = new GridPosition(cell.X, cell.Y);
|
||||
if (m_Session.RobotPosition.ManhattanDistance(destination) == 1)
|
||||
m_Session.MoveRobot(destination);
|
||||
}
|
||||
|
||||
private AppController? m_App;
|
||||
private ForecastList? m_ForecastList;
|
||||
private PanelContainer? m_GridPanel;
|
||||
private GridViewport? m_GridViewport;
|
||||
private LevelHeader? m_Header;
|
||||
private CellInspector? m_Inspector;
|
||||
private InventoryStrip? m_InventoryStrip;
|
||||
private CampaignLevel? m_Level;
|
||||
private int m_LevelCount;
|
||||
private int m_LevelNumber;
|
||||
private bool m_OutcomeVisible;
|
||||
private CanvasLayer? m_OverlayLayer;
|
||||
private GameSession? m_Session;
|
||||
|
||||
Reference in New Issue
Block a user