Run Re-Do
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user