Files
TheDeclineOfWarriors/Assets/Features/VoxelWorld/Runtime/WorldPrefabCollection.cs
T
2026-04-08 09:34:45 +07:00

173 lines
7.6 KiB
C#

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<WorldPrefabEntry> entries = new List<WorldPrefabEntry>();
public IReadOnlyList<WorldPrefabEntry> 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<WorldPrefabEntryRuntime> entries)
{
SourceIndex = sourceIndex;
MaxPlacementsPerChunk = maxPlacementsPerChunk;
AttemptsPerPlacement = attemptsPerPlacement;
ChunkEdgePadding = chunkEdgePadding;
Entries = entries ?? Array.Empty<WorldPrefabEntryRuntime>();
}
public int SourceIndex { get; }
public int MaxPlacementsPerChunk { get; }
public int AttemptsPerPlacement { get; }
public int ChunkEdgePadding { get; }
public IReadOnlyList<WorldPrefabEntryRuntime> 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<Vector2Int> flattenedCells;
public WorldTerrainPatch(IReadOnlyList<Vector2Int> cells)
{
flattenedCells = cells != null ? new List<Vector2Int>(cells) : new List<Vector2Int>();
}
public IReadOnlyList<Vector2Int> 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<WorldSpawnPoint> EmptySpawnPoints = Array.Empty<WorldSpawnPoint>();
private static readonly IReadOnlyList<WorldTerrainPatch> EmptyTerrainPatches = Array.Empty<WorldTerrainPatch>();
private readonly IReadOnlyList<WorldSpawnPoint> spawnPoints;
private readonly IReadOnlyList<WorldTerrainPatch> terrainPatches;
private readonly HashSet<Vector2Int> flattenedCells;
public static WorldChunkPlacementPlan Empty { get; } = new WorldChunkPlacementPlan(EmptySpawnPoints, EmptyTerrainPatches, null);
public WorldChunkPlacementPlan(IReadOnlyList<WorldSpawnPoint> spawnPoints, IReadOnlyList<WorldTerrainPatch> terrainPatches, HashSet<Vector2Int> flattenedCells)
{
this.spawnPoints = spawnPoints ?? EmptySpawnPoints;
this.terrainPatches = terrainPatches ?? EmptyTerrainPatches;
this.flattenedCells = flattenedCells;
}
public IReadOnlyList<WorldSpawnPoint> SpawnPoints => spawnPoints;
public IReadOnlyList<WorldTerrainPatch> 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;
}
}
}