using System.IO; using Godot; using Theriapolis.Core; using Theriapolis.Core.Tactical; using Theriapolis.Core.Util; using Theriapolis.Core.World.Generation; using Theriapolis.GodotHost.Platform; namespace Theriapolis.GodotHost.Rendering; /// /// Standalone demo entry point for the M2+M4 unified seamless-zoom view. /// Runs worldgen inline, spawns a placeholder player, and lets you walk /// around with WASD. Used by the --world-map and --tactical /// CLI flags for headless / debug viewing without going through the /// title → wizard → progress flow. /// /// The actual rendering — biome / polyline / settlement / chunk layers /// and the camera — lives in , which is /// shared with M7's . This shell just /// owns the demo's local player position and streaming loop. /// public partial class WorldView : Node2D { private readonly ulong _seed; private readonly int _startWorldTileX; private readonly int _startWorldTileY; private readonly float _initialZoom; // World-pixel movement speed. 32 wp = 1 world tile, so 96 = ~3 tiles/sec. private const float MoveSpeedWorldPx = 96f; private const int StreamingBufferWorldTiles = 2; private const float StreamRadiusZoomMin = WorldRenderNode.TacticalRenderZoomMin; private ChunkStreamer? _streamer; private WorldRenderNode? _render; private Vec2 _playerPos; private PlayerMarker? _playerMarker; public WorldView(ulong seed, int startWorldTileX = 128, int startWorldTileY = 128, float initialZoom = 0f) { _seed = seed; _startWorldTileX = startWorldTileX; _startWorldTileY = startWorldTileY; _initialZoom = initialZoom; } public override void _Ready() { string dataDir = ContentPaths.DataDir; if (!Directory.Exists(dataDir)) { GD.PrintErr($"[world] Data directory not found: {dataDir}"); return; } GD.Print($"[world] seed=0x{_seed:X} start-tile=({_startWorldTileX},{_startWorldTileY})"); var ctx = new WorldGenContext(_seed, dataDir); WorldGenerator.RunAll(ctx); var world = ctx.World; GD.Print($"[world] worldgen done — rivers={world.Rivers.Count} " + $"roads={world.Roads.Count} rails={world.Rails.Count} " + $"settlements={world.Settlements.Count} bridges={world.Bridges.Count}"); _render = new WorldRenderNode(); AddChild(_render); _render.Initialize(world, _initialZoom); _streamer = new ChunkStreamer(_seed, world, new InMemoryChunkDeltaStore()); _streamer.OnChunkLoaded += _render.AddChunkNode; _streamer.OnChunkEvicting += _render.RemoveChunkNode; _playerPos = new Vec2( _startWorldTileX * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f, _startWorldTileY * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f); _playerMarker = new PlayerMarker { Position = new Vector2(_playerPos.X, _playerPos.Y) }; AddChild(_playerMarker); _render.Camera.Position = new Vector2(_playerPos.X, _playerPos.Y); StreamIfTactical(); } public override void _Process(double delta) { if (_render is null || _playerMarker is null) return; Vector2 dir = Vector2.Zero; if (Godot.Input.IsKeyPressed(Key.W) || Godot.Input.IsKeyPressed(Key.Up)) dir.Y -= 1; if (Godot.Input.IsKeyPressed(Key.S) || Godot.Input.IsKeyPressed(Key.Down)) dir.Y += 1; if (Godot.Input.IsKeyPressed(Key.A) || Godot.Input.IsKeyPressed(Key.Left)) dir.X -= 1; if (Godot.Input.IsKeyPressed(Key.D) || Godot.Input.IsKeyPressed(Key.Right)) dir.X += 1; if (dir != Vector2.Zero) { dir = dir.Normalized(); float step = MoveSpeedWorldPx * (float)delta; _playerPos = new Vec2(_playerPos.X + dir.X * step, _playerPos.Y + dir.Y * step); float maxX = C.WORLD_WIDTH_TILES * C.WORLD_TILE_PIXELS - 1f; float maxY = C.WORLD_HEIGHT_TILES * C.WORLD_TILE_PIXELS - 1f; _playerPos = new Vec2( Mathf.Clamp(_playerPos.X, 0f, maxX), Mathf.Clamp(_playerPos.Y, 0f, maxY)); StreamIfTactical(); } var pos = new Vector2(_playerPos.X, _playerPos.Y); _playerMarker.Position = pos; _render.Camera.Position = pos; // Counter-scale the marker so its on-screen size stays constant. float zoom = _render.Camera.Zoom.X; if (zoom > 0f) _playerMarker.Scale = new Vector2(1f / zoom, 1f / zoom); } private void StreamIfTactical() { if (_streamer is null || _render is null) return; if (_render.Camera.Zoom.X < StreamRadiusZoomMin) return; Vector2 viewport = GetViewport().GetVisibleRect().Size; float halfExtentWorldPx = Mathf.Max(viewport.X, viewport.Y) / _render.Camera.Zoom.X * 0.5f; int halfExtentTiles = Mathf.CeilToInt(halfExtentWorldPx / C.WORLD_TILE_PIXELS); int radius = halfExtentTiles + StreamingBufferWorldTiles; _streamer.EnsureLoadedAround(_playerPos, radius); } }