Files
Christopher Wiebe 42d66c00c3 M4: Tactical render + unified seamless-zoom WorldView
Implements the seamless-zoom contract from CLAUDE.md: one Camera2D
covers both world-map and tactical scales; layers fade in/out at zoom
thresholds; polyline widths and the player marker counter-scale with
zoom so on-screen sizing stays consistent across the full range.

Layers (bottom-up in WorldView):
  Biome sprite     — 256x256 ImageTexture scaled by WORLD_TILE_PIXELS;
                     always visible (acts as backdrop past the tactical
                     streaming radius).
  TacticalChunks   — TacticalChunkNode children added on chunk-loaded
                     event; visible only when zoom ≥ 4.
  Polylines/Bridge — Line2D children; always visible. Width recomputed
                     each frame as baseScreenPx / camera.Zoom so the
                     on-screen stroke is constant (4 px highway, 3 px
                     post road, 2 px dirt road, 4.5/3/2 for major-river/
                     river/stream, 4/2 for rail tie/line, 6 for bridge).
  Settlements      — SettlementDot children; hidden when zoom ≥ 2 (you
                     are visually "inside" them at tactical scale).
  PlayerMarker     — Always visible; Scale = 1/zoom keeps it at
                     PLAYER_MARKER_SCREEN_PX on-screen across all zooms.

TacticalAtlas:
  Loads PNGs from Content/Gfx/tactical/{surface,deco}/ via ContentLoader
  with name_0.png/name_1.png/... variant probing (silent miss). Falls
  back to procedurally-generated solid placeholders matching MonoGame's
  TacticalAtlas colour table so missing art doesn't break rendering.

TacticalChunkNode:
  One Node2D per cached chunk, positioned at (OriginX, OriginY) in
  world-pixel space. _Draw iterates the 64x64 tile grid once and Godot
  caches the rasterised CanvasItem; subsequent frames blit instead of
  re-issuing 4096 DrawTextureRect calls.

ChunkStreamer integration:
  WorldView listens to OnChunkLoaded / OnChunkEvicting and adds /
  removes TacticalChunkNode children. Streaming radius is computed
  dynamically from the viewport size and camera zoom plus a 2-tile
  buffer, so chunk loads always cover the visible viewport with margin.
  Chunks only stream when zoom ≥ 4 (tactical is visible).

Main.cs:
  --world-map [seed]            → WorldView, fit-to-viewport zoom
  --tactical  [seed] [tx] [ty]  → WorldView, zoom 32 at given tile
  Both flags converge on the same scene; mouse wheel transitions
  seamlessly between modes.

ContentLoader silent miss:
  Removed the "Missing texture" PrintErr — atlas variant probing
  legitimately tries name_3.png that doesn't exist, and the noise
  drowned the console. Genuine asset failures still surface via
  AssetTest's count summary.

Deleted (replaced by WorldView):
  Theriapolis.Godot/Rendering/WorldMapView.cs
  Theriapolis.Godot/Rendering/TacticalView.cs (created earlier in M4,
  never committed — superseded before commit).

Closes M4 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M5 (codex design system).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:08:14 -07:00

58 lines
1.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
using Theriapolis.Core;
using Theriapolis.Core.Tactical;
namespace Theriapolis.GodotHost.Rendering;
/// <summary>
/// One Node2D per tactical chunk. Positioned at (chunk.OriginX, chunk.OriginY)
/// in world-pixel space; renders all CHUNK_SIZE x CHUNK_SIZE tiles via _Draw.
///
/// Godot caches CanvasItem _Draw output, so _Draw runs once on first paint
/// and the rasterised result is reused every frame thereafter. We only call
/// QueueRedraw() if the chunk's delta changes (M4 doesn't yet — chunks are
/// static once generated).
///
/// Each tactical tile is 1 world pixel wide; the source sprite is
/// TACTICAL_TILE_SPRITE_PX² and gets squashed into that 1×1 cell via
/// DrawTextureRect's destination rect. The camera's zoom (32x for tactical
/// view) magnifies the rasterisation back to native sprite resolution.
/// </summary>
public partial class TacticalChunkNode : Node2D
{
private TacticalChunk? _chunk;
public void Bind(TacticalChunk chunk)
{
_chunk = chunk;
Position = new Vector2(chunk.OriginX, chunk.OriginY);
QueueRedraw();
}
public override void _Draw()
{
if (_chunk is null) return;
int size = C.TACTICAL_CHUNK_SIZE;
// Pass 1: surfaces.
for (int ly = 0; ly < size; ly++)
for (int lx = 0; lx < size; lx++)
{
ref var t = ref _chunk.Tiles[lx, ly];
var tex = TacticalAtlas.GetSurface(t.Surface, t.Variant);
DrawTextureRect(tex, new Rect2(lx, ly, 1, 1), false);
}
// Pass 2: decos on top.
for (int ly = 0; ly < size; ly++)
for (int lx = 0; lx < size; lx++)
{
ref var t = ref _chunk.Tiles[lx, ly];
var tex = TacticalAtlas.GetDeco(t.Deco, t.Variant);
if (tex is null) continue;
DrawTextureRect(tex, new Rect2(lx, ly, 1, 1), false);
}
}
}