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 Label _hudLabel = null!;
|
||||
private PanelContainer _hudPanel = null!;
|
||||
private Label _cursorDebugLabel = null!;
|
||||
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
|
||||
// middle/right-drag pan independently).
|
||||
@@ -252,6 +259,7 @@ public partial class PlayScreen : Control
|
||||
}
|
||||
|
||||
UpdateHud(tactical);
|
||||
UpdateCursorDebug(tactical);
|
||||
}
|
||||
|
||||
public override void _UnhandledInput(InputEvent @event)
|
||||
@@ -572,6 +580,34 @@ public partial class PlayScreen : Control
|
||||
};
|
||||
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
|
||||
// layer. Hidden by default; FlashSavedToast pops it in.
|
||||
_saveFlashLabel = new Label
|
||||
@@ -589,6 +625,121 @@ public partial class PlayScreen : Control
|
||||
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)
|
||||
{
|
||||
var p = _actors.Player!;
|
||||
|
||||
Reference in New Issue
Block a user