Merge branch 'death-sounds'

This commit is contained in:
Ian Woods 2026-06-30 23:13:13 -07:00
commit 2286de6fbc
26 changed files with 313 additions and 12 deletions

View file

@ -58,7 +58,7 @@ namespace TD.Audio
}
}
public void Play(AudioClip clip, AudioCategory category, float pitch = 1f)
public void Play(AudioClip clip, AudioCategory category, float pitch = 1f, float volume = 1f)
{
if (clip == null) return;
if (!pools.TryGetValue(category, out var pool)) return;
@ -66,6 +66,7 @@ namespace TD.Audio
int idx = indices[category];
var src = pool[idx % pool.Length];
src.pitch = pitch;
src.volume = volume;
src.clip = clip;
src.Play();
indices[category] = idx + 1;

View file

@ -0,0 +1,16 @@
// Assets/_Project/Scripts/Audio/SoundConfig.cs
using UnityEngine;
namespace TD.Audio
{
[System.Serializable]
public struct SoundConfig
{
public AudioClip clip;
[Range(0f, 1f)] public float volume;
[Range(0f, 3f)] public float minPitch;
[Range(0f, 3f)] public float maxPitch;
public float RandomPitch() => Random.Range(minPitch, maxPitch);
}
}

View file

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

View file

@ -0,0 +1,42 @@
// Assets/_Project/Scripts/Combat/EnemyDeathSound.cs
using TD.Audio;
using TD.Gameplay;
using UnityEngine;
namespace TD.Combat
{
/// <summary>
/// Local-only audio for enemy death. Subscribes to <see cref="EnemyHealth.OnDiedClient"/>
/// and plays a randomly chosen clip from <see cref="deathClips"/>.
/// Attach to any enemy prefab and assign its clips in the Inspector.
/// </summary>
[RequireComponent(typeof(EnemyHealth))]
public class EnemyDeathSound : MonoBehaviour
{
[SerializeField] private SoundConfig[] deathSounds;
private EnemyHealth enemyHealth;
private void Awake()
{
enemyHealth = GetComponent<EnemyHealth>();
}
private void OnEnable()
{
if (enemyHealth != null) enemyHealth.OnDiedClient += HandleDied;
}
private void OnDisable()
{
if (enemyHealth != null) enemyHealth.OnDiedClient -= HandleDied;
}
private void HandleDied()
{
if (deathSounds == null || deathSounds.Length == 0) return;
var sound = deathSounds[Random.Range(0, deathSounds.Length)];
AudioManager.Instance?.Play(sound.clip, AudioCategory.EnemyHitDeath, sound.RandomPitch(), sound.volume);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9df6fea9a86daafae998f8eecc44ee7d

View file

@ -17,12 +17,7 @@ namespace TD.Combat
[RequireComponent(typeof(TowerCombat))]
public class TeslaTowerFireSound : MonoBehaviour
{
[Tooltip("Sound played each time the coil discharges.")]
[SerializeField] private AudioClip fireClip;
[Tooltip("Pitch is randomized within this range each shot to avoid a mechanical loop.")]
[SerializeField] private float minPitch = 0.93f;
[SerializeField] private float maxPitch = 1.07f;
[SerializeField] private SoundConfig fireSound;
private TowerCombat combat;
@ -43,7 +38,7 @@ namespace TD.Combat
private void HandleAreaFired(Vector3[] positions)
{
AudioManager.Instance?.Play(fireClip, AudioCategory.TowerFire, Random.Range(minPitch, maxPitch));
AudioManager.Instance?.Play(fireSound.clip, AudioCategory.TowerFire, fireSound.RandomPitch(), fireSound.volume);
}
}
}

View file

@ -0,0 +1,41 @@
// Assets/_Project/Scripts/Combat/TowerBuiltSound.cs
using TD.Audio;
using TD.Gameplay;
using UnityEngine;
namespace TD.Combat
{
/// <summary>
/// Local-only audio for tower construction completion. Subscribes to
/// <see cref="TowerInstance.OnBuiltClient"/> and plays a sound on every peer
/// when the tower finishes building.
/// Attach to any tower prefab and assign its clip in the Inspector.
/// </summary>
[RequireComponent(typeof(TowerInstance))]
public class TowerBuiltSound : MonoBehaviour
{
[SerializeField] private SoundConfig builtSound;
private TowerInstance towerInstance;
private void Awake()
{
towerInstance = GetComponent<TowerInstance>();
}
private void OnEnable()
{
if (towerInstance != null) towerInstance.OnBuiltClient += HandleBuilt;
}
private void OnDisable()
{
if (towerInstance != null) towerInstance.OnBuiltClient -= HandleBuilt;
}
private void HandleBuilt()
{
AudioManager.Instance?.Play(builtSound.clip, AudioCategory.UI, builtSound.RandomPitch(), builtSound.volume);
}
}
}

View file

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

View file

@ -126,6 +126,9 @@ namespace TD.Gameplay
/// </summary>
public event System.Action<EnemyHealth> OnDied;
/// <summary>Fired on ALL peers the moment this enemy dies. Use for client-side visuals and audio.</summary>
public event System.Action OnDiedClient;
// ----- Server-only pre-spawn init -------------------------------------
/// <summary>
@ -240,6 +243,7 @@ namespace TD.Gameplay
if (deathAnimator != null)
deathAnimator.SetTrigger(DieHash);
DiedClientRpc();
StartCoroutine(DeathSequence());
}
@ -263,6 +267,12 @@ namespace TD.Gameplay
// mesh sizes substantially, derive this from a collider bounds instead.
public float SelectionRadius => 0.4f;
[ClientRpc]
private void DiedClientRpc()
{
OnDiedClient?.Invoke();
}
private IEnumerator DeathSequence()
{
// Hold while the death animation plays.

View file

@ -166,6 +166,11 @@ namespace TD.Gameplay
}
}
// ----- Events ---------------------------------------------------------
/// <summary>Fired on ALL peers when this tower finishes construction and spawns.</summary>
public event System.Action OnBuiltClient;
// ----- Server-only initialization -------------------------------------
/// <summary>
@ -262,6 +267,8 @@ namespace TD.Gameplay
$"for {ownerSlot.Value} at anchor {anchorTile.Value}. " +
$"IsServer={IsServer}");
}
OnBuiltClient?.Invoke();
}
public override void OnNetworkDespawn()