// Assets/_Project/Scripts/Gameplay/EnemyAbilities/EnemyAbilityDefinition.cs using UnityEngine; using TD.Core; namespace TD.Gameplay.EnemyAbilities { /// /// Base class for one enemy ability (e.g. "split into smaller enemies on death"). Abilities /// are attached to a wave slot by the post-wave player vote, and every enemy that wave /// spawns from then on carries the whole accumulated set. /// /// /// Assignment is deterministic, not random. This system originally rolled an /// ability per individual enemy against a no-ability weight. Under the 2.0 design the players /// choose it, it applies to every enemy in the wave, and it persists for the rest of the phase /// — so the roll is gone and WaveManager reads the slot's set instead. What survived is /// the shape: one asset per kind, server-only hooks. /// /// Abilities stack. A wave can collect several across a phase's cycles, so an /// enemy holds a list and every hook below runs once per ability. Implementations must not /// assume they are the only ability on the enemy. /// /// Server-only hooks. All hooks run on the server only — /// gates every call on IsServer. They default to no-ops so a new card only overrides the /// one or two it actually needs. /// /// No per-enemy state on the asset. A single asset is shared by every enemy /// carrying the ability, across every wave and match. Anything per-instance belongs on the /// component or the enemy itself. /// public abstract class EnemyAbilityDefinition : ScriptableObject { /// Which enemy ability this asset's data belongs to. public abstract EnemyAbilityKind Kind { get; } [Header("Presentation")] [Tooltip("Name shown in debug logs and the enemy-info panel.")] public string DisplayName; [Tooltip("Short description shown in the enemy-info panel.")] [TextArea(2, 4)] public string Description; // ----- Spawn-time stat modification -------------------------------- /// /// Server-only: alter what this enemy spawns as, before it is built. Runs for every enemy /// of the buffed wave. Default no-op. /// /// /// This is the hook for cards that change what an enemy fundamentally is — flight, /// health, speed, size. It must run before the enemy exists, because spawn position /// (flyers are raised) and EnemyHealth/EnemyMovement initialization all read /// these values once and keep them. /// /// When several abilities stack, they see each other's edits in application order, /// so multiplicative modifiers compose naturally. Prefer multiplying over assigning for /// anything numeric, or the last card to run silently wins. /// public virtual void ServerModifySpawn(ref EnemySpawnContext context) { } // ----- Lifetime hooks ---------------------------------------------- /// Server-only: called once, right after the ability set is assigned (before the /// enemy's NetworkObject is spawned to clients). Default no-op. /// This ability's slot on — the /// same index receives, so per-enemy timers can be seeded here. public virtual void ServerOnSpawn(EnemyAbility instance, int abilityIndex) { } /// /// Server-only: called every frame this ability is active on a live enemy. Default no-op. /// /// This ability's slot on . Pass it /// to to reach a per-enemy float this ability owns. /// /// The asset holds no per-enemy state. One /// instance is shared by every enemy carrying the ability, so a cooldown stored in a field /// here would be shared by the entire wave — every enemy would blink in lockstep, or worse, /// race each other's writes. gives each enemy its own /// slot by reference, which is enough for the timer-driven abilities and costs no /// allocation. /// public virtual void ServerTick(EnemyAbility instance, int abilityIndex, float dt) { } /// Server-only: called the instant the enemy's HP reaches zero, before the /// death animation/despawn sequence plays. Default no-op. public virtual void ServerOnDeath(EnemyAbility instance, EnemyHealth health) { } // ----- Economy hooks ----------------------------------------------- /// /// Server-only: adjust the gold a player earns for killing this enemy. Returns the reward /// to pass on; default is unchanged. /// /// /// Chained across a stacked ability set, each ability receiving the previous one's result. /// Callers clamp the final value at zero, so returning a negative is safe but pointless. /// public virtual int ServerModifyKillReward(EnemyAbility instance, int reward) => reward; /// /// Server-only: called when this enemy reaches the defense point, after lives have been /// deducted. The hook for leak-punishing cards that cost more than lives. Default no-op. /// public virtual void ServerOnReachedGoal(EnemyAbility instance, PlayerSlot originZone) { } } }