// Assets/_Project/Scripts/Gameplay/EnemyAbilities/SplitOnDeathAbilityDefinition.cs using UnityEngine; using TD.Core; namespace TD.Gameplay.EnemyAbilities { /// /// On death, spawns smaller copies of the dying enemy's own type at /// the corpse's position, continuing on toward the goal instead of restarting from the wave's /// spawner. /// /// /// No separate minion asset. The copies reuse whichever /// and prefab the dying enemy was spawned from; every stat is expressed here as a percentage /// of what the parent actually spawned with, so one card covers any enemy it lands on. /// /// Percentages are of the PARENT, not the asset. If other cards on the same wave /// have already modified the enemy, the minions scale off the modified values — a split of a /// buffed enemy produces buffed minions, which is what players will expect from watching it. /// /// Minions inherit no abilities at all — not even this one. That is what makes the /// recursion terminate: a minion of a splitting enemy is a plain enemy, so a wave carrying /// Split produces exactly one extra generation regardless of how many other cards it has /// collected. Enforced at the spawn site, not here. /// [CreateAssetMenu(fileName = "SplitOnDeathAbility", menuName = "TD/Enemy Abilities/Split On Death")] public class SplitOnDeathAbilityDefinition : EnemyAbilityDefinition { public override EnemyAbilityKind Kind => EnemyAbilityKind.SplitOnDeath; [Header("Split")] [Tooltip("How many copies spawn when the parent dies.")] [Min(1)] public int SplitCount = 2; [Header("Minion stats (percent of the parent's spawned values)")] [Tooltip("Minion max HP as a fraction of the parent's max HP.")] [Range(0.01f, 2f)] public float HpPercent = 0.5f; [Tooltip("Minion move speed as a fraction of the parent's speed. Above 1 makes the split " + "faster than what died — the classic 'smaller and quicker' read.")] [Range(0.01f, 3f)] public float SpeedPercent = 1.25f; [Tooltip("Minion transform scale as a fraction of the parent's scale. Requires the enemy " + "prefab's NetworkTransform to sync scale, or minions render full-size on remote peers.")] [Range(0.1f, 2f)] public float ScalePercent = 0.6f; [Tooltip("Minion lives cost as a fraction of the parent's, rounded down but never below 1 " + "— a leak always has to hurt, or splits become a way to farm free ground.")] [Range(0f, 2f)] public float LivesCostPercent = 1f; [Tooltip("Minion flight height as a fraction of the parent's. Only meaningful when the " + "parent is flying; ground splits ignore it.")] [Range(0.1f, 2f)] public float FlightHeightPercent = 1f; [Header("Placement")] [Tooltip("Random scatter radius (world units) applied to each spawn so minions don't " + "stack exactly on top of each other.")] [Min(0f)] public float ScatterRadius = 0.5f; public override void ServerOnDeath(EnemyAbility instance, EnemyHealth health) { var def = health.Definition; if (def == null) return; var movement = instance.GetComponent(); var atTile = GridCoordinates.WorldToGrid(instance.transform.position); var ownerSlot = movement != null ? movement.OriginZone : PlayerSlot.None; // Derive the minion's stats from what the parent actually spawned as. var parent = instance.SpawnContext; var minion = new EnemySpawnContext { MaxHp = Mathf.Max(1f, parent.MaxHp * HpPercent), MoveSpeed = Mathf.Max(0.01f, parent.MoveSpeed * SpeedPercent), IsFlying = parent.IsFlying, FlightHeight = parent.FlightHeight * FlightHeightPercent, LivesCost = Mathf.Max(1, Mathf.FloorToInt(parent.LivesCost * LivesCostPercent)), VisualScale = parent.VisualScale * ScalePercent, }; WaveManager.Instance?.ServerSpawnSplitEnemies( def, SplitCount, atTile, ownerSlot, ScatterRadius, minion); } } }