using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank;
///
/// Decides which self-buffs are missing and in what order to cast them.
///
///
///
/// Pure function of (known self-buffs, active enchantments). No host calls, no
/// state, no clock — so the buff policy can be reasoned about and tested
/// without a live session, which is the whole reason it lives here rather than
/// in the host.
///
///
/// Why the spellbook is the source of truth. The obvious reading of
/// "buff every trained and specialised skill" is to enumerate skills and map
/// each to its buff line. The client cannot do that honestly: the link between
/// a spell and the stat it modifies arrives from the server, in the
/// enchantment message, and is absent from the client's own spell table. What
/// the client does know is which spells the character has learned — and a
/// character only learns the buffs for the skills they actually use. Driving
/// from the spellbook reaches the same set without inventing a mapping the
/// client has no grounds for.
///
///
internal static class BuffPlan
{
///
/// The strongest known buff per family that is not already in force at an
/// equal or higher tier, ordered so the plan is stable between passes.
///
public static List Build(
IReadOnlyList knownSelfBuffs,
IReadOnlyList active,
double refreshWhenUnderSeconds)
{
// Best known candidate per family. Family 0 is retail's "does not
// stack" bucket: those spells share no family identity, so collapsing
// them would drop all but one unrelated buff.
var bestByFamily = new Dictionary();
var unstackable = new List();
foreach (PluginSpellInfo spell in knownSelfBuffs)
{
if (spell.Family == 0)
{
unstackable.Add(spell);
continue;
}
if (!bestByFamily.TryGetValue(spell.Family, out PluginSpellInfo held)
|| spell.Tier > held.Tier)
{
bestByFamily[spell.Family] = spell;
}
}
// Strongest in-force tier per family, and how long it has left.
var activeByFamily = new Dictionary();
var activeSpellSeconds = new Dictionary();
foreach (PluginActiveEnchantment enchantment in active)
{
activeSpellSeconds[enchantment.SpellId] = enchantment.SecondsRemaining;
if (enchantment.Family == 0)
continue;
if (!activeByFamily.TryGetValue(enchantment.Family, out var held)
|| enchantment.Tier > held.Tier)
{
activeByFamily[enchantment.Family] =
(enchantment.Tier, enchantment.SecondsRemaining);
}
}
var plan = new List();
foreach (PluginSpellInfo candidate in bestByFamily.Values)
{
if (!activeByFamily.TryGetValue(candidate.Family, out var inForce))
{
plan.Add(candidate);
continue;
}
// A weaker enchantment in force is still worth replacing: recasting
// at a higher tier supersedes it.
if (candidate.Tier > inForce.Tier
|| inForce.Seconds < refreshWhenUnderSeconds)
{
plan.Add(candidate);
}
}
foreach (PluginSpellInfo candidate in unstackable)
{
if (!activeSpellSeconds.TryGetValue(candidate.SpellId, out double seconds)
|| seconds < refreshWhenUnderSeconds)
{
plan.Add(candidate);
}
}
// Cheapest first: if mana runs out mid-pass, more buffs landed than if
// the expensive ones had gone first.
plan.Sort(static (a, b) =>
a.ManaCost != b.ManaCost
? a.ManaCost.CompareTo(b.ManaCost)
: a.SpellId.CompareTo(b.SpellId));
return plan;
}
}