using System.Collections.Generic; using UnityEngine; namespace InfiniteWorld.VoxelWorld { internal static class WorldSpawnPlanner { public static WorldChunkPlacementPlan PlanChunk( int worldSeed, Vector2Int chunkCoord, int chunkSize, IReadOnlyList collections, System.Func getBaseHeight) { if (collections == null || collections.Count == 0) { return WorldChunkPlacementPlan.Empty; } List spawnPoints = new List(); List terrainPatches = new List(); HashSet flattenedCells = new HashSet(); HashSet occupiedCells = new HashSet(); int spawnOrdinal = 0; for (int collectionIndex = 0; collectionIndex < collections.Count; collectionIndex++) { WorldPrefabCollectionRuntime collection = collections[collectionIndex]; if (collection == null || collection.Entries == null || collection.Entries.Count == 0 || collection.MaxPlacementsPerChunk <= 0) { continue; } for (int placementIndex = 0; placementIndex < collection.MaxPlacementsPerChunk; placementIndex++) { if (!TryPickEntry(collection, worldSeed, chunkCoord, collectionIndex, placementIndex, out int entryIndex, out WorldPrefabEntryRuntime entry)) { continue; } if (!PassesSpawnChance(entry, worldSeed, chunkCoord, collectionIndex, placementIndex)) { continue; } if (!TryPlanPlacement(worldSeed, chunkCoord, chunkSize, collection, collectionIndex, placementIndex, entryIndex, entry, spawnOrdinal, occupiedCells, flattenedCells, getBaseHeight, out WorldSpawnPoint spawnPoint, out WorldTerrainPatch patch, out List reservedCells)) { continue; } spawnPoints.Add(spawnPoint); if (patch != null) { terrainPatches.Add(patch); } for (int i = 0; i < reservedCells.Count; i++) { occupiedCells.Add(reservedCells[i]); } spawnOrdinal++; } } if (spawnPoints.Count == 0 && terrainPatches.Count == 0) { return WorldChunkPlacementPlan.Empty; } return new WorldChunkPlacementPlan(spawnPoints, terrainPatches, flattenedCells.Count > 0 ? flattenedCells : null); } private static bool TryPickEntry(WorldPrefabCollectionRuntime collection, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex, out int entryIndex, out WorldPrefabEntryRuntime entry) { float totalWeight = 0f; for (int i = 0; i < collection.Entries.Count; i++) { WorldPrefabEntryRuntime candidate = collection.Entries[i]; if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f) { continue; } totalWeight += candidate.Weight; } if (totalWeight <= 0f) { entryIndex = -1; entry = null; return false; } float pick = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 1)) * totalWeight; float accumulated = 0f; for (int i = 0; i < collection.Entries.Count; i++) { WorldPrefabEntryRuntime candidate = collection.Entries[i]; if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f) { continue; } accumulated += candidate.Weight; if (pick <= accumulated) { entryIndex = i; entry = candidate; return true; } } for (int i = collection.Entries.Count - 1; i >= 0; i--) { WorldPrefabEntryRuntime candidate = collection.Entries[i]; if (candidate == null || !candidate.HasPrefab || candidate.Weight <= 0f) { continue; } entryIndex = i; entry = candidate; return true; } entryIndex = -1; entry = null; return false; } private static bool PassesSpawnChance(WorldPrefabEntryRuntime entry, int worldSeed, Vector2Int chunkCoord, int collectionIndex, int placementIndex) { if (entry == null) { return false; } if (entry.SpawnChancePercent <= 0f) { return false; } if (entry.SpawnChancePercent >= 100f) { return true; } float roll = WorldSeedUtility.Value01(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, 11)) * 100f; return roll <= entry.SpawnChancePercent; } private static bool TryPlanPlacement( int worldSeed, Vector2Int chunkCoord, int chunkSize, WorldPrefabCollectionRuntime collection, int collectionIndex, int placementIndex, int entryIndex, WorldPrefabEntryRuntime entry, int spawnOrdinal, HashSet occupiedCells, HashSet flattenedCells, System.Func getBaseHeight, out WorldSpawnPoint spawnPoint, out WorldTerrainPatch patch, out List reservedCells) { reservedCells = null; spawnPoint = default; patch = null; Vector2Int chunkOrigin = new Vector2Int(chunkCoord.x * chunkSize, chunkCoord.y * chunkSize); int attempts = Mathf.Max(1, collection.AttemptsPerPlacement); for (int attempt = 0; attempt < attempts; attempt++) { int rotationSteps = entry.AllowRotations ? WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 2), 0, 4) : 0; Vector2Int footprint = GetFootprint(entry.Footprint, rotationSteps); if (footprint.x <= 0 || footprint.y <= 0) { continue; } int localMinX = collection.ChunkEdgePadding; int localMinZ = collection.ChunkEdgePadding; int localMaxX = chunkSize - collection.ChunkEdgePadding - footprint.x; int localMaxZ = chunkSize - collection.ChunkEdgePadding - footprint.y; if (localMaxX < localMinX || localMaxZ < localMinZ) { continue; } int localX = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 3), localMinX, localMaxX + 1); int localZ = WorldSeedUtility.Range(WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex, placementIndex, attempt, 4), localMinZ, localMaxZ + 1); RectInt footprintRect = new RectInt(localX, localZ, footprint.x, footprint.y); RectInt reservedRect = ExpandRect(footprintRect, Mathf.Max(0, entry.Clearance), chunkSize); if (IntersectsOccupied(chunkOrigin, reservedRect, occupiedCells)) { continue; } if (entry.PlacementMode == WorldPlacementMode.GroundOnly) { if (!IsGroundArea(chunkOrigin, reservedRect, flattenedCells, getBaseHeight)) { continue; } reservedCells = CollectCells(chunkOrigin, reservedRect); } else { if (!TryCreateFlattenPatch(chunkOrigin, chunkSize, footprintRect, reservedRect, entry, occupiedCells, flattenedCells, getBaseHeight, out patch, out reservedCells)) { patch = null; continue; } } float rotationY = rotationSteps * 90f; Vector3 position = new Vector3( chunkOrigin.x + footprintRect.x + footprintRect.width * 0.5f, 0f, chunkOrigin.y + footprintRect.y + footprintRect.height * 0.5f); long spawnId = WorldSeedUtility.ToStableId( WorldSeedUtility.Hash(worldSeed, chunkCoord.x, chunkCoord.y, collectionIndex), WorldSeedUtility.Hash(entryIndex, placementIndex, spawnOrdinal, footprintRect.x, footprintRect.y)); spawnPoint = new WorldSpawnPoint(spawnId, chunkCoord, spawnOrdinal, collectionIndex, entryIndex, entry.Id, position, Quaternion.Euler(0f, rotationY, 0f)); return true; } return false; } private static bool TryCreateFlattenPatch( Vector2Int chunkOrigin, int chunkSize, RectInt footprintRect, RectInt reservedRect, WorldPrefabEntryRuntime entry, HashSet occupiedCells, HashSet flattenedCells, System.Func getBaseHeight, out WorldTerrainPatch patch, out List reservedCells) { int flattenPadding = Mathf.Max(entry.Clearance, entry.FlattenPadding); RectInt flattenRect = ExpandRect(footprintRect, flattenPadding, chunkSize); List flattened = CollectCells(chunkOrigin, flattenRect); if (IntersectsOccupied(flattened, occupiedCells)) { patch = null; reservedCells = null; return false; } List corridor = TryBuildAccessCorridor(chunkOrigin, chunkSize, flattenRect, Mathf.Max(1, entry.FlattenSearchRadius), occupiedCells, flattenedCells, getBaseHeight); if (corridor == null) { patch = null; reservedCells = null; return false; } for (int i = 0; i < corridor.Count; i++) { flattened.Add(corridor[i]); } HashSet uniqueFlattened = new HashSet(flattened); foreach (Vector2Int worldCell in uniqueFlattened) { flattenedCells.Add(worldCell); } patch = new WorldTerrainPatch(flattened); reservedCells = CollectCells(chunkOrigin, reservedRect); reservedCells.AddRange(corridor); return true; } private static List TryBuildAccessCorridor( Vector2Int chunkOrigin, int chunkSize, RectInt flattenRect, int searchRadius, HashSet occupiedCells, HashSet flattenedCells, System.Func getBaseHeight) { if (HasAccessibleGroundNeighbor(chunkOrigin, flattenRect, flattenedCells, getBaseHeight)) { return new List(); } Vector2Int center = new Vector2Int(flattenRect.x + flattenRect.width / 2, flattenRect.y + flattenRect.height / 2); Vector2Int? target = FindNearestGroundCell(chunkOrigin, chunkSize, center, flattenRect, searchRadius, occupiedCells, flattenedCells, getBaseHeight); if (!target.HasValue) { return null; } Vector2Int entryCell = new Vector2Int( Mathf.Clamp(target.Value.x, chunkOrigin.x + flattenRect.xMin, chunkOrigin.x + flattenRect.xMax - 1), Mathf.Clamp(target.Value.y, chunkOrigin.y + flattenRect.yMin, chunkOrigin.y + flattenRect.yMax - 1)); List corridor = new List(); Vector2Int cursor = target.Value; while (cursor.x != entryCell.x) { if (!occupiedCells.Contains(cursor)) { corridor.Add(cursor); } cursor.x += cursor.x < entryCell.x ? 1 : -1; } while (cursor.y != entryCell.y) { if (!occupiedCells.Contains(cursor)) { corridor.Add(cursor); } cursor.y += cursor.y < entryCell.y ? 1 : -1; } return corridor; } private static bool HasAccessibleGroundNeighbor(Vector2Int chunkOrigin, RectInt flattenRect, HashSet flattenedCells, System.Func getBaseHeight) { for (int z = flattenRect.yMin - 1; z <= flattenRect.yMax; z++) { Vector2Int left = new Vector2Int(chunkOrigin.x + flattenRect.xMin - 1, chunkOrigin.y + z); Vector2Int right = new Vector2Int(chunkOrigin.x + flattenRect.xMax, chunkOrigin.y + z); if (GetPlannedHeight(left, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(right, flattenedCells, getBaseHeight) == 0) { return true; } } for (int x = flattenRect.xMin; x < flattenRect.xMax; x++) { Vector2Int bottom = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMin - 1); Vector2Int top = new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + flattenRect.yMax); if (GetPlannedHeight(bottom, flattenedCells, getBaseHeight) == 0 || GetPlannedHeight(top, flattenedCells, getBaseHeight) == 0) { return true; } } return false; } private static Vector2Int? FindNearestGroundCell( Vector2Int chunkOrigin, int chunkSize, Vector2Int localCenter, RectInt excludedRect, int searchRadius, HashSet occupiedCells, HashSet flattenedCells, System.Func getBaseHeight) { for (int radius = 1; radius <= searchRadius; radius++) { for (int dz = -radius; dz <= radius; dz++) { for (int dx = -radius; dx <= radius; dx++) { if (Mathf.Max(Mathf.Abs(dx), Mathf.Abs(dz)) != radius) { continue; } int localX = localCenter.x + dx; int localZ = localCenter.y + dz; if (localX < 0 || localZ < 0 || localX >= chunkSize || localZ >= chunkSize) { continue; } if (excludedRect.Contains(new Vector2Int(localX, localZ))) { continue; } Vector2Int worldCell = new Vector2Int(chunkOrigin.x + localX, chunkOrigin.y + localZ); if (occupiedCells.Contains(worldCell)) { continue; } if (GetPlannedHeight(worldCell, flattenedCells, getBaseHeight) == 0) { return worldCell; } } } } return null; } private static int GetPlannedHeight(Vector2Int worldCell, HashSet flattenedCells, System.Func getBaseHeight) { return flattenedCells.Contains(worldCell) ? 0 : getBaseHeight(worldCell); } private static bool IsGroundArea(Vector2Int chunkOrigin, RectInt rect, HashSet flattenedCells, System.Func getBaseHeight) { for (int z = rect.yMin; z < rect.yMax; z++) { for (int x = rect.xMin; x < rect.xMax; x++) { if (GetPlannedHeight(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z), flattenedCells, getBaseHeight) > 0) { return false; } } } return true; } private static bool IntersectsOccupied(Vector2Int chunkOrigin, RectInt rect, HashSet occupiedCells) { for (int z = rect.yMin; z < rect.yMax; z++) { for (int x = rect.xMin; x < rect.xMax; x++) { if (occupiedCells.Contains(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z))) { return true; } } } return false; } private static bool IntersectsOccupied(List cells, HashSet occupiedCells) { for (int i = 0; i < cells.Count; i++) { if (occupiedCells.Contains(cells[i])) { return true; } } return false; } private static List CollectCells(Vector2Int chunkOrigin, RectInt rect) { List result = new List(rect.width * rect.height); for (int z = rect.yMin; z < rect.yMax; z++) { for (int x = rect.xMin; x < rect.xMax; x++) { result.Add(new Vector2Int(chunkOrigin.x + x, chunkOrigin.y + z)); } } return result; } private static RectInt ExpandRect(RectInt rect, int amount, int chunkSize) { return new RectInt( Mathf.Max(0, rect.xMin - amount), Mathf.Max(0, rect.yMin - amount), Mathf.Min(chunkSize, rect.xMax + amount) - Mathf.Max(0, rect.xMin - amount), Mathf.Min(chunkSize, rect.yMax + amount) - Mathf.Max(0, rect.yMin - amount)); } private static Vector2Int GetFootprint(Vector2Int footprint, int rotationSteps) { footprint.x = Mathf.Max(1, footprint.x); footprint.y = Mathf.Max(1, footprint.y); return rotationSteps % 2 == 0 ? footprint : new Vector2Int(footprint.y, footprint.x); } } }