// Assets/_Project/Scripts/Gameplay/BuilderSpells/BuilderSpellDefinition.cs using UnityEngine; using TD.Core; namespace TD.Gameplay.BuilderSpells { /// /// Base class for one builder spell — a player-cast, key-triggered ability (e.g. "drop a /// fireball on a point", "slow enemies in a radius"). Granted via the draft () and tracked per-player by , which owns the hotkey slot and cooldown state. /// /// /// Contrast with . Effects /// are passive and queried by consumers; spells are active and resolve themselves. /// is that resolution — it IS the "apply the effect" method, /// replacing the effect system's IsValidFor-only gate pattern (grant-time validity /// still lives on ). /// /// One asset per kind. is fixed per subclass, same as /// . uses /// it to build a fixed-size, enum-indexed lookup table. /// /// Server-only resolution, client-only visual. runs on /// the server and applies damage/status via the same Physics.OverlapSphereNonAlloc + /// EnemyHealth/EnemyStatus pattern TowerCombat already uses. /// Presentation is split into (on cast) and /// (on contact, seconds later for a /// projectile spell), both running on every peer including the server. /// public abstract class BuilderSpellDefinition : ScriptableObject { /// Which builder spell this asset's data belongs to. public abstract BuilderSpellKind Kind { get; } [Header("Presentation")] [Tooltip("Name shown on the draft card and the cast hotbar.")] public string DisplayName; [Tooltip("Short description shown on the draft card.")] [TextArea(2, 4)] public string Description; [Tooltip("Icon shown on the draft card and the cast hotbar.")] public Sprite Icon; [Header("Casting")] [Tooltip("Seconds before this spell can be cast again after a successful cast.")] [Min(0f)] public float Cooldown = 5f; [Tooltip("How the cast controller aims this spell. AreaOfEffect previews Radius as a " + "decal while aiming; PointTarget does not, even if Radius is used internally " + "(e.g. a splash radius).")] public SpellTargetType TargetType; [Tooltip("AreaOfEffect: the resolution radius AND the client-side preview size. " + "PointTarget: optional internal-only radius (e.g. splash) with no preview.")] [Min(0f)] public float Radius; [Tooltip("Physics layer(s) enemies occupy, queried by this spell's own OverlapSphere " + "call. Each spell asset authors its own mask — same convention as " + "TowerCombat/Projectile's per-instance enemyLayerMask.")] [SerializeField] protected LayerMask enemyLayerMask; // Shared scratch buffer for OverlapSphereNonAlloc queries. The server processes casts // sequentially (one RPC handler at a time), so a static buffer shared across all spell // assets is safe — mirrors TowerCombat.s_overlapBuffer, but sized larger (64 vs. 32): // OverlapSphereNonAlloc silently truncates past the buffer length with no way to // detect the truncation, and spells are more likely than a single tower's splash/chain // radius to catch a dense horde. TowerCombat's buffer is intentionally left at 32 — // out of scope here, flagged separately for the team to revisit. protected static readonly Collider[] s_overlapBuffer = new Collider[64]; /// /// Server-only: resolve this spell's effect at for the /// casting player. Returns false if the cast could not be applied (e.g. hit nothing) — /// treats a false return as a no-op and does not start /// the cooldown. /// public abstract bool ServerCast(ulong clientId, Vector3 targetPoint); /// /// Seconds between the cast and the effect landing. 0 = instant: damage and the impact /// sound resolve immediately on cast. A projectile-style spell (e.g. the Fireball meteor) /// overrides this with its fall/travel time, so spawns the /// visual on cast but holds (damage) and /// (sound) until the projectile reaches the ground. Set it to match the VFX's fall time. /// public virtual float ImpactDelay => 0f; /// /// Runs on every peer (via 's ClientRpc) the moment the /// spell is cast. Spawns the visual: for an instant spell that's the whole effect; for a /// delayed spell it's the projectile/travel visual that lands after . /// Default no-op. /// public virtual void ClientSpawnVfx(Vector3 targetPoint) { } /// /// Runs on every peer when the spell makes contact — immediately for an instant spell, or /// seconds after cast for a delayed one. Play the impact sound /// (and any impact-moment visual) here so it lands with the effect, not the throw. /// Default no-op. /// public virtual void ClientPlayImpact(Vector3 targetPoint) { } } }