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>
112 lines
3.7 KiB
C#
112 lines
3.7 KiB
C#
using Godot;
|
|
using Theriapolis.GodotHost.Platform;
|
|
using Theriapolis.GodotHost.Rendering;
|
|
|
|
namespace Theriapolis.GodotHost;
|
|
|
|
public partial class Main : Node
|
|
{
|
|
public override void _Ready()
|
|
{
|
|
// GetCmdlineArgs returns every arg (Godot's own flags + ours);
|
|
// GetCmdlineUserArgs only returns args after a "--" separator.
|
|
// Use the union so users don't have to remember the separator.
|
|
var userArgs = OS.GetCmdlineUserArgs();
|
|
var allArgs = OS.GetCmdlineArgs();
|
|
var args = new string[userArgs.Length + allArgs.Length];
|
|
userArgs.CopyTo(args, 0);
|
|
allArgs.CopyTo(args, userArgs.Length);
|
|
|
|
ulong? smokeTestSeed = null;
|
|
ulong? worldMapSeed = null;
|
|
bool runAssetTest = false;
|
|
(ulong seed, int tx, int ty)? tacticalArgs = null;
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--smoke-test")
|
|
{
|
|
ulong seed = 12345UL;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var parsed))
|
|
seed = parsed;
|
|
smokeTestSeed = seed;
|
|
break;
|
|
}
|
|
if (args[i] == "--asset-test")
|
|
{
|
|
runAssetTest = true;
|
|
break;
|
|
}
|
|
if (args[i] == "--world-map")
|
|
{
|
|
ulong seed = 12345UL;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var parsed))
|
|
seed = parsed;
|
|
worldMapSeed = seed;
|
|
break;
|
|
}
|
|
if (args[i] == "--tactical")
|
|
{
|
|
ulong seed = 12345UL;
|
|
int tx = 128, ty = 128;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var s))
|
|
seed = s;
|
|
if (i + 2 < args.Length && int.TryParse(args[i + 2], out var x))
|
|
tx = x;
|
|
if (i + 3 < args.Length && int.TryParse(args[i + 3], out var y))
|
|
ty = y;
|
|
tacticalArgs = (seed, tx, ty);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (smokeTestSeed.HasValue)
|
|
{
|
|
int code = SmokeTest.Run(smokeTestSeed.Value);
|
|
GetTree().Quit(code);
|
|
return;
|
|
}
|
|
|
|
if (runAssetTest)
|
|
{
|
|
int code = AssetTest.Run();
|
|
GetTree().Quit(code);
|
|
return;
|
|
}
|
|
|
|
if (worldMapSeed.HasValue)
|
|
{
|
|
// M4: unified seamless-zoom view. --world-map starts zoomed out
|
|
// (fit-to-viewport, initialZoom=0 = compute fit), --tactical
|
|
// starts at native sprite zoom 32 with the player at the given
|
|
// tile. Wheel between them seamlessly.
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
AddChild(new WorldView(worldMapSeed.Value));
|
|
return;
|
|
}
|
|
|
|
if (tacticalArgs.HasValue)
|
|
{
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
var (seed, tx, ty) = tacticalArgs.Value;
|
|
AddChild(new WorldView(seed, tx, ty, initialZoom: 32f));
|
|
return;
|
|
}
|
|
|
|
GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
|
|
}
|
|
|
|
public override void _UnhandledInput(InputEvent @event)
|
|
{
|
|
if (@event.IsActionPressed("ui_toggle_fullscreen"))
|
|
{
|
|
var mode = DisplayServer.WindowGetMode();
|
|
DisplayServer.WindowSetMode(
|
|
mode == DisplayServer.WindowMode.Fullscreen
|
|
? DisplayServer.WindowMode.Windowed
|
|
: DisplayServer.WindowMode.Fullscreen);
|
|
}
|
|
}
|
|
}
|