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:
@@ -0,0 +1,120 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// 8-directional A* pathfinder on the 1024×1024 world tile grid.
|
||||
/// Uses pre-allocated arrays and a generation counter to avoid clearing between queries.
|
||||
/// The cost function receives (fromX, fromY, toX, toY, entryDir) and returns the cost
|
||||
/// of moving to the 'to' tile, or float.PositiveInfinity if impassable.
|
||||
/// </summary>
|
||||
public sealed class AStarPathfinder
|
||||
{
|
||||
private const int W = C.WORLD_WIDTH_TILES;
|
||||
private const int H = C.WORLD_HEIGHT_TILES;
|
||||
private const int TileCount = W * H;
|
||||
|
||||
// Persistent arrays reused across queries (generation counter avoids clearing)
|
||||
private readonly float[] _gScore = new float[TileCount];
|
||||
private readonly int[] _cameFrom = new int[TileCount]; // -1 = none
|
||||
private readonly byte[] _generation = new byte[TileCount]; // current run's gen tag
|
||||
private byte _currentGen;
|
||||
|
||||
private readonly BinaryHeap<int> _open = new(4096);
|
||||
|
||||
// 8-directional deltas: N, NE, E, SE, S, SW, W, NW
|
||||
private static readonly (int dx, int dy)[] Dirs =
|
||||
{
|
||||
( 0,-1), ( 1,-1), ( 1, 0), ( 1, 1),
|
||||
( 0, 1), (-1, 1), (-1, 0), (-1,-1),
|
||||
};
|
||||
|
||||
private static float Diagonal(int dx, int dy)
|
||||
=> (dx != 0 && dy != 0) ? 1.4142f : 1f; // sqrt(2) for diagonals
|
||||
|
||||
private static float OctileHeuristic(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int dx = Math.Abs(x1 - x0);
|
||||
int dy = Math.Abs(y1 - y0);
|
||||
return Math.Max(dx, dy) + (1.4142f - 1f) * Math.Min(dx, dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the lowest-cost path from (sx,sy) to (gx,gy).
|
||||
/// <paramref name="costFn"/> receives (fromX, fromY, toX, toY, entryDir) and returns
|
||||
/// the cost of entering the 'to' tile from this direction, or PositiveInfinity if impassable.
|
||||
/// Returns null if no path exists within the tile grid or if maxExpansions is exceeded.
|
||||
/// </summary>
|
||||
public List<(int X, int Y)>? FindPath(
|
||||
int sx, int sy,
|
||||
int gx, int gy,
|
||||
Func<int, int, int, int, byte, float> costFn,
|
||||
int maxExpansions = 600_000)
|
||||
{
|
||||
if (sx == gx && sy == gy) return new List<(int, int)> { (sx, sy) };
|
||||
int expansions = 0;
|
||||
|
||||
// Increment generation (wrap around)
|
||||
_currentGen = (byte)((_currentGen + 1) == 0 ? 1 : _currentGen + 1);
|
||||
_open.Clear();
|
||||
|
||||
int startIdx = sy * W + sx;
|
||||
_gScore[startIdx] = 0f;
|
||||
_cameFrom[startIdx] = -1;
|
||||
_generation[startIdx] = _currentGen;
|
||||
|
||||
float h0 = OctileHeuristic(sx, sy, gx, gy);
|
||||
_open.Insert(startIdx, h0);
|
||||
|
||||
while (!_open.IsEmpty)
|
||||
{
|
||||
if (++expansions > maxExpansions) return null; // safety cap
|
||||
|
||||
int curIdx = _open.ExtractMin();
|
||||
int curX = curIdx % W;
|
||||
int curY = curIdx / W;
|
||||
|
||||
if (curX == gx && curY == gy)
|
||||
return ReconstructPath(curIdx);
|
||||
|
||||
float curG = _gScore[curIdx];
|
||||
|
||||
for (int d = 0; d < 8; d++)
|
||||
{
|
||||
int nx = curX + Dirs[d].dx;
|
||||
int ny = curY + Dirs[d].dy;
|
||||
if ((uint)nx >= W || (uint)ny >= H) continue;
|
||||
|
||||
byte entryDir = (byte)d;
|
||||
float moveCost = costFn(curX, curY, nx, ny, entryDir);
|
||||
if (float.IsPositiveInfinity(moveCost)) continue;
|
||||
|
||||
float newG = curG + Diagonal(Dirs[d].dx, Dirs[d].dy) + moveCost;
|
||||
int nIdx = ny * W + nx;
|
||||
|
||||
if (_generation[nIdx] == _currentGen && newG >= _gScore[nIdx])
|
||||
continue; // already found a better path
|
||||
|
||||
_gScore[nIdx] = newG;
|
||||
_cameFrom[nIdx] = curIdx;
|
||||
_generation[nIdx] = _currentGen;
|
||||
|
||||
float f = newG + OctileHeuristic(nx, ny, gx, gy);
|
||||
_open.Insert(nIdx, f);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // no path found
|
||||
}
|
||||
|
||||
private List<(int X, int Y)> ReconstructPath(int goalIdx)
|
||||
{
|
||||
var path = new List<(int, int)>();
|
||||
int cur = goalIdx;
|
||||
while (cur != -1)
|
||||
{
|
||||
path.Add((cur % W, cur / W));
|
||||
cur = _cameFrom[cur];
|
||||
}
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Min-heap priority queue used by AStarPathfinder.
|
||||
/// Items with lower priority are extracted first.
|
||||
/// Uses lazy deletion: does not support decrease-key; instead, duplicates are
|
||||
/// inserted and stale entries are skipped at extraction time.
|
||||
/// </summary>
|
||||
public sealed class BinaryHeap<T>
|
||||
{
|
||||
private struct Entry
|
||||
{
|
||||
public float Priority;
|
||||
public T Item;
|
||||
}
|
||||
|
||||
private Entry[] _heap;
|
||||
private int _count;
|
||||
|
||||
public BinaryHeap(int capacity = 1024)
|
||||
{
|
||||
_heap = new Entry[capacity];
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
public int Count => _count;
|
||||
public bool IsEmpty => _count == 0;
|
||||
|
||||
public void Clear() => _count = 0;
|
||||
|
||||
public void Insert(T item, float priority)
|
||||
{
|
||||
if (_count == _heap.Length)
|
||||
Array.Resize(ref _heap, _heap.Length * 2);
|
||||
|
||||
_heap[_count] = new Entry { Priority = priority, Item = item };
|
||||
BubbleUp(_count);
|
||||
_count++;
|
||||
}
|
||||
|
||||
public (T Item, float Priority) Peek()
|
||||
{
|
||||
if (_count == 0) throw new InvalidOperationException("Heap is empty.");
|
||||
return (_heap[0].Item, _heap[0].Priority);
|
||||
}
|
||||
|
||||
public T ExtractMin()
|
||||
{
|
||||
if (_count == 0) throw new InvalidOperationException("Heap is empty.");
|
||||
var result = _heap[0];
|
||||
_count--;
|
||||
if (_count > 0)
|
||||
{
|
||||
_heap[0] = _heap[_count];
|
||||
BubbleDown(0);
|
||||
}
|
||||
return result.Item;
|
||||
}
|
||||
|
||||
private void BubbleUp(int i)
|
||||
{
|
||||
while (i > 0)
|
||||
{
|
||||
int parent = (i - 1) >> 1;
|
||||
if (_heap[parent].Priority <= _heap[i].Priority) break;
|
||||
(_heap[parent], _heap[i]) = (_heap[i], _heap[parent]);
|
||||
i = parent;
|
||||
}
|
||||
}
|
||||
|
||||
private void BubbleDown(int i)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int left = (i << 1) + 1;
|
||||
int right = left + 1;
|
||||
int min = i;
|
||||
|
||||
if (left < _count && _heap[left].Priority < _heap[min].Priority) min = left;
|
||||
if (right < _count && _heap[right].Priority < _heap[min].Priority) min = right;
|
||||
|
||||
if (min == i) break;
|
||||
(_heap[min], _heap[i]) = (_heap[i], _heap[min]);
|
||||
i = min;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Direction encoding for tile-level features (rivers, rail, roads).
|
||||
/// Directions are stored as byte values 0–7, matching the 8 compass points.
|
||||
/// 255 = "no direction" / unset.
|
||||
/// </summary>
|
||||
public static class Dir
|
||||
{
|
||||
// Direction constants (0-based, CCW from North)
|
||||
public const byte None = 255;
|
||||
public const byte N = 0; // (dx= 0, dy=-1)
|
||||
public const byte NE = 1; // (dx= 1, dy=-1)
|
||||
public const byte E = 2; // (dx= 1, dy= 0)
|
||||
public const byte SE = 3; // (dx= 1, dy= 1)
|
||||
public const byte S = 4; // (dx= 0, dy= 1)
|
||||
public const byte SW = 5; // (dx=-1, dy= 1)
|
||||
public const byte W = 6; // (dx=-1, dy= 0)
|
||||
public const byte NW = 7; // (dx=-1, dy=-1)
|
||||
|
||||
private static readonly (int dx, int dy)[] _deltas =
|
||||
{
|
||||
( 0,-1), ( 1,-1), ( 1, 0), ( 1, 1),
|
||||
( 0, 1), (-1, 1), (-1, 0), (-1,-1),
|
||||
};
|
||||
|
||||
/// <summary>Convert a (dx, dy) delta to a direction byte. Both values must be in {-1,0,1}.</summary>
|
||||
public static byte FromDelta(int dx, int dy)
|
||||
{
|
||||
for (byte i = 0; i < 8; i++)
|
||||
if (_deltas[i].dx == dx && _deltas[i].dy == dy)
|
||||
return i;
|
||||
return None;
|
||||
}
|
||||
|
||||
public static (int dx, int dy) ToDelta(byte dir)
|
||||
{
|
||||
if (dir == None) return (0, 0);
|
||||
return _deltas[dir & 7];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two directions are parallel if their angular difference is ≤ 45° (including opposite directions).
|
||||
/// </summary>
|
||||
public static bool IsParallel(byte a, byte b)
|
||||
{
|
||||
if (a == None || b == None) return false;
|
||||
int diff = Math.Abs((int)(a & 7) - (int)(b & 7));
|
||||
diff = Math.Min(diff, 8 - diff);
|
||||
// diff 0 = same; diff 1 = 45°; diff 4 = 180° (opposite still parallel)
|
||||
return diff <= 1 || diff >= 3; // ≤45° or ≥135° (anti-parallel)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Two directions are perpendicular if their angular difference is in [60°, 120°].
|
||||
/// Using discrete 45° steps: diff 2 = 90°.
|
||||
/// </summary>
|
||||
public static bool IsPerpendicular(byte a, byte b)
|
||||
{
|
||||
if (a == None || b == None) return false;
|
||||
int diff = Math.Abs((int)(a & 7) - (int)(b & 7));
|
||||
diff = Math.Min(diff, 8 - diff);
|
||||
return diff == 2;
|
||||
}
|
||||
|
||||
/// <summary>Opposite direction.</summary>
|
||||
public static byte Opposite(byte d) => d == None ? None : (byte)((d + 4) & 7);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// FastNoiseLite — vendored C# implementation.
|
||||
// Based on the FastNoiseLite library by Jordan Peck (Auburn).
|
||||
// Provides 2D Simplex noise with fractal FBm support.
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
public sealed class FastNoiseLite
|
||||
{
|
||||
public enum NoiseType { OpenSimplex2, Simplex, Perlin }
|
||||
public enum FractalType { None, FBm, Ridged, PingPong }
|
||||
|
||||
// ── Properties ────────────────────────────────────────────────────────────
|
||||
public int Seed { get; set; } = 1337;
|
||||
public float Frequency { get; set; } = 0.01f;
|
||||
public NoiseType Noise { get; set; } = NoiseType.OpenSimplex2;
|
||||
public FractalType Fractal { get; set; } = FractalType.FBm;
|
||||
public int Octaves { get; set; } = 3;
|
||||
public float Lacunarity{ get; set; } = 2.0f;
|
||||
public float Gain { get; set; } = 0.5f;
|
||||
public float WeightedStrength { get; set; } = 0.0f;
|
||||
|
||||
// ── Permutation table ─────────────────────────────────────────────────────
|
||||
private readonly int[] _perm = new int[512];
|
||||
private int _cachedSeed = int.MinValue;
|
||||
|
||||
private void EnsurePerm()
|
||||
{
|
||||
if (_cachedSeed == Seed) return;
|
||||
_cachedSeed = Seed;
|
||||
// Build the shuffled permutation table from the seed
|
||||
var p = new int[256];
|
||||
for (int i = 0; i < 256; i++) p[i] = i;
|
||||
// Seeded Fisher–Yates
|
||||
ulong state = (ulong)Seed ^ 0x9e3779b97f4a7c15UL;
|
||||
for (int i = 255; i > 0; i--)
|
||||
{
|
||||
state += 0x9e3779b97f4a7c15UL;
|
||||
ulong z = state;
|
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9UL;
|
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111ebUL;
|
||||
int j = (int)((z ^ (z >> 31)) % (ulong)(i + 1));
|
||||
(p[i], p[j]) = (p[j], p[i]);
|
||||
}
|
||||
for (int i = 0; i < 512; i++) _perm[i] = p[i & 255];
|
||||
}
|
||||
|
||||
// ── 2D gradient table (12 directions) ────────────────────────────────────
|
||||
private static readonly float[] GradX = { 1, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, 0 };
|
||||
private static readonly float[] GradY = { 1, 1, -1, -1, 0, 0, 0, 0, 1, -1, 1, -1 };
|
||||
|
||||
private static int FastFloor(float x) => x >= 0 ? (int)x : (int)x - 1;
|
||||
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
|
||||
|
||||
// ── Core 2D simplex noise ────────────────────────────────────────────────
|
||||
private float Simplex2D(float x, float y)
|
||||
{
|
||||
EnsurePerm();
|
||||
const float F2 = 0.3660254037844387f; // (sqrt(3)-1)/2
|
||||
const float G2 = 0.21132486540518713f; // (3-sqrt(3))/6
|
||||
|
||||
float s = (x + y) * F2;
|
||||
int i = FastFloor(x + s);
|
||||
int j = FastFloor(y + s);
|
||||
float t = (i + j) * G2;
|
||||
float x0 = x - (i - t);
|
||||
float y0 = y - (j - t);
|
||||
|
||||
int i1, j1;
|
||||
if (x0 > y0) { i1 = 1; j1 = 0; }
|
||||
else { i1 = 0; j1 = 1; }
|
||||
|
||||
float x1 = x0 - i1 + G2;
|
||||
float y1 = y0 - j1 + G2;
|
||||
float x2 = x0 - 1f + 2f * G2;
|
||||
float y2 = y0 - 1f + 2f * G2;
|
||||
|
||||
int gi0 = _perm[( i + _perm[ j & 255]) & 255] % 12;
|
||||
int gi1 = _perm[((i + i1) + _perm[((j + j1)) & 255]) & 255] % 12;
|
||||
int gi2 = _perm[( i + 1 + _perm[( j + 1 ) & 255]) & 255] % 12;
|
||||
|
||||
float n = Contribution(gi0, x0, y0)
|
||||
+ Contribution(gi1, x1, y1)
|
||||
+ Contribution(gi2, x2, y2);
|
||||
return 70f * n; // scale to approximately [-1, 1]
|
||||
}
|
||||
|
||||
private static float Contribution(int gi, float x, float y)
|
||||
{
|
||||
float t = 0.5f - x * x - y * y;
|
||||
if (t < 0) return 0f;
|
||||
t *= t;
|
||||
return t * t * (GradX[gi] * x + GradY[gi] * y);
|
||||
}
|
||||
|
||||
// ── OpenSimplex2 (alias to Simplex for vendored build) ────────────────────
|
||||
// Full OpenSimplex2 derivation is identical in output characteristics for
|
||||
// our use; the distinction matters only for tiling, which we don't use.
|
||||
private float OpenSimplex2_2D(float x, float y)
|
||||
{
|
||||
const float sqrt3 = 1.7320508075688772f;
|
||||
const float F2 = 0.5f * (sqrt3 - 1f);
|
||||
float t = (x + y) * F2;
|
||||
x += t; y += t;
|
||||
return Simplex2D(x, y);
|
||||
}
|
||||
|
||||
// ── Single noise sample (no fractal) ──────────────────────────────────────
|
||||
private float SingleNoise(float x, float y) => Noise switch
|
||||
{
|
||||
NoiseType.OpenSimplex2 => OpenSimplex2_2D(x, y),
|
||||
NoiseType.Simplex => Simplex2D(x, y),
|
||||
NoiseType.Perlin => Simplex2D(x, y), // use simplex as stand-in
|
||||
_ => Simplex2D(x, y),
|
||||
};
|
||||
|
||||
// ── Fractal FBm ───────────────────────────────────────────────────────────
|
||||
private float FractalFBm(float x, float y)
|
||||
{
|
||||
float sum = 0;
|
||||
float amp = CalcFractalBounding();
|
||||
float freq = Frequency;
|
||||
for (int i = 0; i < Octaves; i++)
|
||||
{
|
||||
float n = SingleNoise(x * freq, y * freq);
|
||||
sum += n * amp;
|
||||
amp *= Lerp(1f, MathF.Min(n + 1f, 2f) * 0.5f, WeightedStrength);
|
||||
amp *= Gain;
|
||||
freq *= Lacunarity;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private float FractalRidged(float x, float y)
|
||||
{
|
||||
float sum = 0;
|
||||
float amp = CalcFractalBounding();
|
||||
float freq = Frequency;
|
||||
for (int i = 0; i < Octaves; i++)
|
||||
{
|
||||
float n = MathF.Abs(SingleNoise(x * freq, y * freq));
|
||||
sum += (n * -2f + 1f) * amp;
|
||||
amp *= Lerp(1f, 1f - n, WeightedStrength);
|
||||
amp *= Gain;
|
||||
freq *= Lacunarity;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private float CalcFractalBounding()
|
||||
{
|
||||
float amp = Gain;
|
||||
float ampFractal = 1f;
|
||||
for (int i = 1; i < Octaves; i++)
|
||||
{
|
||||
ampFractal += amp;
|
||||
amp *= Gain;
|
||||
}
|
||||
return 1f / ampFractal;
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Returns noise in approximately [-1, 1].</summary>
|
||||
public float GetNoise(float x, float y) => Fractal switch
|
||||
{
|
||||
FractalType.None => SingleNoise(x * Frequency, y * Frequency),
|
||||
FractalType.FBm => FractalFBm(x, y),
|
||||
FractalType.Ridged => FractalRidged(x, y),
|
||||
FractalType.PingPong => FractalFBm(x, y), // simplification
|
||||
_ => FractalFBm(x, y),
|
||||
};
|
||||
|
||||
/// <summary>Returns noise remapped to [0, 1].</summary>
|
||||
public float GetNoise01(float x, float y) => (GetNoise(x, y) + 1f) * 0.5f;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Simple syllable-based settlement name generator.
|
||||
/// Names are deterministic from the RNG stream passed in.
|
||||
/// </summary>
|
||||
public static class NameGenerator
|
||||
{
|
||||
// Prefixes keyed roughly by biome/region character
|
||||
private static readonly string[] ForestPrefixes =
|
||||
{ "Mill", "Wood", "Ash", "Elm", "Oak", "Dark", "Green", "Birch", "Briar", "Hollow" };
|
||||
private static readonly string[] GrasslandPrefixes =
|
||||
{ "Flat", "Wind", "Broad", "Long", "Wide", "Open", "West", "East", "South", "High" };
|
||||
private static readonly string[] MountainPrefixes =
|
||||
{ "Stone", "Iron", "Crag", "Peak", "High", "Cold", "Hard", "Grey", "Frost", "Ridge" };
|
||||
private static readonly string[] CoastPrefixes =
|
||||
{ "Port", "Bay", "Salt", "Shore", "Wave", "Tide", "Sea", "Gull", "Haven", "Cove" };
|
||||
private static readonly string[] IndustrialPrefixes =
|
||||
{ "Iron", "Coal", "Forge", "Mill", "Steel", "Smoke", "Works", "Rail", "Ash", "Soot" };
|
||||
private static readonly string[] DefaultPrefixes =
|
||||
{ "North", "South", "East", "West", "New", "Old", "Cross", "Red", "Black", "White" };
|
||||
|
||||
private static readonly string[] Roots =
|
||||
{
|
||||
"haven", "feld", "ford", "bridge", "bury", "ton", "wick", "croft", "moor", "vale",
|
||||
"thorpe", "worth", "stead", "gate", "well", "lea", "marsh", "brook", "heath", "down",
|
||||
"field", "wood", "ridge", "cliff", "holm", "beck", "burn", "shaw", "thwaite", "garth",
|
||||
};
|
||||
|
||||
private static readonly string[] Suffixes =
|
||||
{ "", "", "", "ville", "ton", "burg", "berg", "port", "ford", "cross", "hall", "keep" };
|
||||
|
||||
/// <summary>
|
||||
/// Generate a settlement name from the given RNG and biome character.
|
||||
/// The biome parameter is the macro cell's biome type string.
|
||||
/// </summary>
|
||||
public static string Generate(SeededRng rng, string biomeType)
|
||||
{
|
||||
var prefixes = biomeType.ToLowerInvariant() switch
|
||||
{
|
||||
var b when b.Contains("forest") => ForestPrefixes,
|
||||
var b when b.Contains("grassland") => GrasslandPrefixes,
|
||||
var b when b.Contains("mountain") => MountainPrefixes,
|
||||
var b when b.Contains("coast") => CoastPrefixes,
|
||||
var b when b.Contains("industrial") => IndustrialPrefixes,
|
||||
var b when b.Contains("subtropical") => CoastPrefixes,
|
||||
var b when b.Contains("wetland") => ForestPrefixes,
|
||||
_ => DefaultPrefixes,
|
||||
};
|
||||
|
||||
string prefix = prefixes[rng.NextInt(prefixes.Length)];
|
||||
string root = Roots[rng.NextInt(Roots.Length)];
|
||||
string suffix = Suffixes[rng.NextInt(Suffixes.Length)];
|
||||
|
||||
// Avoid awkward concatenations (e.g., "Millmill")
|
||||
if (root.StartsWith(prefix.ToLowerInvariant()))
|
||||
prefix = DefaultPrefixes[rng.NextInt(DefaultPrefixes.Length)];
|
||||
|
||||
return prefix + root + suffix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// SplitMix64-based pseudo-random number generator with named sub-stream support.
|
||||
/// All game randomness must go through this class — no new System.Random() anywhere.
|
||||
/// </summary>
|
||||
public sealed class SeededRng
|
||||
{
|
||||
private ulong _state;
|
||||
|
||||
public SeededRng(ulong seed)
|
||||
{
|
||||
// Mix the seed to avoid bad low-entropy states
|
||||
_state = seed == 0 ? 0x9e3779b97f4a7c15UL : seed;
|
||||
// Warm up the state
|
||||
NextUInt64();
|
||||
NextUInt64();
|
||||
}
|
||||
|
||||
/// <summary>Create a sub-stream for a specific subsystem using the world seed and a named tag constant.</summary>
|
||||
public static SeededRng ForSubsystem(ulong worldSeed, ulong subsystemTag)
|
||||
=> new(worldSeed ^ subsystemTag);
|
||||
|
||||
public ulong NextUInt64()
|
||||
{
|
||||
// SplitMix64 step
|
||||
_state += 0x9e3779b97f4a7c15UL;
|
||||
ulong z = _state;
|
||||
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9UL;
|
||||
z = (z ^ (z >> 27)) * 0x94d049bb133111ebUL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
public uint NextUInt32() => (uint)(NextUInt64() >> 32);
|
||||
|
||||
/// <summary>Returns a double in [0, 1).</summary>
|
||||
public double NextDouble() => (NextUInt64() >> 11) * (1.0 / (1UL << 53));
|
||||
|
||||
/// <summary>Returns a float in [0, 1).</summary>
|
||||
public float NextFloat() => (float)NextDouble();
|
||||
|
||||
/// <summary>Returns a float in [min, max).</summary>
|
||||
public float NextFloat(float min, float max) => min + NextFloat() * (max - min);
|
||||
|
||||
/// <summary>Returns an int in [min, max).</summary>
|
||||
public int NextInt(int min, int max)
|
||||
{
|
||||
if (max <= min) return min;
|
||||
return min + (int)(NextUInt64() % (ulong)(max - min));
|
||||
}
|
||||
|
||||
/// <summary>Returns an int in [0, max).</summary>
|
||||
public int NextInt(int max) => NextInt(0, max);
|
||||
|
||||
/// <summary>Returns true with probability p (0–1).</summary>
|
||||
public bool NextBool(double p = 0.5) => NextDouble() < p;
|
||||
|
||||
/// <summary>Shuffles a span in place using Fisher–Yates.</summary>
|
||||
public void Shuffle<T>(Span<T> span)
|
||||
{
|
||||
for (int i = span.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = NextInt(0, i + 1);
|
||||
(span[i], span[j]) = (span[j], span[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace Theriapolis.Core.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight 2D float vector for world-pixel-space polyline coordinates.
|
||||
/// Intentionally avoids System.Numerics to keep Core dependency-free.
|
||||
/// </summary>
|
||||
public readonly struct Vec2
|
||||
{
|
||||
public readonly float X;
|
||||
public readonly float Y;
|
||||
|
||||
public Vec2(float x, float y) { X = x; Y = y; }
|
||||
|
||||
public static Vec2 operator +(Vec2 a, Vec2 b) => new(a.X + b.X, a.Y + b.Y);
|
||||
public static Vec2 operator -(Vec2 a, Vec2 b) => new(a.X - b.X, a.Y - b.Y);
|
||||
public static Vec2 operator *(Vec2 a, float s) => new(a.X * s, a.Y * s);
|
||||
public static Vec2 operator *(float s, Vec2 a) => new(a.X * s, a.Y * s);
|
||||
public static Vec2 operator /(Vec2 a, float s) => new(a.X / s, a.Y / s);
|
||||
|
||||
public float LengthSquared => X * X + Y * Y;
|
||||
public float Length => MathF.Sqrt(LengthSquared);
|
||||
|
||||
public Vec2 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
float len = Length;
|
||||
return len < 1e-6f ? new Vec2(0, 0) : this * (1f / len);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>90° CCW rotation — perpendicular vector.</summary>
|
||||
public Vec2 Perp => new(-Y, X);
|
||||
|
||||
public static float Dot(Vec2 a, Vec2 b) => a.X * b.X + a.Y * b.Y;
|
||||
public static float DistSq(Vec2 a, Vec2 b) => (a - b).LengthSquared;
|
||||
public static float Dist(Vec2 a, Vec2 b) => (a - b).Length;
|
||||
|
||||
/// <summary>Linear interpolation between a and b at parameter t.</summary>
|
||||
public static Vec2 Lerp(Vec2 a, Vec2 b, float t) => a + (b - a) * t;
|
||||
|
||||
public override string ToString() => $"({X:F1}, {Y:F1})";
|
||||
}
|
||||
Reference in New Issue
Block a user