M7.4a: PlayScreen polish — facing tick, road overlap, WASD pan

Facing tick stuck at the initial angle. PlayerMarker._Draw was
computing the tick direction from a FacingAngleRad auto-property,
but Godot caches CanvasItem draw commands and only re-runs _Draw on
QueueRedraw. Setter never called QueueRedraw → tick never rotated.
Fixed by leaning on the Node2D transform instead: tick is drawn
along the local +X axis, PlayScreen sets marker.Rotation = facing
each frame. The transform rotation applies to the cached commands
without re-invoking _Draw — efficient and correct. FacingAngleRad
property removed; ShowFacingTick became a property with QueueRedraw
on change (visibility toggle still needs to invalidate the cache).

Tactical view double-drew roads. TacticalChunkGen.Pass2_Polylines
already bakes roads + rivers + bridges into the surface tiles of
each chunk. WorldRenderNode's Line2D overlay was still visible at
tactical zoom, stroking the same path on top of the rasterised
version — showed as a brown line over every road. Ported the
MonoGame "suppress polyline overlay in tactical" rule into
UpdateLayerVisibility: _polylineLayer and _bridgeLayer hide when
zoom >= TacticalRenderZoomMin.

WASD now pans the world map. Previously WASD did nothing in
world-map mode — only right-drag / middle-drag / mouse-wheel worked.
WASD is now context-sensitive: tactical mode steps the player
(unchanged), world-map mode pans the camera at 400 screen px/sec
(world-pixel speed scales as 1/zoom so the perceived rate stays
constant). Diagonal motion is √2-normalised to match tactical step.
Suppressed during click-to-travel since the camera-follow would
clobber any pan input anyway. HUD hint updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-05-10 19:51:44 -07:00
parent 116193c1e3
commit 6f47700820
3 changed files with 69 additions and 31 deletions
+26 -16
View File
@@ -7,36 +7,46 @@ namespace Theriapolis.GodotHost.Rendering;
/// Player marker — small dot with a thin facing tick. Drawn at
/// <see cref="C.PLAYER_MARKER_SCREEN_PX"/>/2 wp; the owner sets
/// <see cref="Node2D.Scale"/> = 1/zoom every frame so the on-screen size
/// stays constant across the seamless zoom range.
///
/// Mirrors the MonoGame PlayerSprite's visual vocabulary — dark outline,
/// faction-red fill, optional facing tick. Phase-7-styled `PlayerSprite`
/// proper (walking animation frames) lands later.
/// stays constant across the seamless zoom range. Facing is driven by
/// the owner via <see cref="Node2D.Rotation"/>; the tick is drawn along
/// the local +X axis so rotating the node rotates the tick without
/// invalidating the cached <c>_Draw</c> commands.
/// </summary>
public partial class PlayerMarker : Node2D
{
private const float RadiusWorldPx = C.PLAYER_MARKER_SCREEN_PX * 0.5f;
private const float FacingTickPx = RadiusWorldPx * 1.4f;
/// <summary>Facing direction in radians; 0 = +X. Drives the optional
/// tick rendered on the body's leading edge.</summary>
public float FacingAngleRad { get; set; }
private bool _showFacingTick = true;
/// <summary>When true, draws a small tick at the leading edge so the
/// player can read facing without a full sprite. Hidden at low zoom
/// to avoid clutter.</summary>
public bool ShowFacingTick { get; set; } = true;
/// <summary>When true, draws a small tick along the local +X axis
/// so the player can read facing without a full sprite. Hidden at
/// low zoom to avoid clutter. Triggers <see cref="CanvasItem.QueueRedraw"/>
/// on change.</summary>
public bool ShowFacingTick
{
get => _showFacingTick;
set
{
if (_showFacingTick == value) return;
_showFacingTick = value;
QueueRedraw();
}
}
public override void _Draw()
{
DrawCircle(Vector2.Zero, RadiusWorldPx, new Color(0, 0, 0, 0.78f));
DrawCircle(Vector2.Zero, RadiusWorldPx * 0.85f, new Color(0.86f, 0.31f, 0.24f));
if (ShowFacingTick)
if (_showFacingTick)
{
var dir = new Vector2(Mathf.Cos(FacingAngleRad), Mathf.Sin(FacingAngleRad));
DrawLine(dir * (RadiusWorldPx * 0.4f), dir * FacingTickPx,
new Color(1f, 0.96f, 0.86f), width: RadiusWorldPx * 0.18f, antialiased: false);
DrawLine(
new Vector2(RadiusWorldPx * 0.4f, 0),
new Vector2(FacingTickPx, 0),
new Color(1f, 0.96f, 0.86f),
width: RadiusWorldPx * 0.18f,
antialiased: false);
}
}
}
+13 -1
View File
@@ -288,10 +288,22 @@ public partial class WorldRenderNode : Node2D
{
if (_camera is null) return;
float zoom = _camera.Zoom.X;
bool tactical = zoom >= TacticalRenderZoomMin;
if (_tacticalLayer is not null)
_tacticalLayer.Visible = zoom >= TacticalRenderZoomMin;
_tacticalLayer.Visible = tactical;
if (_settlementLayer is not null)
_settlementLayer.Visible = zoom < SettlementHideZoom;
// Polylines and bridges are baked into the tactical chunk surface
// tiles by TacticalChunkGen.Pass2_Polylines, so re-stroking the
// Line2D overlay at tactical zoom double-draws the road and shows
// as a brown line over top of the rasterised one. Hide the line
// overlay when tactical is active.
if (_polylineLayer is not null)
_polylineLayer.Visible = !tactical;
if (_bridgeLayer is not null)
_bridgeLayer.Visible = !tactical;
}
private void UpdateZoomScaledNodes()