// Assets/_Project/Scripts/Combat/Projectile.cs
using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Gameplay;
namespace TD.Combat
{
///
/// A traveling projectile spawned by when a tower has
/// a non-null ProjectilePrefab configured.
///
///
/// Authority: Movement and hit detection run server-only.
/// NetworkTransform (required on the prefab) replicates the position to
/// clients so the projectile is visible on all peers.
///
/// Initialization: Mirrors the TowerInstance.InitializeServer pattern —
/// is called by TowerCombat immediately after
/// Instantiate and before NetworkObject.Spawn(), which avoids writing
/// to NetworkVariables before spawn.
///
/// Target loss: If the target dies or is destroyed before the projectile
/// arrives, the projectile despawns silently (no hit, no damage).
///
/// Chain + Projectile: By design, TargetType.Chain is hitscan. If a designer
/// sets TargetType = Chain on a tower that has a ProjectilePrefab, the projectile
/// will hit the primary target only and ignore the chain. Log a warning to surface
/// the misconfiguration.
///
/// Prefab requirements: Must have NetworkObject, NetworkTransform,
/// and this Projectile component at the root.
///
[RequireComponent(typeof(NetworkObject))]
public class Projectile : NetworkBehaviour
{
// Hit threshold: squared distance at which the projectile considers itself
// to have reached the target. 0.09 = 0.3 world units; small enough to
// feel accurate, large enough to survive a high-speed frame where the
// projectile could skip past the target's transform in one step.
private const float HitThresholdSq = 0.09f;
// All fields are server-local. Set by InitializeServer before Spawn.
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 bool initialized;
// Shared with TowerCombat's overlap calls. Both components run on the
// server main thread so there is no concurrent access.
private static readonly Collider[] s_overlapBuffer = new Collider[32];
// ----- Initialization (server-only, called before Spawn) -----------
///
/// Stores all data this projectile needs to travel and apply damage.
/// Call this immediately after Instantiate and before
/// NetworkObject.Spawn().
///
public void InitializeServer(
EnemyHealth target,
float damage,
DamageType damageType,
TargetType targetType,
float splashRadius,
float slowFactor,
float dotDamagePerSecond,
float effectDuration,
float speed,
LayerMask enemyLayerMask)
{
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;
initialized = true;
if (targetType == TargetType.Chain)
Debug.LogWarning("[Projectile] TargetType.Chain is hitscan-only. " +
"This projectile will hit the primary target only. " +
"Consider using hitscan (null ProjectilePrefab) for chain towers.");
}
// ----- Server movement + hit detection -----------------------------
private void Update()
{
if (!IsServer || !initialized) return;
// Target gone — silently despawn, no damage applied.
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;
}
// Rotate to face the target so the projectile mesh looks correct
// on all clients (NetworkTransform replicates both position and rotation).
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: // chain falls back to single-target on projectiles
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();
if (eh == null || eh.IsDead || (object)eh == (object)target) continue;
HitEnemy(eh);
}
}
break;
}
}
private void HitEnemy(EnemyHealth eh)
{
eh.TakeDamage(damage, damageType);
if (effectDuration > 0f)
{
float magnitude = damageType switch
{
DamageType.Cold => slowFactor,
DamageType.Fire => dotDamagePerSecond,
DamageType.Poison => dotDamagePerSecond,
_ => 0f,
};
if (magnitude > 0f)
eh.GetComponent()
?.ApplyEffect(damageType, magnitude, effectDuration);
}
}
}
}