165 lines
6.1 KiB
C#
165 lines
6.1 KiB
C#
// Assets/_Project/Scripts/Combat/Projectile.cs
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
using TD.Core;
|
|
using TD.Gameplay;
|
|
|
|
namespace TD.Combat
|
|
{
|
|
/// <summary>
|
|
/// A traveling projectile spawned by <see cref="TowerCombat"/> when a tower has
|
|
/// a non-null <c>ProjectilePrefab</c> configured.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <b>Authority:</b> Movement and hit detection run server-only.
|
|
/// <c>NetworkTransform</c> (required on the prefab) replicates position to clients
|
|
/// so the projectile is visible on all peers.
|
|
///
|
|
/// <b>Initialization:</b> Call <see cref="InitializeServer"/> after
|
|
/// <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>.
|
|
///
|
|
/// <b>Kill attribution:</b> <paramref name="sourceOwner"/> is the
|
|
/// <see cref="PlayerSlot"/> of the firing tower. It is passed to
|
|
/// <see cref="EnemyHealth.TakeDamage"/> and <see cref="EnemyStatus.ApplyEffect"/>
|
|
/// so kill gold credits the correct player even for projectile kills.
|
|
///
|
|
/// <b>Chain + Projectile:</b> <see cref="TargetType.Chain"/> is hitscan-only.
|
|
/// A projectile with Chain type will hit the primary target only and log a warning.
|
|
///
|
|
/// <b>Prefab requirements:</b> <c>NetworkObject</c>, <c>NetworkTransform</c>,
|
|
/// and this component at the root.
|
|
/// </remarks>
|
|
[RequireComponent(typeof(NetworkObject))]
|
|
public class Projectile : NetworkBehaviour
|
|
{
|
|
private const float HitThresholdSq = 0.09f; // 0.3 world units
|
|
|
|
private EnemyHealth target;
|
|
private float damage;
|
|
private DamageType damageType;
|
|
private TargetType targetType;
|
|
private float splashRadius;
|
|
private float slowFactor;
|
|
private float dotDamagePerSecond;
|
|
private float effectDuration;
|
|
private float speed;
|
|
private LayerMask enemyLayerMask;
|
|
private PlayerSlot sourceOwner;
|
|
private bool initialized;
|
|
|
|
private static readonly Collider[] s_overlapBuffer = new Collider[32];
|
|
|
|
// ----- Pre-spawn init ---------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Called by <see cref="TowerCombat"/> after <c>Instantiate</c> and before
|
|
/// <c>NetworkObject.Spawn()</c>. <paramref name="sourceOwner"/> is the firing
|
|
/// tower's <see cref="PlayerSlot"/> for kill-gold attribution.
|
|
/// </summary>
|
|
public void InitializeServer(
|
|
EnemyHealth target,
|
|
float damage,
|
|
DamageType damageType,
|
|
TargetType targetType,
|
|
float splashRadius,
|
|
float slowFactor,
|
|
float dotDamagePerSecond,
|
|
float effectDuration,
|
|
float speed,
|
|
LayerMask enemyLayerMask,
|
|
PlayerSlot sourceOwner)
|
|
{
|
|
this.target = target;
|
|
this.damage = damage;
|
|
this.damageType = damageType;
|
|
this.targetType = targetType;
|
|
this.splashRadius = splashRadius;
|
|
this.slowFactor = slowFactor;
|
|
this.dotDamagePerSecond = dotDamagePerSecond;
|
|
this.effectDuration = effectDuration;
|
|
this.speed = speed;
|
|
this.enemyLayerMask = enemyLayerMask;
|
|
this.sourceOwner = sourceOwner;
|
|
initialized = true;
|
|
|
|
if (targetType == TargetType.Chain)
|
|
Debug.LogWarning("[Projectile] TargetType.Chain is hitscan-only. " +
|
|
"This projectile will hit the primary target only.");
|
|
}
|
|
|
|
// ----- Server movement + hit detection ----------------------------
|
|
|
|
private void Update()
|
|
{
|
|
if (!IsServer || !initialized) return;
|
|
|
|
if (target == null || target.IsDead || (target as UnityEngine.Object) == null)
|
|
{
|
|
NetworkObject.Despawn();
|
|
return;
|
|
}
|
|
|
|
Vector3 toTarget = target.transform.position - transform.position;
|
|
|
|
if (toTarget.sqrMagnitude <= HitThresholdSq)
|
|
{
|
|
ApplyHit();
|
|
NetworkObject.Despawn();
|
|
return;
|
|
}
|
|
|
|
transform.rotation = Quaternion.LookRotation(toTarget);
|
|
transform.position += toTarget.normalized * (speed * Time.deltaTime);
|
|
}
|
|
|
|
// ----- Hit application --------------------------------------------
|
|
|
|
private void ApplyHit()
|
|
{
|
|
switch (targetType)
|
|
{
|
|
case TargetType.Single:
|
|
case TargetType.Chain:
|
|
HitEnemy(target);
|
|
break;
|
|
|
|
case TargetType.Splash:
|
|
HitEnemy(target);
|
|
if (splashRadius > 0f)
|
|
{
|
|
int count = Physics.OverlapSphereNonAlloc(
|
|
transform.position, splashRadius,
|
|
s_overlapBuffer, enemyLayerMask);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
|
if (eh == null || eh.IsDead || (object)eh == (object)target) continue;
|
|
HitEnemy(eh);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void HitEnemy(EnemyHealth eh)
|
|
{
|
|
eh.TakeDamage(damage, damageType, sourceOwner);
|
|
|
|
if (effectDuration > 0f)
|
|
{
|
|
float magnitude = damageType switch
|
|
{
|
|
DamageType.Cold => slowFactor,
|
|
DamageType.Fire => dotDamagePerSecond,
|
|
DamageType.Poison => dotDamagePerSecond,
|
|
_ => 0f,
|
|
};
|
|
|
|
if (magnitude > 0f)
|
|
eh.GetComponent<EnemyStatus>()
|
|
?.ApplyEffect(damageType, magnitude, effectDuration, sourceOwner);
|
|
}
|
|
}
|
|
}
|
|
}
|