Initial commit: Theriapolis baseline at port/godot branch point

Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-04-30 20:40:51 -07:00
commit b451f83174
525 changed files with 75786 additions and 0 deletions
@@ -0,0 +1,81 @@
namespace Theriapolis.Core.World.Generation;
/// <summary>
/// Computes 8-connected land-component IDs and sizes via flood fill. Settlement
/// placement uses this to confine settlements to the main landmass — roads
/// can't cross ocean, so a settlement on a disconnected island would either be
/// unreachable or get a sea-crossing straight-line connector stub from
/// <c>EnsureSettlementConnectivity</c>. 8-connected matches A*'s movement
/// model so "reachable by road" and "same component" coincide.
/// </summary>
internal static class LandmassMap
{
private const int W = C.WORLD_WIDTH_TILES;
private const int H = C.WORLD_HEIGHT_TILES;
private static readonly (int dx, int dy)[] Neighbors =
{
( 0,-1), ( 1,-1), ( 1, 0), ( 1, 1),
( 0, 1), (-1, 1), (-1, 0), (-1,-1),
};
/// <summary>
/// Returns a pair of arrays: <c>componentId[x,y]</c> gives the 8-connected
/// land component ID at (x,y), or -1 if (x,y) is ocean; <c>componentSize[id]</c>
/// gives the tile count of component <paramref name="id"/>.
/// </summary>
public static (int[,] componentId, int[] componentSize) Compute(WorldState world)
{
var compId = new int[W, H];
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
compId[x, y] = -1;
var sizes = new List<int>();
var queue = new Queue<(int x, int y)>();
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
{
if (compId[x, y] != -1) continue;
if (world.Tiles[x, y].Biome == BiomeId.Ocean) continue;
int id = sizes.Count;
int size = 0;
compId[x, y] = id;
queue.Enqueue((x, y));
while (queue.Count > 0)
{
var (cx, cy) = queue.Dequeue();
size++;
foreach (var (dx, dy) in Neighbors)
{
int nx = cx + dx, ny = cy + dy;
if ((uint)nx >= W || (uint)ny >= H) continue;
if (compId[nx, ny] != -1) continue;
if (world.Tiles[nx, ny].Biome == BiomeId.Ocean) continue;
compId[nx, ny] = id;
queue.Enqueue((nx, ny));
}
}
sizes.Add(size);
}
return (compId, sizes.ToArray());
}
/// <summary>
/// Returns the component ID of the largest land component, or -1 if the
/// world has no land.
/// </summary>
public static int LargestComponentId(int[] componentSizes)
{
int best = -1;
int bestSize = 0;
for (int i = 0; i < componentSizes.Length; i++)
if (componentSizes[i] > bestSize) { bestSize = componentSizes[i]; best = i; }
return best;
}
}