enemy spawner

This commit is contained in:
2025-10-26 14:17:31 +01:00
parent 20d3b46834
commit 40a62b5b5a
2102 changed files with 1255290 additions and 70 deletions

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Game.Scripts.Runtime.Data
{
[CreateAssetMenu(menuName = "Game/Spawning/Boss Schedule", fileName = "BossSchedule")]
public class BossSchedule : ScriptableObject
{
[Serializable]
public class BossEvent
{
[Min(0f)] public float TimeSinceStart;
public EnemyDefinition Boss;
public int Count = 1;
[SerializeField] private bool useSpawnRadiusOverride;
[SerializeField] private float spawnRadiusOverride = 10f;
public bool HasSpawnRadiusOverride => useSpawnRadiusOverride;
public float SpawnRadiusOverride => spawnRadiusOverride;
}
[SerializeField] private List<BossEvent> events = new();
public IReadOnlyList<BossEvent> Events => events;
private void OnValidate()
{
events.Sort((a, b) => a.TimeSinceStart.CompareTo(b.TimeSinceStart));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e2ad306ddab577d19b1e0e6bbb6fea9

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace Game.Scripts.Runtime.Data
{
[CreateAssetMenu(menuName = "Game/Spawning/Enemy Definition", fileName = "EnemyDefinition")]
public class EnemyDefinition : ScriptableObject
{
[Tooltip("Unique identifier leveraged by pooling and save systems. Defaults to asset name when left blank.")]
[SerializeField] private string id;
[Tooltip("Prefab that represents this enemy. Should include required behaviours and visuals.")]
public GameObject Prefab;
public bool IsBoss;
public float BaseHP = 10f;
public float MoveSpeed = 3.5f;
public float Damage = 1f;
public Vector3 PrefabPivotOffset = Vector3.zero;
public string[] Tags = System.Array.Empty<string>();
public string Id => string.IsNullOrEmpty(id) ? name : id;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b0e84710d3119d60af5d5373005e029

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Game.Scripts.Runtime.Data
{
[CreateAssetMenu(menuName = "Game/Spawning/Spawn Table", fileName = "SpawnTable")]
public class SpawnTable : ScriptableObject
{
[Serializable]
public class Entry
{
public EnemyDefinition Def;
[Min(0)] public int Weight = 1;
public int MinWave = 0;
public int MaxWave = int.MaxValue;
[Tooltip("Optional cap per wave. Values <= 0 mean unlimited.")]
public int Cap = -1;
public bool IsWaveValid(int wave) => wave >= MinWave && wave <= MaxWave;
public bool IsUnderCap(int currentCount)
{
if (Cap <= 0)
{
return true;
}
return currentCount < Cap;
}
}
[SerializeField] private List<Entry> entries = new();
private readonly List<Entry> _eligibleBuffer = new();
public IReadOnlyList<Entry> Entries => entries;
public EnemyDefinition Pick(int wave, System.Random rng)
{
if (entries == null || entries.Count == 0)
{
return null;
}
_eligibleBuffer.Clear();
foreach (var entry in entries)
{
if (entry?.Def == null || entry.Weight <= 0)
{
continue;
}
if (entry.IsWaveValid(wave))
{
_eligibleBuffer.Add(entry);
}
}
if (_eligibleBuffer.Count == 0)
{
return null;
}
var totalWeight = 0;
foreach (var entry in _eligibleBuffer)
{
totalWeight += Mathf.Max(1, entry.Weight);
}
var roll = rng.Next(totalWeight);
foreach (var entry in _eligibleBuffer)
{
var weight = Mathf.Max(1, entry.Weight);
if (roll < weight)
{
return entry.Def;
}
roll -= weight;
}
return _eligibleBuffer[^1].Def;
}
public IEnumerable<Entry> EnumerateEligibleEntries(int wave)
{
if (entries == null)
{
yield break;
}
foreach (var entry in entries)
{
if (entry?.Def == null || entry.Weight <= 0)
{
continue;
}
if (entry.IsWaveValid(wave))
{
yield return entry;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8fd6e160d6ab3dfecbc59a0685007a97

View File

@@ -0,0 +1,44 @@
using UnityEngine;
namespace Game.Scripts.Runtime.Data
{
[CreateAssetMenu(menuName = "Game/Spawning/Spawner Config", fileName = "SpawnerConfig")]
public class SpawnerConfig : ScriptableObject
{
[Header("Radii")]
public float MinSpawnRadius = 8f;
public float MaxSpawnRadius = 24f;
[Header("Timing")]
public float SpawnInterval = 2f;
public AnimationCurve SpawnRateOverTime = AnimationCurve.Linear(0f, 1f, 1f, 1f);
[Header("Limits")]
public int MaxConcurrent = 30;
public int MaxPlacementAttempts = 6;
public int PrewarmPerType = 4;
public int PoolHardCapPerType = 60;
[Header("Placement")]
public float GroundRaycastHeight = 5f;
public float MaxHeightDelta = 4f;
public bool UseLineOfSight = false;
[Header("Layers")]
public LayerMask GroundMask = Physics.DefaultRaycastLayers;
public LayerMask ObstacleMask = Physics.DefaultRaycastLayers;
[Header("NavMesh")]
public int NavMeshAreaMask = -1;
[Header("RNG")]
[SerializeField] private bool useSeed;
[SerializeField] private int seedValue = 0;
[Header("Debug")]
public bool DrawGizmos = false;
public int GizmoSampleCount = 16;
public int? Seed => useSeed ? seedValue : (int?)null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4af5312c5abd9f725a626ceb6f7f8ae5