42d66c00c3
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>
58 lines
2.0 KiB
C#
58 lines
2.0 KiB
C#
using Godot;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace Theriapolis.GodotHost.Platform;
|
|
|
|
/// <summary>
|
|
/// Loads PNGs from Content/Gfx as Godot ImageTextures and caches them by
|
|
/// relative path. Texture filter is set to Nearest (project default for
|
|
/// pixel art).
|
|
///
|
|
/// This bypasses Godot's res:// import pipeline because Content/ lives
|
|
/// outside the project — but for static pixel-art assets at native size
|
|
/// the import pipeline doesn't add anything we need.
|
|
///
|
|
/// Tactical-view tile rendering (M4) will hit this from chunk streamers,
|
|
/// so the cache is per-process and never evicted; the full atlas
|
|
/// (~50 PNGs at 32x32) is well under 1 MB.
|
|
/// </summary>
|
|
public static class ContentLoader
|
|
{
|
|
private static readonly Dictionary<string, ImageTexture> _cache = new();
|
|
|
|
/// <summary>
|
|
/// Loads a PNG from <c>Content/Gfx/<relativePath></c>. Returns
|
|
/// null and logs an error if the file is missing or unreadable.
|
|
/// </summary>
|
|
public static ImageTexture? LoadGfx(string relativePath)
|
|
{
|
|
if (_cache.TryGetValue(relativePath, out var cached)) return cached;
|
|
|
|
string absolute = Path.Combine(ContentPaths.GfxDir, relativePath);
|
|
if (!File.Exists(absolute))
|
|
{
|
|
// Silent miss — callers (e.g. atlas variant probes) treat null
|
|
// as "no more variants" and shouldn't generate error noise.
|
|
// Genuine missing-asset diagnostics live in AssetTest's summary.
|
|
return null;
|
|
}
|
|
|
|
var image = Image.LoadFromFile(absolute);
|
|
if (image is null)
|
|
{
|
|
GD.PrintErr($"[ContentLoader] Image.LoadFromFile failed: {absolute}");
|
|
return null;
|
|
}
|
|
|
|
var tex = ImageTexture.CreateFromImage(image);
|
|
_cache[relativePath] = tex;
|
|
return tex;
|
|
}
|
|
|
|
public static int CacheCount => _cache.Count;
|
|
|
|
/// <summary>Drops every cached texture. Useful for hot-reload tests.</summary>
|
|
public static void ClearCache() => _cache.Clear();
|
|
}
|