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; } internal sealed class WorldPrefabCollectionRuntime { public WorldPrefabCollectionRuntime(int sourceIndex, int maxPlacementsPerChunk, int attemptsPerPlacement, int chunkEdgePadding, IReadOnlyList entries) { SourceIndex = sourceIndex; MaxPlacementsPerChunk = maxPlacementsPerChunk; AttemptsPerPlacement = attemptsPerPlacement; ChunkEdgePadding = chunkEdgePadding; Entries = entries ?? Array.Empty(); } public int SourceIndex { get; } public int MaxPlacementsPerChunk { get; } public int AttemptsPerPlacement { get; } public int ChunkEdgePadding { get; } public IReadOnlyList Entries { get; } } internal sealed class WorldPrefabEntryRuntime { public WorldPrefabEntryRuntime(int sourceIndex, string id, float weight, float spawnChancePercent, WorldPlacementMode placementMode, Vector2Int footprint, int clearance, int flattenPadding, int flattenSearchRadius, bool allowRotations, bool hasPrefab) { SourceIndex = sourceIndex; Id = string.IsNullOrWhiteSpace(id) ? $"entry_{sourceIndex}" : id; Weight = Mathf.Max(0f, weight); SpawnChancePercent = Mathf.Clamp(spawnChancePercent, 0f, 100f); PlacementMode = placementMode; Footprint = new Vector2Int(Mathf.Max(1, footprint.x), Mathf.Max(1, footprint.y)); Clearance = Mathf.Max(0, clearance); FlattenPadding = Mathf.Max(0, flattenPadding); FlattenSearchRadius = Mathf.Max(1, flattenSearchRadius); AllowRotations = allowRotations; HasPrefab = hasPrefab; } public int SourceIndex { get; } public string Id { get; } public float Weight { get; } public float SpawnChancePercent { get; } public WorldPlacementMode PlacementMode { get; } public Vector2Int Footprint { get; } public int Clearance { get; } public int FlattenPadding { get; } public int FlattenSearchRadius { get; } public bool AllowRotations { get; } public bool HasPrefab { get; } } 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, string entryId, Vector3 position, Quaternion rotation) { SpawnId = spawnId; ChunkCoord = chunkCoord; SpawnOrdinalInChunk = spawnOrdinalInChunk; CollectionIndex = collectionIndex; EntryIndex = entryIndex; EntryId = entryId; 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 string EntryId { 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; } } }