140 lines
6.2 KiB
C#
140 lines
6.2 KiB
C#
using Unity.Mathematics;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// Pure, deterministic per-room layout math: resolves a map node (<see cref="RunMapNode"/>) into a concrete
|
|
/// <see cref="RoomPlan"/> and scatters points within the room's shape. No RNG state (scatter takes a
|
|
/// caller-seeded <see cref="Random"/> by ref); no wall-clock — EditMode-unit-testable and save/replay reproducible
|
|
/// (mirrors <see cref="ZoneEnemyMath"/> / <see cref="RunMapMath"/>). Archetype numbers (per-shape radius, per-type
|
|
/// node counts) are const tables here; an authored <c>RoomArchetype</c> blob can later back these when RoomFieldSystem
|
|
/// wants designer-tuned variety, without changing this signature's callers.
|
|
/// </summary>
|
|
public static class RoomLayoutMath
|
|
{
|
|
/// <summary>Resolve a map node + its depth into the concrete room spec the server lays out.</summary>
|
|
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),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Depth-based difficulty rung fed to <see cref="ZoneEnemyMath"/>: deeper rooms are harder (layer+1 floor),
|
|
/// with Elite/Boss bumps. Lower-bounded at 1. Pure integer.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Arena scatter radius (world units) for a shape id.</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deterministic scatter of point <paramref name="index"/> of <paramref name="count"/> within the room's
|
|
/// shape around <paramref name="center"/>, using a caller-seeded RNG. Every returned point satisfies
|
|
/// <see cref="ContainsPoint"/> for the same shape/center (asserted in tests). Y is preserved from
|
|
/// <paramref name="center"/>. <paramref name="index"/>/<paramref name="count"/> are reserved for future
|
|
/// even-spacing variants; today the RNG draw is the sole source of position.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True iff planar point <paramref name="p"/> lies within the shape's footprint around <paramref name="center"/>
|
|
/// (the exact bound <see cref="ScatterInShape"/> produces). Used to validate scatter and (later) placement.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|