Spell System¶
This document describes the lifecycle of a spell from definition to client animation.
Spell Data Model¶
SpellTemplate (persisted — Avalon.Domain.World)¶
Stored in the World database. Loaded at server startup into StaticData.SpellTemplates.
| Field | Type | Description |
|---|---|---|
Id |
SpellId |
Primary key |
Name |
string |
Display name |
CastTime |
uint |
Cast duration in milliseconds; 0 = instant |
Cooldown |
uint |
Cooldown in milliseconds |
Cost |
uint |
Power cost (mana / energy / fury depending on class) |
SpellScript |
string |
Script class name used to resolve the SpellScript type |
Range |
SpellRange |
Targeting range category |
Effects |
SpellEffect |
Flags: Damage, Heal, AoE, DoT, HoT, etc. |
EffectValue |
uint |
Base effect magnitude (damage dealt, healing done, etc.) |
AllowedClasses |
List<CharacterClass> |
Which classes can learn this spell |
AnimationId |
uint |
Client animation ID to play on cast (not yet propagated to packet) |
SpellMetadata (runtime — Avalon.World.Public.Spells)¶
In-memory representation loaded from SpellTemplate. Immutable (uses init properties).
public class SpellMetadata
{
public string Name { get; init; }
public float CastTime { get; init; } // seconds
public float Cooldown { get; init; } // seconds
public uint Cost { get; init; }
public string ScriptName { get; init; }
public SpellRange Range { get; init; }
public SpellEffect Effects { get; init; }
public uint EffectValue { get; init; }
public uint AnimationId { get; init; }
}
Spell Lifecycle¶
Player presses spell key
│
▼
CCharacterAttackPacket / CCastSpellPacket
│
▼
CharacterAttackHandler.Execute
├── AoE check: skip target if SpellEffect.AoE
├── Single-target: validate target exists and is in range
└── context.QueueSpell(character, target, spell)
│
▼
InstanceSpellSystem.QueueSpell
├── Power check: character.CurrentPower >= spell.Metadata.Cost?
│ └── NO → return false (client notified out-of-power)
├── Deduct power: character.CurrentPower -= Cost
└── Add SpellInstance to _spellQueue
│
▼
InstanceSpellSystem.Update (per-tick)
├── Decrement CastTimeTimer
├── Check for cast interruption (movement)
└── CastTimeTimer <= 0
│
├── Resolve SpellScript type via IScriptManager
├── Instantiate SpellScript via DI
└── SpellScript.Prepare() → add to _activeSpells
│
▼
SpellScript.Update (per-tick)
└── Script logic: hit detection, projectile movement, DoT ticks, etc.
│
▼
On impact / completion
├── Apply damage / heal to target
├── Broadcast SUnitDamagePacket or SHealPacket
└── BroadcastFinishCastAnimation (uses AnimationId)
Power Cost Deduction¶
Power deduction happens at queue time (when the spell is accepted into the queue), not at cast completion. This prevents exploiting cast interruptions to "bank" the power refund.
Power Types¶
PowerType |
Class(es) | Mechanic |
|---|---|---|
Mana |
Wizard, Healer | Depleted on cast; regenerates over time |
Energy |
Hunter | Depleted on cast; regenerates quickly |
Fury |
Warrior | Accumulation mechanic — do not deduct on cast; generated by melee and consumed differently |
None |
— | No resource check |
SpellScript.Clone()¶
SpellScript instances are cloned from a prototype when a spell is activated. The base class provides a virtual Clone() using MemberwiseClone, which handles all value-type fields. Subclasses override when they have additional mutable state.
Base Implementation¶
public virtual SpellScript Clone()
{
// Shallow clone of all value-type fields
var clone = (SpellScript)MemberwiseClone();
// Deep-clone the chained scripts list
var clonedChain = new List<SpellScript>(ChainedScripts.Count);
foreach (var chained in ChainedScripts)
clonedChain.Add(chained.Clone());
clone._chainedScripts = clonedChain;
return clone;
}
Concrete scripts with additional mutable state (e.g. an elapsed timer field) override and reset those fields:
public override SpellScript Clone()
{
var clone = (FireballScript)base.Clone();
clone._elapsedTime = 0f;
return clone;
}
Animation ID¶
SpellTemplate and SpellMetadata carry an AnimationId field. The SUnitAttackAnimationPacket is currently broadcast with a hardcoded default animation; propagating spell.Metadata.AnimationId to the packet is a pending improvement. An EF migration adding animation_id to the spell_templates table is also needed.
Reserved Animation IDs¶
| ID | Meaning |
|---|---|
| 0 | No animation (silent cast) |
| 1 | Default melee swing |
| 2+ | Spell-specific (defined in client assets) |
Creature Spell Support¶
Creatures currently always execute melee attacks. Support for caster creatures requires adding IReadOnlyList<SpellId> SpellIds to ICreatureMetadata and updating CreatureCombatScript.AttackTarget to select a spell when one is off cooldown and in range.
AoE Targeting¶
AoE spells have SpellEffect.AoE in their Effects flags. They do not require a single target in the attack packet. The CharacterAttackHandler routes AoE spells without a target directly to QueueSpell; the SpellScript is responsible for finding affected targets within its radius during Prepare() or Update().
Test Coverage¶
| Scenario |
|---|
| Sufficient power → spell queued, power deducted |
Insufficient power → QueueSpell returns false |
| Clone has independent chain list |
| Subclass clone resets mutable fields |
SpellMetadata.AnimationId preserved through clone |
Creature with spell → QueueSpell called |
| AoE spell, no target → queued |
| Non-AoE spell, no target → rejected |