We've got enemies and movement!!
This commit is contained in:
parent
42ee0bf65d
commit
3287e8ea43
26 changed files with 1409 additions and 161 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,24 +249,27 @@ namespace TD.Combat
|
|||
|
||||
private void ApplyDamageToTarget(TowerDefinition def, EnemyHealth primary, Vector3 primaryPos)
|
||||
{
|
||||
PlayerSlot owner = towerInstance.Owner;
|
||||
|
||||
switch (def.TargetType)
|
||||
{
|
||||
case TargetType.Single:
|
||||
HitEnemy(def, primary);
|
||||
HitEnemy(def, primary, owner);
|
||||
break;
|
||||
|
||||
case TargetType.Splash:
|
||||
HitEnemy(def, primary);
|
||||
ApplySplash(def, primary, primaryPos);
|
||||
HitEnemy(def, primary, owner);
|
||||
ApplySplash(def, primary, primaryPos, owner);
|
||||
break;
|
||||
|
||||
case TargetType.Chain:
|
||||
ApplyChain(def, primary);
|
||||
ApplyChain(def, primary, owner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySplash(TowerDefinition def, EnemyHealth primary, Vector3 origin)
|
||||
private void ApplySplash(TowerDefinition def, EnemyHealth primary,
|
||||
Vector3 origin, PlayerSlot owner)
|
||||
{
|
||||
if (def.SplashRadius <= 0f) return;
|
||||
|
||||
|
|
@ -277,25 +280,22 @@ namespace TD.Combat
|
|||
{
|
||||
var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
||||
if (eh == null || eh.IsDead || (object)eh == (object)primary) continue;
|
||||
HitEnemy(def, eh);
|
||||
HitEnemy(def, eh, owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyChain(TowerDefinition def, EnemyHealth primary)
|
||||
private void ApplyChain(TowerDefinition def, EnemyHealth primary, PlayerSlot owner)
|
||||
{
|
||||
// Chain hit positions are collected and sent to clients for the
|
||||
// future lightning-arc visual. The list is per-fire, so allocation
|
||||
// here is acceptable; optimise to a pool if chain towers become hot.
|
||||
var hitPositions = new List<Vector3> { primary.transform.position };
|
||||
var alreadyHit = new HashSet<EnemyHealth> { primary };
|
||||
|
||||
HitEnemy(def, primary);
|
||||
HitEnemy(def, primary, owner);
|
||||
|
||||
EnemyHealth current = primary;
|
||||
for (int jump = 0; jump < def.ChainCount; jump++)
|
||||
{
|
||||
EnemyHealth next = null;
|
||||
float bestSqr = float.MaxValue;
|
||||
EnemyHealth next = null;
|
||||
float bestSqr = float.MaxValue;
|
||||
|
||||
int count = Physics.OverlapSphereNonAlloc(
|
||||
current.transform.position, def.ChainRange, s_overlapBuffer, enemyLayerMask);
|
||||
|
|
@ -313,20 +313,20 @@ namespace TD.Combat
|
|||
|
||||
alreadyHit.Add(next);
|
||||
hitPositions.Add(next.transform.position);
|
||||
HitEnemy(def, next);
|
||||
HitEnemy(def, next, owner);
|
||||
current = next;
|
||||
}
|
||||
|
||||
ChainFiredClientRpc(hitPositions.ToArray());
|
||||
}
|
||||
|
||||
private void HitEnemy(TowerDefinition def, EnemyHealth target)
|
||||
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||
{
|
||||
target.TakeDamage(def.Damage, def.DamageType);
|
||||
ApplyStatusEffect(def, target);
|
||||
target.TakeDamage(def.Damage, def.DamageType, owner);
|
||||
ApplyStatusEffect(def, target, owner);
|
||||
}
|
||||
|
||||
private void ApplyStatusEffect(TowerDefinition def, EnemyHealth target)
|
||||
private void ApplyStatusEffect(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||
{
|
||||
if (def.EffectDuration <= 0f) return;
|
||||
|
||||
|
|
@ -341,7 +341,7 @@ namespace TD.Combat
|
|||
if (magnitude <= 0f) return;
|
||||
|
||||
target.GetComponent<EnemyStatus>()
|
||||
?.ApplyEffect(def.DamageType, magnitude, def.EffectDuration);
|
||||
?.ApplyEffect(def.DamageType, magnitude, def.EffectDuration, owner);
|
||||
}
|
||||
|
||||
// ----- Projectile spawning -----------------------------------------
|
||||
|
|
@ -370,7 +370,8 @@ namespace TD.Combat
|
|||
def.DotDamagePerSecond,
|
||||
def.EffectDuration,
|
||||
def.ProjectileSpeed,
|
||||
enemyLayerMask);
|
||||
enemyLayerMask,
|
||||
towerInstance.Owner);
|
||||
|
||||
go.GetComponent<NetworkObject>().Spawn();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue