M7.4b: Top-right cursor-debug panel for play-testing
Mirror of the player-status panel, anchored top-right. Per-frame
sample of GetViewport().GetMousePosition() converted to world coords
via the camera transform; surfaces:
- World-pixel and world-tile coords under the cursor
- Biome at that tile
- Feature flag bitmask (HasRoad / HasRiver / HasRail / IsSettlement
/ IsPoi / IsCoast / RiverAdjacent / RailroadAdjacent)
- Settlement on the tile via WorldTile.SettlementId lookup —
name, tier, and economy/governance/pop (or PoI type)
- Bridge under cursor via point-on-segment hit test against
world.Bridges (6 px hit radius, ≤ a few dozen bridges so cheap)
- Tactical block when zoomed in: tactical-tile coords, surface +
variant, deco, walkability tag (walkable / slow / blocked)
- NPC under cursor within 12 px hit radius — display name, role
tag or template id, allegiance, HP
Cached StringBuilder field on PlayScreen (Clear() each frame instead
of new'ing) to keep per-frame GC pressure low. Held keys produce
auto-repeat InputEventKey instances that Godot 4 mono's GC must
collect before engine shutdown; reducing per-frame garbage buys
that collection more headroom and avoided a shutdown-assertion
race observed on the first launch with the panel mounted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,14 @@ public partial class PlayScreen : Control
|
|||||||
private readonly Dictionary<int, NpcMarker> _npcMarkers = new();
|
private readonly Dictionary<int, NpcMarker> _npcMarkers = new();
|
||||||
private Label _hudLabel = null!;
|
private Label _hudLabel = null!;
|
||||||
private PanelContainer _hudPanel = null!;
|
private PanelContainer _hudPanel = null!;
|
||||||
|
private Label _cursorDebugLabel = null!;
|
||||||
private Label? _saveFlashLabel;
|
private Label? _saveFlashLabel;
|
||||||
|
// Reused per-frame builders — avoid GC pressure on hot _Process path.
|
||||||
|
// Holding a key produces auto-repeat InputEventKey objects that the C#
|
||||||
|
// GC must release before engine shutdown asserts on empty bindings;
|
||||||
|
// reducing per-frame allocations buys headroom for those collections.
|
||||||
|
private readonly System.Text.StringBuilder _cursorSb = new(256);
|
||||||
|
private readonly System.Text.StringBuilder _hudSb = new(256);
|
||||||
|
|
||||||
// Click-vs-drag state (left-click only; PanZoomCamera handles
|
// Click-vs-drag state (left-click only; PanZoomCamera handles
|
||||||
// middle/right-drag pan independently).
|
// middle/right-drag pan independently).
|
||||||
@@ -252,6 +259,7 @@ public partial class PlayScreen : Control
|
|||||||
}
|
}
|
||||||
|
|
||||||
UpdateHud(tactical);
|
UpdateHud(tactical);
|
||||||
|
UpdateCursorDebug(tactical);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _UnhandledInput(InputEvent @event)
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
@@ -572,6 +580,34 @@ public partial class PlayScreen : Control
|
|||||||
};
|
};
|
||||||
margin.AddChild(_hudLabel);
|
margin.AddChild(_hudLabel);
|
||||||
|
|
||||||
|
// Cursor-debug panel — top-right counterpart to the player-status
|
||||||
|
// panel. Shows tile coords, biome, feature flags, settlement,
|
||||||
|
// tactical-tile surface/deco, and any NPC under the mouse.
|
||||||
|
var cursorPanel = new PanelContainer
|
||||||
|
{
|
||||||
|
ThemeTypeVariation = "Card",
|
||||||
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
|
AnchorLeft = 1, AnchorRight = 1,
|
||||||
|
OffsetLeft = -460, OffsetTop = 12, OffsetRight = -12, OffsetBottom = 260,
|
||||||
|
};
|
||||||
|
hudLayer.AddChild(cursorPanel);
|
||||||
|
|
||||||
|
var cursorMargin = new MarginContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||||
|
cursorMargin.AddThemeConstantOverride("margin_left", 12);
|
||||||
|
cursorMargin.AddThemeConstantOverride("margin_top", 8);
|
||||||
|
cursorMargin.AddThemeConstantOverride("margin_right", 12);
|
||||||
|
cursorMargin.AddThemeConstantOverride("margin_bottom", 8);
|
||||||
|
cursorPanel.AddChild(cursorMargin);
|
||||||
|
|
||||||
|
_cursorDebugLabel = new Label
|
||||||
|
{
|
||||||
|
Text = "CURSOR",
|
||||||
|
ThemeTypeVariation = "CardBody",
|
||||||
|
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||||
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
|
};
|
||||||
|
cursorMargin.AddChild(_cursorDebugLabel);
|
||||||
|
|
||||||
// Save-flash toast, mounted bottom-center on the same canvas
|
// Save-flash toast, mounted bottom-center on the same canvas
|
||||||
// layer. Hidden by default; FlashSavedToast pops it in.
|
// layer. Hidden by default; FlashSavedToast pops it in.
|
||||||
_saveFlashLabel = new Label
|
_saveFlashLabel = new Label
|
||||||
@@ -589,6 +625,121 @@ public partial class PlayScreen : Control
|
|||||||
hudLayer.AddChild(_saveFlashLabel);
|
hudLayer.AddChild(_saveFlashLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Top-right debug panel — what is under the mouse this
|
||||||
|
/// frame. World/tile coords, biome, feature flags, the settlement
|
||||||
|
/// whose footprint contains the tile, the tactical surface + deco
|
||||||
|
/// + walkability when zoomed in, and any NPC within hit radius.</summary>
|
||||||
|
private void UpdateCursorDebug(bool tactical)
|
||||||
|
{
|
||||||
|
var screenPos = GetViewport().GetMousePosition();
|
||||||
|
var worldPos = ScreenToWorld(screenPos);
|
||||||
|
int tx = (int)Mathf.Floor(worldPos.X / C.WORLD_TILE_PIXELS);
|
||||||
|
int ty = (int)Mathf.Floor(worldPos.Y / C.WORLD_TILE_PIXELS);
|
||||||
|
|
||||||
|
var sb = _cursorSb;
|
||||||
|
sb.Clear();
|
||||||
|
sb.Append("CURSOR world (").Append((int)worldPos.X).Append(", ")
|
||||||
|
.Append((int)worldPos.Y).Append(") tile (")
|
||||||
|
.Append(tx).Append(", ").Append(ty).Append(')').AppendLine();
|
||||||
|
|
||||||
|
if ((uint)tx < C.WORLD_WIDTH_TILES && (uint)ty < C.WORLD_HEIGHT_TILES)
|
||||||
|
{
|
||||||
|
ref var t = ref _ctx.World.TileAt(tx, ty);
|
||||||
|
sb.Append(" Biome: ").Append(t.Biome).AppendLine();
|
||||||
|
if (t.Features != FeatureFlags.None)
|
||||||
|
sb.Append(" Flags: ").Append(t.Features).AppendLine();
|
||||||
|
|
||||||
|
// Copy SettlementId out of the ref local before the lambda
|
||||||
|
// capture below — `ref var t` can't escape into a closure.
|
||||||
|
int settlementId = t.SettlementId;
|
||||||
|
if (settlementId != 0)
|
||||||
|
{
|
||||||
|
var settle = _ctx.World.Settlements.FirstOrDefault(s => s.Id == settlementId);
|
||||||
|
if (settle is not null)
|
||||||
|
{
|
||||||
|
sb.Append(" Settlement: ").Append(settle.Name)
|
||||||
|
.Append(" (Tier ").Append(settle.Tier).Append(')').AppendLine();
|
||||||
|
if (!settle.IsPoi)
|
||||||
|
sb.Append(" ").Append(settle.Economy)
|
||||||
|
.Append(" · ").Append(settle.Governance)
|
||||||
|
.Append(" · pop ").Append(settle.Population).AppendLine();
|
||||||
|
else if (settle.PoiType != PoiType.None)
|
||||||
|
sb.Append(" PoI: ").Append(settle.PoiType).AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tactical)
|
||||||
|
{
|
||||||
|
int tacticalX = (int)Mathf.Floor(worldPos.X);
|
||||||
|
int tacticalY = (int)Mathf.Floor(worldPos.Y);
|
||||||
|
var tt = _streamer.SampleTile(tacticalX, tacticalY);
|
||||||
|
string move = !tt.IsWalkable ? "blocked"
|
||||||
|
: tt.SlowsMovement ? "slow" : "walkable";
|
||||||
|
string deco = tt.Deco == TacticalDeco.None ? "—" : tt.Deco.ToString();
|
||||||
|
sb.Append(" Tactical (").Append(tacticalX).Append(", ").Append(tacticalY).Append(')').AppendLine();
|
||||||
|
sb.Append(" Surface: ").Append(tt.Surface)
|
||||||
|
.Append(" (v").Append(tt.Variant).Append(") Deco: ").Append(deco).AppendLine();
|
||||||
|
sb.Append(" Move: ").Append(move).AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(" <off-world>").AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bridge under cursor (point-on-segment test — cheap, ≤ a few dozen bridges).
|
||||||
|
const float BridgeHitPx = 6f;
|
||||||
|
foreach (var bridge in _ctx.World.Bridges)
|
||||||
|
{
|
||||||
|
if (DistancePointToSegmentSq(worldPos.X, worldPos.Y,
|
||||||
|
bridge.Start.X, bridge.Start.Y, bridge.End.X, bridge.End.Y) < BridgeHitPx * BridgeHitPx)
|
||||||
|
{
|
||||||
|
sb.Append(" Bridge over road ").Append(bridge.RoadId).AppendLine();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPC under cursor (within marker hit radius).
|
||||||
|
const float NpcHitPx = 12f;
|
||||||
|
float closestSq = NpcHitPx * NpcHitPx;
|
||||||
|
NpcActor? hovered = null;
|
||||||
|
foreach (var npc in _actors.Npcs)
|
||||||
|
{
|
||||||
|
float ddx = npc.Position.X - worldPos.X;
|
||||||
|
float ddy = npc.Position.Y - worldPos.Y;
|
||||||
|
float distSq = ddx * ddx + ddy * ddy;
|
||||||
|
if (distSq < closestSq) { closestSq = distSq; hovered = npc; }
|
||||||
|
}
|
||||||
|
if (hovered is not null)
|
||||||
|
{
|
||||||
|
string tag = !string.IsNullOrEmpty(hovered.RoleTag)
|
||||||
|
? hovered.RoleTag
|
||||||
|
: (hovered.Template?.Id ?? "<resident>");
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append("NPC: ").Append(hovered.DisplayName)
|
||||||
|
.Append(" [").Append(tag).Append(']').AppendLine();
|
||||||
|
sb.Append(" Allegiance: ").Append(hovered.Allegiance)
|
||||||
|
.Append(" HP ").Append(hovered.CurrentHp).Append('/').Append(hovered.MaxHp).AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cursorDebugLabel.Text = sb.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float DistancePointToSegmentSq(float px, float py,
|
||||||
|
float ax, float ay, float bx, float by)
|
||||||
|
{
|
||||||
|
float vx = bx - ax, vy = by - ay;
|
||||||
|
float wx = px - ax, wy = py - ay;
|
||||||
|
float c1 = vx * wx + vy * wy;
|
||||||
|
if (c1 <= 0f) return wx * wx + wy * wy;
|
||||||
|
float c2 = vx * vx + vy * vy;
|
||||||
|
if (c2 <= c1) { float ex = px - bx, ey = py - by; return ex * ex + ey * ey; }
|
||||||
|
float t = c1 / c2;
|
||||||
|
float qx = ax + t * vx, qy = ay + t * vy;
|
||||||
|
float dx = px - qx, dy = py - qy;
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateHud(bool tactical)
|
private void UpdateHud(bool tactical)
|
||||||
{
|
{
|
||||||
var p = _actors.Player!;
|
var p = _actors.Player!;
|
||||||
|
|||||||
Reference in New Issue
Block a user