diff --git a/Theriapolis.Godot/Rendering/PlayerMarker.cs b/Theriapolis.Godot/Rendering/PlayerMarker.cs
index b82e529..a981455 100644
--- a/Theriapolis.Godot/Rendering/PlayerMarker.cs
+++ b/Theriapolis.Godot/Rendering/PlayerMarker.cs
@@ -7,36 +7,46 @@ namespace Theriapolis.GodotHost.Rendering;
/// Player marker — small dot with a thin facing tick. Drawn at
/// /2 wp; the owner sets
/// = 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 ; the tick is drawn along
+/// the local +X axis so rotating the node rotates the tick without
+/// invalidating the cached _Draw commands.
///
public partial class PlayerMarker : Node2D
{
private const float RadiusWorldPx = C.PLAYER_MARKER_SCREEN_PX * 0.5f;
private const float FacingTickPx = RadiusWorldPx * 1.4f;
- /// Facing direction in radians; 0 = +X. Drives the optional
- /// tick rendered on the body's leading edge.
- public float FacingAngleRad { get; set; }
+ private bool _showFacingTick = true;
- /// 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.
- public bool ShowFacingTick { get; set; } = true;
+ /// 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
+ /// on change.
+ 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);
}
}
}
diff --git a/Theriapolis.Godot/Rendering/WorldRenderNode.cs b/Theriapolis.Godot/Rendering/WorldRenderNode.cs
index d4456cf..04b785c 100644
--- a/Theriapolis.Godot/Rendering/WorldRenderNode.cs
+++ b/Theriapolis.Godot/Rendering/WorldRenderNode.cs
@@ -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()
diff --git a/Theriapolis.Godot/Scenes/PlayScreen.cs b/Theriapolis.Godot/Scenes/PlayScreen.cs
index b07397d..2105abd 100644
--- a/Theriapolis.Godot/Scenes/PlayScreen.cs
+++ b/Theriapolis.Godot/Scenes/PlayScreen.cs
@@ -152,7 +152,7 @@ public partial class PlayScreen : Control
_playerMarker = new PlayerMarker
{
Position = new Vector2(_actors.Player!.Position.X, _actors.Player.Position.Y),
- FacingAngleRad = _actors.Player.FacingAngleRad,
+ Rotation = _actors.Player.FacingAngleRad,
};
AddChild(_playerMarker);
@@ -182,23 +182,39 @@ public partial class PlayScreen : Control
bool tactical = _render.Camera.Zoom.X >= WorldRenderNode.TacticalRenderZoomMin;
- // Tactical WASD direction (world-map mode ignores keys — middle-drag
- // pans, click-to-travel sets the destination).
- float dx = 0f, dy = 0f;
- if (tactical)
+ // WASD is context-sensitive: tactical mode steps the player,
+ // world-map mode pans the camera. Same keys, intent depends on zoom.
+ float wasdX = 0f, wasdY = 0f;
+ if (Godot.Input.IsKeyPressed(Key.W) || Godot.Input.IsKeyPressed(Key.Up)) wasdY -= 1f;
+ if (Godot.Input.IsKeyPressed(Key.S) || Godot.Input.IsKeyPressed(Key.Down)) wasdY += 1f;
+ if (Godot.Input.IsKeyPressed(Key.A) || Godot.Input.IsKeyPressed(Key.Left)) wasdX -= 1f;
+ if (Godot.Input.IsKeyPressed(Key.D) || Godot.Input.IsKeyPressed(Key.Right)) wasdX += 1f;
+
+ // Controller always ticks (path-follow runs even when WASD is idle).
+ // Pass step input only in tactical mode.
+ float stepX = tactical ? wasdX : 0f;
+ float stepY = tactical ? wasdY : 0f;
+ _controller.Update(dt, stepX, stepY, tactical, isFocused: true);
+
+ // World-map WASD pan. Skip while traveling — the follow logic below
+ // re-centres the camera on the player and would clobber the pan.
+ // Speed scales inversely with zoom so the on-screen pan rate feels
+ // consistent at any zoom level (matches MonoGame's 400 px/sec).
+ if (!tactical && !_controller.IsTraveling && (wasdX != 0f || wasdY != 0f))
{
- if (Godot.Input.IsKeyPressed(Key.W) || Godot.Input.IsKeyPressed(Key.Up)) dy -= 1f;
- if (Godot.Input.IsKeyPressed(Key.S) || Godot.Input.IsKeyPressed(Key.Down)) dy += 1f;
- if (Godot.Input.IsKeyPressed(Key.A) || Godot.Input.IsKeyPressed(Key.Left)) dx -= 1f;
- if (Godot.Input.IsKeyPressed(Key.D) || Godot.Input.IsKeyPressed(Key.Right)) dx += 1f;
+ const float PanScreenPxPerSec = 400f;
+ float invLen = (wasdX != 0f && wasdY != 0f) ? 0.70710678f : 1f;
+ float panSpeed = PanScreenPxPerSec / Mathf.Max(_render.Camera.Zoom.X, 0.01f);
+ _render.Camera.Position += new Vector2(wasdX * invLen, wasdY * invLen) * panSpeed * dt;
}
- _controller.Update(dt, dx, dy, tactical, isFocused: true);
-
- // Sync the player marker from Core state.
+ // Sync the player marker from Core state. Rotation drives the
+ // facing tick via the transform — auto-property setters on a
+ // PlayerMarker field would skip QueueRedraw and the cached
+ // _Draw commands would stay stuck at the initial angle.
var p = _actors.Player;
_playerMarker.Position = new Vector2(p.Position.X, p.Position.Y);
- _playerMarker.FacingAngleRad = p.FacingAngleRad;
+ _playerMarker.Rotation = p.FacingAngleRad;
// Camera follow when traveling or in tactical (matches MonoGame).
if (_controller.IsTraveling || tactical)
@@ -591,7 +607,7 @@ public partial class PlayScreen : Control
string viewBlock = tactical
? "View: Tactical (WASD to step)"
- : "View: World Map (click a tile to travel)";
+ : "View: World Map (WASD to pan · click a tile to travel)";
string status = _controller.IsTraveling
? "Traveling…"