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);