We've got enemies and movement!!

This commit is contained in:
Matt F 2026-05-12 22:18:23 -07:00
parent 42ee0bf65d
commit 3287e8ea43
26 changed files with 1409 additions and 161 deletions

View file

@ -12,35 +12,28 @@ namespace TD.Combat
/// </summary>
/// <remarks>
/// <b>Authority:</b> Movement and hit detection run server-only.
/// <c>NetworkTransform</c> (required on the prefab) replicates the position to
/// clients so the projectile is visible on all peers.
/// <c>NetworkTransform</c> (required on the prefab) replicates position to clients
/// so the projectile is visible on all peers.
///
/// <b>Initialization:</b> Mirrors the <c>TowerInstance.InitializeServer</c> pattern —
/// <see cref="InitializeServer"/> is called by <c>TowerCombat</c> immediately after
/// <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>, which avoids writing
/// to NetworkVariables before spawn.
/// <b>Initialization:</b> Call <see cref="InitializeServer"/> after
/// <c>Instantiate</c> and before <c>NetworkObject.Spawn()</c>.
///
/// <b>Target loss:</b> If the target dies or is destroyed before the projectile
/// arrives, the projectile despawns silently (no hit, no damage).
/// <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> 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.
/// <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> Must have <c>NetworkObject</c>, <c>NetworkTransform</c>,
/// and this <c>Projectile</c> component at the root.
/// <b>Prefab requirements:</b> <c>NetworkObject</c>, <c>NetworkTransform</c>,
/// and this component at the root.
/// </remarks>
[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;
private const float HitThresholdSq = 0.09f; // 0.3 world units
// All fields are server-local. Set by InitializeServer before Spawn.
private EnemyHealth target;
private float damage;
private DamageType damageType;
@ -51,18 +44,17 @@ namespace TD.Combat
private float effectDuration;
private float speed;
private LayerMask enemyLayerMask;
private PlayerSlot sourceOwner;
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) -----------
// ----- Pre-spawn init ---------------------------------------------
/// <summary>
/// Stores all data this projectile needs to travel and apply damage.
/// Call this immediately after <c>Instantiate</c> and before
/// <c>NetworkObject.Spawn()</c>.
/// 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,
@ -74,7 +66,8 @@ namespace TD.Combat
float dotDamagePerSecond,
float effectDuration,
float speed,
LayerMask enemyLayerMask)
LayerMask enemyLayerMask,
PlayerSlot sourceOwner)
{
this.target = target;
this.damage = damage;
@ -86,24 +79,21 @@ namespace TD.Combat
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. " +
"Consider using hitscan (null ProjectilePrefab) for chain towers.");
"This projectile will hit the primary target only.");
}
// ----- Server movement + hit detection -----------------------------
// ----- 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)
if (target == null || target.IsDead || (target as UnityEngine.Object) == null)
{
NetworkObject.Despawn();
return;
@ -118,20 +108,18 @@ namespace TD.Combat
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 ---------------------------------------------
// ----- Hit application --------------------------------------------
private void ApplyHit()
{
switch (targetType)
{
case TargetType.Single:
case TargetType.Chain: // chain falls back to single-target on projectiles
case TargetType.Chain:
HitEnemy(target);
break;
@ -156,7 +144,7 @@ namespace TD.Combat
private void HitEnemy(EnemyHealth eh)
{
eh.TakeDamage(damage, damageType);
eh.TakeDamage(damage, damageType, sourceOwner);
if (effectDuration > 0f)
{
@ -170,7 +158,7 @@ namespace TD.Combat
if (magnitude > 0f)
eh.GetComponent<EnemyStatus>()
?.ApplyEffect(damageType, magnitude, effectDuration);
?.ApplyEffect(damageType, magnitude, effectDuration, sourceOwner);
}
}
}