Compare commits
1 commit
main
...
slow-area-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7419e9a140 |
2 changed files with 97 additions and 20 deletions
|
|
@ -9,11 +9,16 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
/// Builder spell #2 — slow every enemy within an area for a duration.
|
/// Builder spell #2 — slow every enemy within an area for a duration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <see cref="BuilderSpellDefinition.TargetType"/> is <see cref="SpellTargetType.AreaOfEffect"/>
|
/// <para><see cref="BuilderSpellDefinition.TargetType"/> is <see cref="SpellTargetType.AreaOfEffect"/>
|
||||||
/// — the cast controller previews <see cref="BuilderSpellDefinition.Radius"/> as a decal
|
/// — the cast controller previews <see cref="BuilderSpellDefinition.Radius"/> as a decal
|
||||||
/// while aiming, and this spell resolves against that same radius (no separate field).
|
/// while aiming, and this spell resolves against that same radius (no separate field).
|
||||||
/// Reuses the existing slow/DoT system (<see cref="EnemyStatus.ApplyEffect"/>) rather than
|
/// </para>
|
||||||
/// introducing a new status mechanism.
|
/// <para><b>Zone, not a snapshot.</b> <see cref="ServerCast"/> spawns a <see
|
||||||
|
/// cref="SlowZoneRuntime"/> that keeps re-checking the area for <see cref="EffectDuration"/>
|
||||||
|
/// seconds, so only enemies currently standing inside the radius are ever slowed — walking
|
||||||
|
/// out drops the slow almost immediately. Always succeeds and starts the cooldown, even if
|
||||||
|
/// no enemy is in the radius at the moment of cast, since the zone can still catch one later.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[CreateAssetMenu(fileName = "SlowAreaSpell", menuName = "TD/Builder Spells/Slow Area")]
|
[CreateAssetMenu(fileName = "SlowAreaSpell", menuName = "TD/Builder Spells/Slow Area")]
|
||||||
public class SlowAreaSpellDefinition : BuilderSpellDefinition
|
public class SlowAreaSpellDefinition : BuilderSpellDefinition
|
||||||
|
|
@ -35,7 +40,8 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)]
|
||||||
public float SlowFactor = 0.5f;
|
public float SlowFactor = 0.5f;
|
||||||
|
|
||||||
[Tooltip("Seconds the slow lasts. Re-casting on an already-slowed enemy refreshes it.")]
|
[Tooltip("Seconds the zone itself persists. Enemies are only slowed while standing " +
|
||||||
|
"inside it — walking out drops the slow almost immediately.")]
|
||||||
[Min(0f)]
|
[Min(0f)]
|
||||||
public float EffectDuration = 3f;
|
public float EffectDuration = 3f;
|
||||||
|
|
||||||
|
|
@ -43,23 +49,14 @@ namespace TD.Gameplay.BuilderSpells
|
||||||
{
|
{
|
||||||
PlayerSlot owner = PlayerMatchState.SlotForClient(clientId);
|
PlayerSlot owner = PlayerMatchState.SlotForClient(clientId);
|
||||||
|
|
||||||
int count = Physics.OverlapSphereNonAlloc(
|
var zoneObject = new GameObject("SlowZone (server)");
|
||||||
targetPoint, Mathf.Max(Radius, 0.01f), s_overlapBuffer, enemyLayerMask);
|
zoneObject.AddComponent<SlowZoneRuntime>().Initialize(
|
||||||
|
targetPoint, Mathf.Max(Radius, 0.01f), SlowFactor, EffectDuration,
|
||||||
|
enemyLayerMask, owner);
|
||||||
|
|
||||||
bool hitAny = false;
|
// Always succeeds — placing the zone starts the cooldown even if no enemy is
|
||||||
for (int i = 0; i < count; i++)
|
// standing in it yet, since it keeps slowing whoever walks in for EffectDuration.
|
||||||
{
|
return true;
|
||||||
var enemyHealth = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
|
||||||
if (enemyHealth == null || enemyHealth.IsDead) continue;
|
|
||||||
|
|
||||||
var enemyStatus = s_overlapBuffer[i].GetComponent<EnemyStatus>();
|
|
||||||
if (enemyStatus == null) continue;
|
|
||||||
|
|
||||||
enemyStatus.ApplyEffect(DamageType.Cold, SlowFactor, EffectDuration, owner);
|
|
||||||
hitAny = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hitAny;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ClientSpawnVfx(Vector3 targetPoint)
|
public override void ClientSpawnVfx(Vector3 targetPoint)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuilderSpells/SlowZoneRuntime.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Gameplay.BuilderSpells
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only runtime object for a Slow Area cast. Re-scans its radius every frame for the
|
||||||
|
/// rest of the zone's lifetime, continuously refreshing the Cold status on whatever enemies
|
||||||
|
/// currently overlap it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Zone, not a snapshot.</b> Replaces <see cref="SlowAreaSpellDefinition"/>'s old
|
||||||
|
/// single <c>OverlapSphereNonAlloc</c> call at cast time, which slowed whoever was standing
|
||||||
|
/// there for a fixed duration regardless of whether they walked out. This ticks continuously
|
||||||
|
/// instead, so only enemies currently inside the radius are ever affected.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Refresh, not enter/exit events.</b> Each tick re-applies
|
||||||
|
/// <see cref="EnemyStatus.ApplyEffect"/> with a short fixed window (<see cref="RefreshWindow"/>),
|
||||||
|
/// not the zone's remaining lifetime. An enemy that walks out of the zone simply stops being
|
||||||
|
/// refreshed and the slow expires on its own <see cref="RefreshWindow"/> seconds later —
|
||||||
|
/// no explicit exit detection needed.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Not a NetworkObject.</b> It only ever runs where <see
|
||||||
|
/// cref="SlowAreaSpellDefinition.ServerCast"/> runs (the server), and it never needs to exist
|
||||||
|
/// on clients — the zone's visual is spawned independently on every peer via
|
||||||
|
/// <see cref="SlowAreaSpellDefinition.ClientSpawnVfx"/>.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class SlowZoneRuntime : MonoBehaviour
|
||||||
|
{
|
||||||
|
// Comfortably longer than one frame so a single missed tick doesn't let the slow
|
||||||
|
// visibly drop while an enemy is still inside, but short enough that leaving the
|
||||||
|
// zone reads as an almost-immediate loss of the slow.
|
||||||
|
private const float RefreshWindow = 0.35f;
|
||||||
|
|
||||||
|
private static readonly Collider[] s_overlapBuffer = new Collider[64];
|
||||||
|
|
||||||
|
private float radius;
|
||||||
|
private float slowFactor;
|
||||||
|
private float remainingLifetime;
|
||||||
|
private LayerMask enemyLayerMask;
|
||||||
|
private PlayerSlot owner;
|
||||||
|
|
||||||
|
/// <summary>Positions and configures the zone. Call immediately after AddComponent.</summary>
|
||||||
|
public void Initialize(Vector3 position, float radius, float slowFactor, float lifetime,
|
||||||
|
LayerMask enemyLayerMask, PlayerSlot owner)
|
||||||
|
{
|
||||||
|
transform.position = position;
|
||||||
|
this.radius = radius;
|
||||||
|
this.slowFactor = slowFactor;
|
||||||
|
this.remainingLifetime = lifetime;
|
||||||
|
this.enemyLayerMask = enemyLayerMask;
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
remainingLifetime -= Time.deltaTime;
|
||||||
|
if (remainingLifetime <= 0f)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = Physics.OverlapSphereNonAlloc(
|
||||||
|
transform.position, radius, s_overlapBuffer, enemyLayerMask);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var enemyHealth = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
||||||
|
if (enemyHealth == null || enemyHealth.IsDead) continue;
|
||||||
|
|
||||||
|
var enemyStatus = s_overlapBuffer[i].GetComponent<EnemyStatus>();
|
||||||
|
if (enemyStatus == null) continue;
|
||||||
|
|
||||||
|
enemyStatus.ApplyEffect(DamageType.Cold, slowFactor, RefreshWindow, owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue