using System; using System.Collections.Generic; using UnityEngine; namespace InfiniteWorld.VoxelWorld { public enum WorldPlacementMode : byte { GroundOnly, FlattenTerrain } [CreateAssetMenu(menuName = "Infinite World/World Prefab Collection", fileName = "WorldPrefabCollection")] public sealed class WorldPrefabCollection : ScriptableObject { [Tooltip("Maximum number of spawn points this collection may contribute to a single chunk.")] [Min(0)] public int maxPlacementsPerChunk = 2; [Tooltip("How many deterministic candidate positions are tested for each spawn slot before it is skipped.")] [Min(1)] public int attemptsPerPlacement = 8; [Tooltip("How many cells near the chunk border are reserved so large prefabs do not clip outside the chunk.")] [Min(0)] public int chunkEdgePadding = 1; public List entries = new List(); public IReadOnlyList Entries => entries; } [Serializable] public sealed class WorldPrefabEntry { [Tooltip("Stable logical identifier for this entry. Use it later for save data, analytics, or seeded config generation.")] public string id = "entry"; [Tooltip("Prefab that will be instantiated when this entry wins placement generation.")] public GameObject prefab; [Tooltip("Relative weight inside the collection. Higher values make this entry more likely to be selected compared to others.")] [Min(0f)] public float weight = 1f; [Tooltip("Percent chance from 0 to 100 that the selected entry will actually spawn in its slot after being chosen by weight.")] [Range(0f, 100f)] public float spawnChancePercent = 100f; [Tooltip("GroundOnly requires an already flat area. FlattenTerrain carves the area to ground level and ensures an approach path.")] public WorldPlacementMode placementMode; [Tooltip("Required placement size in world cells. Rotations may swap X and Y if enabled.")] public Vector2Int footprint = Vector2Int.one; [Tooltip("Extra reserved cells around the footprint so other generated placements do not overlap too closely.")] [Min(0)] public int clearance; [Tooltip("Extra cells around the footprint that will also be flattened when using FlattenTerrain.")] [Min(0)] public int flattenPadding = 1; [Tooltip("Maximum search radius in cells for finding existing ground and cutting an access corridor to the flattened area.")] [Min(1)] public int flattenSearchRadius = 6; [Tooltip("Allows deterministic 90-degree rotations so the same prefab can fit in more layout variations.")] public bool allowRotations = true; } public sealed class WorldTerrainPatch { private readonly List flattenedCells; public WorldTerrainPatch(IReadOnlyList cells) { flattenedCells = cells != null ? new List(cells) : new List(); } public IReadOnlyList FlattenedCells => flattenedCells; } public readonly struct WorldSpawnPoint { public WorldSpawnPoint(long spawnId, Vector2Int chunkCoord, int spawnOrdinalInChunk, int collectionIndex, int entryIndex, WorldPrefabEntry entry, Vector3 position, Quaternion rotation) { SpawnId = spawnId; ChunkCoord = chunkCoord; SpawnOrdinalInChunk = spawnOrdinalInChunk; CollectionIndex = collectionIndex; EntryIndex = entryIndex; Entry = entry; Position = position; Rotation = rotation; } public long SpawnId { get; } public Vector2Int ChunkCoord { get; } public int SpawnOrdinalInChunk { get; } public int CollectionIndex { get; } public int EntryIndex { get; } public WorldPrefabEntry Entry { get; } public Vector3 Position { get; } public Quaternion Rotation { get; } } public sealed class WorldChunkPlacementPlan { private static readonly IReadOnlyList EmptySpawnPoints = Array.Empty(); private static readonly IReadOnlyList EmptyTerrainPatches = Array.Empty(); private readonly IReadOnlyList spawnPoints; private readonly IReadOnlyList terrainPatches; private readonly HashSet flattenedCells; public static WorldChunkPlacementPlan Empty { get; } = new WorldChunkPlacementPlan(EmptySpawnPoints, EmptyTerrainPatches, null); public WorldChunkPlacementPlan(IReadOnlyList spawnPoints, IReadOnlyList terrainPatches, HashSet flattenedCells) { this.spawnPoints = spawnPoints ?? EmptySpawnPoints; this.terrainPatches = terrainPatches ?? EmptyTerrainPatches; this.flattenedCells = flattenedCells; } public IReadOnlyList SpawnPoints => spawnPoints; public IReadOnlyList TerrainPatches => terrainPatches; public bool HasFlattening => flattenedCells != null && flattenedCells.Count > 0; public int GetFinalHeight(Vector2Int worldCell, int baseHeight) { return flattenedCells != null && flattenedCells.Contains(worldCell) ? 0 : baseHeight; } } }