Files
TheriapolisV3/Theriapolis.Godot/Rendering/WorldView.cs
T

132 lines
5.1 KiB
C#
Raw Normal View History

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;
/// <summary>
/// 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 <c>--world-map</c> and <c>--tactical</c>
/// 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 <see cref="WorldRenderNode"/>, which is
/// shared with M7's <see cref="Scenes.PlayScreen"/>. This shell just
/// owns the demo's local player position and streaming loop.
/// </summary>
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);
}
}