Adding major combat changes and features
This commit is contained in:
parent
abcefcd7f1
commit
42ee0bf65d
28 changed files with 1653 additions and 46 deletions
90
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
Normal file
90
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Assets/_Project/Scripts/Gameplay/EnemyHealth.cs
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-enemy HP component. Holds replicated HP and is the single point
|
||||
/// through which all damage flows, so resistance lookups (Phase 1.5+) can
|
||||
/// be added in one place without touching every damage source.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives on the enemy prefab root alongside <see cref="EnemyStatus"/> and the
|
||||
/// future <c>EnemyMovement</c> component (Phase 1.5/1.6). HP is server-written
|
||||
/// and replicated to all clients so health bars can render on any peer.
|
||||
///
|
||||
/// <b>Death flow (server-only):</b>
|
||||
/// <c>TakeDamage</c> clamps HP to 0, fires <see cref="OnDied"/>, then calls
|
||||
/// <c>NetworkObject.Despawn</c>. Subscribers must not touch the NetworkObject
|
||||
/// after <c>OnDied</c> returns.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class EnemyHealth : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private float maxHp = 100f;
|
||||
|
||||
private readonly NetworkVariable<float> hp = new NetworkVariable<float>(
|
||||
0f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
// ----- Public state -----------------------------------------------
|
||||
|
||||
public float CurrentHp => hp.Value;
|
||||
public float MaxHp => maxHp;
|
||||
public bool IsDead => hp.Value <= 0f;
|
||||
|
||||
// Stub: set by EnemyMovement or spawner in Phase 1.5/1.6.
|
||||
// TowerCombat reads this to honour the GroundedOnly tower flag.
|
||||
public bool IsFlying => false;
|
||||
|
||||
// ----- Events -----------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Fired on the server immediately before the enemy NetworkObject is despawned.
|
||||
/// <see cref="TD.Combat.TowerCombat"/> subscribes to clear its target reference.
|
||||
/// Do not access the NetworkObject after this event returns.
|
||||
/// </summary>
|
||||
public event System.Action<EnemyHealth> OnDied;
|
||||
|
||||
// ----- NGO lifecycle ----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (IsServer)
|
||||
hp.Value = maxHp;
|
||||
}
|
||||
|
||||
// ----- Server API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Applies <paramref name="damage"/> to this enemy. Server-only; no-op on clients.
|
||||
/// <paramref name="type"/> is recorded for future resistance/weakness lookups —
|
||||
/// all damage is full-value until the resistance table is implemented (Phase 1.5+).
|
||||
/// </summary>
|
||||
public void TakeDamage(float damage, DamageType type)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (IsDead) return;
|
||||
|
||||
// STUB — resistance table slot:
|
||||
// float modified = ResistanceTable.Apply(damage, type, this);
|
||||
float modified = damage;
|
||||
|
||||
hp.Value = Mathf.Max(0f, hp.Value - modified);
|
||||
|
||||
if (hp.Value <= 0f)
|
||||
HandleDeath();
|
||||
}
|
||||
|
||||
// ----- Private ----------------------------------------------------
|
||||
|
||||
private void HandleDeath()
|
||||
{
|
||||
OnDied?.Invoke(this);
|
||||
NetworkObject.Despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/EnemyHealth.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 21dea26768768b8449a8924f638557be
|
||||
160
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
Normal file
160
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
// Assets/_Project/Scripts/Gameplay/EnemyStatus.cs
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// A single active lingering effect on an enemy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Magnitude semantics by source:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Cold — fraction of speed retained (0.5 = half speed)</item>
|
||||
/// <item>Fire — damage per second applied as a DoT tick</item>
|
||||
/// <item>Poison — damage per second applied as a DoT tick</item>
|
||||
/// <item>Others — unused (magnitude = 0)</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public struct StatusEffect
|
||||
{
|
||||
public DamageType Source;
|
||||
public float Magnitude;
|
||||
public float RemainingDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks and ticks lingering status effects (slow, burn, poison) on an enemy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Authority:</b> The active-effect list is server-local (not replicated).
|
||||
/// Only the derived <see cref="speedMultiplier"/> NetworkVariable is replicated,
|
||||
/// so <c>EnemyMovement</c> (Phase 1.5/1.6) can scale speed on all peers without
|
||||
/// re-broadcasting the full effect list.
|
||||
///
|
||||
/// <b>Stacking rule:</b> A second hit of the same <see cref="DamageType"/> refreshes
|
||||
/// the duration and magnitude rather than stacking. Cross-type interactions (e.g.
|
||||
/// Cold + Fire) are not yet implemented; <see cref="HasEffect"/> is the hook for
|
||||
/// when that design is worked out.
|
||||
///
|
||||
/// <b>DoT damage</b> is applied by calling <see cref="EnemyHealth.TakeDamage"/> each
|
||||
/// tick so resistance lookups remain in one place.
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class EnemyStatus : NetworkBehaviour
|
||||
{
|
||||
// Replicated so EnemyMovement can read it on all clients without
|
||||
// knowing anything about which effects are active.
|
||||
private readonly NetworkVariable<float> speedMultiplier = new NetworkVariable<float>(
|
||||
1f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
// Server-local — only the derived speedMultiplier NV crosses the wire.
|
||||
private readonly List<StatusEffect> activeEffects = new List<StatusEffect>();
|
||||
|
||||
// Resolved once; used by Tick for DoT TakeDamage calls.
|
||||
private EnemyHealth health;
|
||||
|
||||
// ----- NGO lifecycle -----------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
health = GetComponent<EnemyHealth>();
|
||||
}
|
||||
|
||||
// ----- Public API --------------------------------------------------
|
||||
|
||||
/// <summary>Current speed fraction (0–1). 1 = full speed, 0.5 = half speed, etc.</summary>
|
||||
public float GetSpeedMultiplier() => speedMultiplier.Value;
|
||||
|
||||
/// <summary>True if an effect of the given type is currently active on this enemy.</summary>
|
||||
public bool HasEffect(DamageType type)
|
||||
{
|
||||
for (int i = 0; i < activeEffects.Count; i++)
|
||||
if (activeEffects[i].Source == type) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies or refreshes a lingering effect. Server-only; no-op on clients.
|
||||
/// Re-hitting with the same damage type refreshes duration and magnitude.
|
||||
/// </summary>
|
||||
public void ApplyEffect(DamageType source, float magnitude, float duration)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
for (int i = 0; i < activeEffects.Count; i++)
|
||||
{
|
||||
if (activeEffects[i].Source != source) continue;
|
||||
|
||||
var e = activeEffects[i];
|
||||
e.Magnitude = magnitude;
|
||||
e.RemainingDuration = duration;
|
||||
activeEffects[i] = e;
|
||||
RecalculateSpeedMultiplier();
|
||||
return;
|
||||
}
|
||||
|
||||
activeEffects.Add(new StatusEffect
|
||||
{
|
||||
Source = source,
|
||||
Magnitude = magnitude,
|
||||
RemainingDuration = duration,
|
||||
});
|
||||
RecalculateSpeedMultiplier();
|
||||
}
|
||||
|
||||
// ----- Server tick ------------------------------------------------
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer || activeEffects.Count == 0) return;
|
||||
TickEffects(Time.deltaTime);
|
||||
}
|
||||
|
||||
private void TickEffects(float dt)
|
||||
{
|
||||
bool anyExpired = false;
|
||||
|
||||
for (int i = activeEffects.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var e = activeEffects[i];
|
||||
|
||||
// Apply DoT for Fire and Poison.
|
||||
if (e.Source == DamageType.Fire || e.Source == DamageType.Poison)
|
||||
{
|
||||
if (health != null && !health.IsDead)
|
||||
health.TakeDamage(e.Magnitude * dt, e.Source);
|
||||
}
|
||||
|
||||
e.RemainingDuration -= dt;
|
||||
if (e.RemainingDuration <= 0f)
|
||||
{
|
||||
activeEffects.RemoveAt(i);
|
||||
anyExpired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
activeEffects[i] = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyExpired)
|
||||
RecalculateSpeedMultiplier();
|
||||
}
|
||||
|
||||
private void RecalculateSpeedMultiplier()
|
||||
{
|
||||
float mult = 1f;
|
||||
for (int i = 0; i < activeEffects.Count; i++)
|
||||
{
|
||||
if (activeEffects[i].Source == DamageType.Cold)
|
||||
mult = Mathf.Min(mult, activeEffects[i].Magnitude);
|
||||
}
|
||||
speedMultiplier.Value = mult;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/EnemyStatus.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 292d1b92cd49dc74f8dfd74cdbe4ece7
|
||||
Loading…
Add table
Add a link
Reference in a new issue