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:
@@ -7,36 +7,46 @@ namespace Theriapolis.GodotHost.Rendering;
|
|||||||
/// Player marker — small dot with a thin facing tick. Drawn at
|
/// 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="C.PLAYER_MARKER_SCREEN_PX"/>/2 wp; the owner sets
|
||||||
/// <see cref="Node2D.Scale"/> = 1/zoom every frame so the on-screen size
|
/// <see cref="Node2D.Scale"/> = 1/zoom every frame so the on-screen size
|
||||||
/// stays constant across the seamless zoom range.
|
/// stays constant across the seamless zoom range. Facing is driven by
|
||||||
///
|
/// the owner via <see cref="Node2D.Rotation"/>; the tick is drawn along
|
||||||
/// Mirrors the MonoGame PlayerSprite's visual vocabulary — dark outline,
|
/// the local +X axis so rotating the node rotates the tick without
|
||||||
/// faction-red fill, optional facing tick. Phase-7-styled `PlayerSprite`
|
/// invalidating the cached <c>_Draw</c> commands.
|
||||||
/// proper (walking animation frames) lands later.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class PlayerMarker : Node2D
|
public partial class PlayerMarker : Node2D
|
||||||
{
|
{
|
||||||
private const float RadiusWorldPx = C.PLAYER_MARKER_SCREEN_PX * 0.5f;
|
private const float RadiusWorldPx = C.PLAYER_MARKER_SCREEN_PX * 0.5f;
|
||||||
private const float FacingTickPx = RadiusWorldPx * 1.4f;
|
private const float FacingTickPx = RadiusWorldPx * 1.4f;
|
||||||
|
|
||||||
/// <summary>Facing direction in radians; 0 = +X. Drives the optional
|
private bool _showFacingTick = true;
|
||||||
/// tick rendered on the body's leading edge.</summary>
|
|
||||||
public float FacingAngleRad { get; set; }
|
|
||||||
|
|
||||||
/// <summary>When true, draws a small tick at the leading edge so the
|
/// <summary>When true, draws a small tick along the local +X axis
|
||||||
/// player can read facing without a full sprite. Hidden at low zoom
|
/// so the player can read facing without a full sprite. Hidden at
|
||||||
/// to avoid clutter.</summary>
|
/// low zoom to avoid clutter. Triggers <see cref="CanvasItem.QueueRedraw"/>
|
||||||
public bool ShowFacingTick { get; set; } = true;
|
/// on change.</summary>
|
||||||
|
public bool ShowFacingTick
|
||||||
|
{
|
||||||
|
get => _showFacingTick;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_showFacingTick == value) return;
|
||||||
|
_showFacingTick = value;
|
||||||
|
QueueRedraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void _Draw()
|
public override void _Draw()
|
||||||
{
|
{
|
||||||
DrawCircle(Vector2.Zero, RadiusWorldPx, new Color(0, 0, 0, 0.78f));
|
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));
|
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(
|
||||||
DrawLine(dir * (RadiusWorldPx * 0.4f), dir * FacingTickPx,
|
new Vector2(RadiusWorldPx * 0.4f, 0),
|
||||||
new Color(1f, 0.96f, 0.86f), width: RadiusWorldPx * 0.18f, antialiased: false);
|
new Vector2(FacingTickPx, 0),
|
||||||
|
new Color(1f, 0.96f, 0.86f),
|
||||||
|
width: RadiusWorldPx * 0.18f,
|
||||||
|
antialiased: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,10 +288,22 @@ public partial class WorldRenderNode : Node2D
|
|||||||
{
|
{
|
||||||
if (_camera is null) return;
|
if (_camera is null) return;
|
||||||
float zoom = _camera.Zoom.X;
|
float zoom = _camera.Zoom.X;
|
||||||
|
bool tactical = zoom >= TacticalRenderZoomMin;
|
||||||
|
|
||||||
if (_tacticalLayer is not null)
|
if (_tacticalLayer is not null)
|
||||||
_tacticalLayer.Visible = zoom >= TacticalRenderZoomMin;
|
_tacticalLayer.Visible = tactical;
|
||||||
if (_settlementLayer is not null)
|
if (_settlementLayer is not null)
|
||||||
_settlementLayer.Visible = zoom < SettlementHideZoom;
|
_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()
|
private void UpdateZoomScaledNodes()
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public partial class PlayScreen : Control
|
|||||||
_playerMarker = new PlayerMarker
|
_playerMarker = new PlayerMarker
|
||||||
{
|
{
|
||||||
Position = new Vector2(_actors.Player!.Position.X, _actors.Player.Position.Y),
|
Position = new Vector2(_actors.Player!.Position.X, _actors.Player.Position.Y),
|
||||||
FacingAngleRad = _actors.Player.FacingAngleRad,
|
Rotation = _actors.Player.FacingAngleRad,
|
||||||
};
|
};
|
||||||
AddChild(_playerMarker);
|
AddChild(_playerMarker);
|
||||||
|
|
||||||
@@ -182,23 +182,39 @@ public partial class PlayScreen : Control
|
|||||||
|
|
||||||
bool tactical = _render.Camera.Zoom.X >= WorldRenderNode.TacticalRenderZoomMin;
|
bool tactical = _render.Camera.Zoom.X >= WorldRenderNode.TacticalRenderZoomMin;
|
||||||
|
|
||||||
// Tactical WASD direction (world-map mode ignores keys — middle-drag
|
// WASD is context-sensitive: tactical mode steps the player,
|
||||||
// pans, click-to-travel sets the destination).
|
// world-map mode pans the camera. Same keys, intent depends on zoom.
|
||||||
float dx = 0f, dy = 0f;
|
float wasdX = 0f, wasdY = 0f;
|
||||||
if (tactical)
|
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;
|
const float PanScreenPxPerSec = 400f;
|
||||||
if (Godot.Input.IsKeyPressed(Key.S) || Godot.Input.IsKeyPressed(Key.Down)) dy += 1f;
|
float invLen = (wasdX != 0f && wasdY != 0f) ? 0.70710678f : 1f;
|
||||||
if (Godot.Input.IsKeyPressed(Key.A) || Godot.Input.IsKeyPressed(Key.Left)) dx -= 1f;
|
float panSpeed = PanScreenPxPerSec / Mathf.Max(_render.Camera.Zoom.X, 0.01f);
|
||||||
if (Godot.Input.IsKeyPressed(Key.D) || Godot.Input.IsKeyPressed(Key.Right)) dx += 1f;
|
_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. Rotation drives the
|
||||||
|
// facing tick via the transform — auto-property setters on a
|
||||||
// Sync the player marker from Core state.
|
// PlayerMarker field would skip QueueRedraw and the cached
|
||||||
|
// _Draw commands would stay stuck at the initial angle.
|
||||||
var p = _actors.Player;
|
var p = _actors.Player;
|
||||||
_playerMarker.Position = new Vector2(p.Position.X, p.Position.Y);
|
_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).
|
// Camera follow when traveling or in tactical (matches MonoGame).
|
||||||
if (_controller.IsTraveling || tactical)
|
if (_controller.IsTraveling || tactical)
|
||||||
@@ -591,7 +607,7 @@ public partial class PlayScreen : Control
|
|||||||
|
|
||||||
string viewBlock = tactical
|
string viewBlock = tactical
|
||||||
? "View: Tactical (WASD to step)"
|
? "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
|
string status = _controller.IsTraveling
|
||||||
? "Traveling…"
|
? "Traveling…"
|
||||||
|
|||||||
Reference in New Issue
Block a user