using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace AcDream.Core.Spells; /// /// Client-side spellbook mirror. Tracks which spells the player has /// learned (UpdateSpell 0x02C1) + a parallel active-enchantment /// table keyed by layer id (UpdateEnchantment 0x02C2 etc.). /// /// /// The UI binds to the collection-changed events so the spellbook /// panel + active-buff bar redraw automatically when the server /// pushes changes. /// /// public sealed class Spellbook { private readonly HashSet _learnedSpells = new(); private readonly ConcurrentDictionary _activeByLayer = new(); private readonly SpellTable _table; /// /// Build a Spellbook with an optional /// metadata source. When provided, /// returns the static spell descriptor (name / school / family / /// icon / mana / duration / etc.). When absent (back-compat for /// existing constructors / tests), /// always returns false. /// public Spellbook(SpellTable? table = null) { _table = table ?? SpellTable.Empty; } /// /// Look up static spell metadata. Closes ISSUES.md #11 — feeds the /// buff bar UI labels + icons + the family-stacking aggregation in /// EnchantmentMath (issue #6). /// public bool TryGetMetadata(uint spellId, out SpellMetadata meta) => _table.TryGet(spellId, out meta); /// The spell-metadata table this spellbook was built with. /// Returns if none was provided. public SpellTable Metadata => _table; /// /// Issue #6 — combined buff modifier for a vital stat. Aggregates /// over through /// with family-stacking dedup /// from the spell metadata table. Returns /// when no buffs /// apply (or when no SpellTable was wired). /// public EnchantmentMath.VitalMod GetVitalMod(uint statKey) => EnchantmentMath.GetMod(ActiveEnchantments, _table, statKey); /// Fires when a spell is added to the player's spellbook. public event Action? SpellLearned; /// Fires when a spell is removed (rare — usually on respec / admin). public event Action? SpellForgotten; /// Fires when an enchantment is added / refreshed. public event Action? EnchantmentAdded; /// Fires when an enchantment is removed (expired / dispelled). public event Action? EnchantmentRemoved; /// All currently learned spell ids. public IReadOnlyCollection LearnedSpells => _learnedSpells; /// All currently-active enchantments. public IEnumerable ActiveEnchantments => _activeByLayer.Values; public int LearnedCount => _learnedSpells.Count; public int ActiveCount => _activeByLayer.Count; public bool Knows(uint spellId) => _learnedSpells.Contains(spellId); // ── Inbound handlers ───────────────────────────────────────────────────── /// 0x02C1 MagicUpdateSpell: learn a spell. public void OnSpellLearned(uint spellId) { if (_learnedSpells.Add(spellId)) SpellLearned?.Invoke(spellId); } /// 0x01A8 MagicRemoveSpell: forget a spell. public void OnSpellForgotten(uint spellId) { if (_learnedSpells.Remove(spellId)) SpellForgotten?.Invoke(spellId); } /// 0x02C2 MagicUpdateEnchantment: enchantment added / refreshed. public void OnEnchantmentAdded(uint spellId, uint layerId, float duration, uint casterGuid) { var record = new ActiveEnchantmentRecord(spellId, layerId, duration, casterGuid); _activeByLayer[layerId] = record; EnchantmentAdded?.Invoke(record); } /// /// Issue #7 / #12 — accept a fully-populated record from /// PlayerDescription's enchantment block (which carries /// the StatMod triad + bucket). Used when the wire-format extension /// gives us the full per-enchantment payload, rather than the /// 4-field summary from MagicUpdateEnchantment. /// public void OnEnchantmentAdded(ActiveEnchantmentRecord record) { _activeByLayer[record.LayerId] = record; EnchantmentAdded?.Invoke(record); } /// 0x02C3 / 0x02C7 MagicRemove/DispelEnchantment. public void OnEnchantmentRemoved(uint layerId, uint spellId) { if (_activeByLayer.TryRemove(layerId, out var record)) EnchantmentRemoved?.Invoke(record); else EnchantmentRemoved?.Invoke(new ActiveEnchantmentRecord(spellId, layerId, 0f, 0)); } /// 0x02C6 MagicPurgeEnchantments: clear all active buffs. public void OnPurgeAll() { foreach (var rec in _activeByLayer.Values) EnchantmentRemoved?.Invoke(rec); _activeByLayer.Clear(); } public void Clear() { _learnedSpells.Clear(); _activeByLayer.Clear(); } } /// /// Summary of one active enchantment layer on the player. The /// optional StatMod fields (issue #12) carry the wire-level /// `_smod` triad (type, key, val) when available — only /// `PlayerDescription`'s enchantment block currently populates these /// (). /// `MagicUpdateEnchantment` events still produce records with these /// fields null until the wire parser is extended. /// /// /// tells EnchantmentMath whether this /// enchantment's StatMod is multiplicative (0x01), additive /// (0x02), cooldown (0x04), or vitae (0x08) per /// the retail EnchantmentMask classification. /// /// public readonly record struct ActiveEnchantmentRecord( uint SpellId, uint LayerId, float Duration, uint CasterGuid, uint? StatModType = null, uint? StatModKey = null, float? StatModValue = null, uint Bucket = 0);