Files
Christopher Wiebe 59784048cd M2: World map render in Godot
Renders the full worldgen output as a Godot scene at visual parity with
worldgen-dump's PNG output: biome tiles, rivers/roads/rails as Line2D
polylines, settlements as filled circles. Pan + zoom via Camera2D.

WorldMapView.cs:
  - Loads Content/Data via res:// walk-up, runs WorldGenerator.RunAll
  - Tile palette built from BiomeDef.ParsedColor() — same source as the
    PNG dump, so colours are identical
  - Tiles rendered as a 256x256 Image scaled by WORLD_TILE_PIXELS to
    cover world-pixel space (matches polyline coord system)
  - Polyline draw order mirrors LineFeatureRenderer.cs: roads (smaller
    first) -> rivers -> rail tie underlay -> rail line. Bridges as
    short Line2Ds; settlements as SettlementDot (Node2D + _Draw circle)
  - Line widths in world-pixel space, tuned for visibility at world-map
    zoom; M4 will add zoom-aware width scaling for tactical view
  - Camera fits the whole world (95% of viewport) on first frame

PanZoomCamera.cs:
  - Mouse-wheel zoom centered on cursor (cursor world-point stays fixed)
  - Middle/right click + drag to pan
  - MinZoom/MaxZoom configurable per-instance

Main.cs:
  - --world-map [seed] flag launches the view (default seed 12345)
  - Arg parser now reads both GetCmdlineArgs and GetCmdlineUserArgs so
    callers don't need to remember the "--" separator
  - --smoke-test path and M0 hello-world fallback unchanged

Visual diff against world_seed12345.png (generated by
worldgen-dump --seed 12345) confirmed manually: same biome palette, same
rivers/roads topology, same settlement placement and tier colours.
3 rivers, 91 roads, 226 settlements (138 PoIs), 0 rails (ENABLE_RAIL=false),
0 bridges (this seed has no road/river crossings). All match the PNG.

Settlement dot sizes iterated twice from user feedback — final values
in tile units, scaled to world-pixel space, so they shrink at world-map
zoom and grow toward tactical zoom (the right "scale with the map"
behaviour).

Closes M2 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M3 (asset pipeline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 19:04:02 -07:00

70 lines
2.4 KiB
C#

using Godot;
namespace Theriapolis.GodotHost.Rendering;
/// <summary>
/// Camera2D with mouse-wheel zoom and middle/right-button click-drag pan.
/// Zoom is centered on the cursor so zooming feels natural. Bounds:
/// the zoom factor is clamped between MinZoom (everything fits) and
/// MaxZoom (1 tile fills the screen).
/// </summary>
public partial class PanZoomCamera : Camera2D
{
[Export] public float MinZoom { get; set; } = 0.04f;
[Export] public float MaxZoom { get; set; } = 4.0f;
[Export] public float ZoomStep { get; set; } = 1.15f;
private bool _dragging;
private Vector2 _dragStartCursor;
private Vector2 _dragStartCameraPos;
public override void _UnhandledInput(InputEvent @event)
{
switch (@event)
{
case InputEventMouseButton mb when mb.Pressed:
HandleMouseButtonPressed(mb);
break;
case InputEventMouseButton mb when !mb.Pressed:
if (mb.ButtonIndex is MouseButton.Middle or MouseButton.Right)
_dragging = false;
break;
case InputEventMouseMotion mm when _dragging:
Position = _dragStartCameraPos
+ (_dragStartCursor - mm.Position) / Zoom;
break;
}
}
private void HandleMouseButtonPressed(InputEventMouseButton mb)
{
switch (mb.ButtonIndex)
{
case MouseButton.WheelUp:
ApplyZoom(ZoomStep, mb.Position);
break;
case MouseButton.WheelDown:
ApplyZoom(1f / ZoomStep, mb.Position);
break;
case MouseButton.Middle:
case MouseButton.Right:
_dragging = true;
_dragStartCursor = mb.Position;
_dragStartCameraPos = Position;
break;
}
}
private void ApplyZoom(float factor, Vector2 cursorScreen)
{
float newZoom = Mathf.Clamp(Zoom.X * factor, MinZoom, MaxZoom);
if (Mathf.IsEqualApprox(newZoom, Zoom.X)) return;
// Zoom toward the cursor: keep the world point under the cursor fixed.
Vector2 worldBefore = GetCanvasTransform().AffineInverse() * cursorScreen;
Zoom = new Vector2(newZoom, newZoom);
Vector2 worldAfter = GetCanvasTransform().AffineInverse() * cursorScreen;
Position += worldBefore - worldAfter;
}
}