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); } } } }