2026-05-01 20:08:14 -07:00
|
|
|
using System.IO;
|
2026-05-10 18:07:28 -07:00
|
|
|
using Godot;
|
2026-05-01 20:08:14 -07:00
|
|
|
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>
|
2026-05-10 18:07:28 -07:00
|
|
|
/// 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.
|
2026-05-01 20:08:14 -07:00
|
|
|
///
|
2026-05-10 18:07:28 -07:00
|
|
|
/// 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.
|
2026-05-01 20:08:14 -07:00
|
|
|
/// </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;
|
2026-05-10 18:07:28 -07:00
|
|
|
private const float StreamRadiusZoomMin = WorldRenderNode.TacticalRenderZoomMin;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
|
|
|
|
private ChunkStreamer? _streamer;
|
2026-05-10 18:07:28 -07:00
|
|
|
private WorldRenderNode? _render;
|
2026-05-01 20:08:14 -07:00
|
|
|
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;
|
2026-05-10 18:07:28 -07:00
|
|
|
_initialZoom = initialZoom;
|
2026-05-01 20:08:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}");
|
|
|
|
|
|
2026-05-10 18:07:28 -07:00
|
|
|
_render = new WorldRenderNode();
|
|
|
|
|
AddChild(_render);
|
|
|
|
|
_render.Initialize(world, _initialZoom);
|
2026-05-01 20:08:14 -07:00
|
|
|
|
2026-05-10 18:07:28 -07:00
|
|
|
_streamer = new ChunkStreamer(_seed, world, new InMemoryChunkDeltaStore());
|
|
|
|
|
_streamer.OnChunkLoaded += _render.AddChunkNode;
|
|
|
|
|
_streamer.OnChunkEvicting += _render.RemoveChunkNode;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
|
|
|
|
_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);
|
|
|
|
|
|
2026-05-10 18:07:28 -07:00
|
|
|
_render.Camera.Position = new Vector2(_playerPos.X, _playerPos.Y);
|
2026-05-01 20:08:14 -07:00
|
|
|
StreamIfTactical();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void _Process(double delta)
|
|
|
|
|
{
|
2026-05-10 18:07:28 -07:00
|
|
|
if (_render is null || _playerMarker is null) return;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
|
|
|
|
Vector2 dir = Vector2.Zero;
|
2026-05-10 18:07:28 -07:00
|
|
|
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;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
|
|
|
|
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;
|
2026-05-10 18:07:28 -07:00
|
|
|
_render.Camera.Position = pos;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
2026-05-10 18:07:28 -07:00
|
|
|
// 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);
|
2026-05-01 20:08:14 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void StreamIfTactical()
|
|
|
|
|
{
|
2026-05-10 18:07:28 -07:00
|
|
|
if (_streamer is null || _render is null) return;
|
|
|
|
|
if (_render.Camera.Zoom.X < StreamRadiusZoomMin) return;
|
2026-05-01 20:08:14 -07:00
|
|
|
|
|
|
|
|
Vector2 viewport = GetViewport().GetVisibleRect().Size;
|
2026-05-10 18:07:28 -07:00
|
|
|
float halfExtentWorldPx = Mathf.Max(viewport.X, viewport.Y) / _render.Camera.Zoom.X * 0.5f;
|
2026-05-01 20:08:14 -07:00
|
|
|
int halfExtentTiles = Mathf.CeilToInt(halfExtentWorldPx / C.WORLD_TILE_PIXELS);
|
|
|
|
|
int radius = halfExtentTiles + StreamingBufferWorldTiles;
|
|
|
|
|
|
|
|
|
|
_streamer.EnsureLoadedAround(_playerPos, radius);
|
|
|
|
|
}
|
|
|
|
|
}
|