using Unity.Mathematics;
namespace ProjectM.Simulation
{
///
/// Pure, deterministic per-room layout math: resolves a map node () into a concrete
/// and scatters points within the room's shape. No RNG state (scatter takes a
/// caller-seeded by ref); no wall-clock — EditMode-unit-testable and save/replay reproducible
/// (mirrors / ). Archetype numbers (per-shape radius, per-type
/// node counts) are const tables here; an authored RoomArchetype blob can later back these when RoomFieldSystem
/// wants designer-tuned variety, without changing this signature's callers.
///
public static class RoomLayoutMath
{
/// Resolve a map node + its depth into the concrete room spec the server lays out.
public static RoomPlan Plan(in RunMapNode node, int layer, int roomCount)
{
return new RoomPlan
{
RoomType = node.RoomType,
Biome = node.Biome,
ShapeId = node.ShapeId,
Radius = ShapeRadius(node.ShapeId),
NodeCount = BaseNodeCount(node.RoomType),
DifficultyEpoch = DifficultyEpoch(layer, node.RoomType),
};
}
///
/// Depth-based difficulty rung fed to : deeper rooms are harder (layer+1 floor),
/// with Elite/Boss bumps. Lower-bounded at 1. Pure integer.
///
public static int DifficultyEpoch(int layer, byte roomType)
{
int d = math.max(1, layer + 1);
if (roomType == RoomTypeId.Elite) d += 2;
if (roomType == RoomTypeId.Boss) d += 3;
return d;
}
/// Base resource-node count per room type (before the run-wide scarcity budget floors it). Reward
/// rooms are dense; combat/elite lean; the Boss room is minimal.
public static int BaseNodeCount(byte roomType)
{
switch (roomType)
{
case RoomTypeId.Reward: return 5;
case RoomTypeId.Combat: return 2;
case RoomTypeId.Elite: return 2;
case RoomTypeId.Boss: return 1;
default: return 2;
}
}
/// Arena scatter radius (world units) for a shape id.
public static float ShapeRadius(byte shapeId)
{
switch (shapeId)
{
case RoomShapeId.Wide: return 24f;
case RoomShapeId.Long: return 24f;
case RoomShapeId.Cross: return 22f;
case RoomShapeId.Disk:
default: return 18f;
}
}
///
/// Deterministic scatter of point of within the room's
/// shape around , using a caller-seeded RNG. Every returned point satisfies
/// for the same shape/center (asserted in tests). Y is preserved from
/// . / are reserved for future
/// even-spacing variants; today the RNG draw is the sole source of position.
///
public static float3 ScatterInShape(byte shapeId, float3 center, int index, int count, ref Random rng)
{
float r = ShapeRadius(shapeId);
switch (shapeId)
{
case RoomShapeId.Wide:
{
float x = rng.NextFloat(-r, r);
float z = rng.NextFloat(-r * 0.5f, r * 0.5f);
return new float3(center.x + x, center.y, center.z + z);
}
case RoomShapeId.Long:
{
float x = rng.NextFloat(-r * 0.5f, r * 0.5f);
float z = rng.NextFloat(-r, r);
return new float3(center.x + x, center.y, center.z + z);
}
case RoomShapeId.Cross:
{
bool horiz = rng.NextInt(0, 2) == 0;
float along = rng.NextFloat(-r, r);
float across = rng.NextFloat(-r * 0.25f, r * 0.25f);
return horiz
? new float3(center.x + along, center.y, center.z + across)
: new float3(center.x + across, center.y, center.z + along);
}
case RoomShapeId.Disk:
default:
{
float ang = rng.NextFloat(0f, math.PI * 2f);
float rad = r * math.sqrt(rng.NextFloat(0f, 1f)); // area-uniform
return new float3(center.x + math.cos(ang) * rad, center.y, center.z + math.sin(ang) * rad);
}
}
}
///
/// True iff planar point lies within the shape's footprint around
/// (the exact bound produces). Used to validate scatter and (later) placement.
///
public static bool ContainsPoint(byte shapeId, float3 center, float3 p)
{
const float eps = 1e-3f;
float r = ShapeRadius(shapeId);
float dx = p.x - center.x;
float dz = p.z - center.z;
switch (shapeId)
{
case RoomShapeId.Wide:
return math.abs(dx) <= r + eps && math.abs(dz) <= r * 0.5f + eps;
case RoomShapeId.Long:
return math.abs(dx) <= r * 0.5f + eps && math.abs(dz) <= r + eps;
case RoomShapeId.Cross:
{
bool horizArm = math.abs(dx) <= r + eps && math.abs(dz) <= r * 0.25f + eps;
bool vertArm = math.abs(dz) <= r + eps && math.abs(dx) <= r * 0.25f + eps;
return horizArm || vertArm;
}
case RoomShapeId.Disk:
default:
return dx * dx + dz * dz <= (r + eps) * (r + eps);
}
}
}
}