using System.Text.RegularExpressions;
using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank;
/// What a buff line raises.
public enum BuffTargetKind
{
Unknown = 0,
Skill,
Attribute,
}
/// One buff line: a family, what it raises, and its known tiers.
public sealed record BuffLine(
uint Family,
BuffTargetKind Kind,
string TargetName,
List Tiers);
///
/// Works out which stat each known self-buff raises, straight from retail data.
///
///
///
/// The client's spell table carries no link between a spell and the stat it
/// modifies — that arrives from the server with the enchantment. But retail
/// writes it in the spell's own description:
///
///
/// Increases the caster's Life Magic skill by 10 points.
/// Increases the caster's Strength by 10 points.
///
///
/// So the mapping is derived from shipped data rather than hard-coded, which
/// matters because the naming is genuinely irregular and no rule would cover
/// it: Invulnerability raises Melee Defense, Impregnability
/// raises Missile Defense, Fealty raises Loyalty, Sprint raises
/// Run, Arcane Enlightenment raises Arcane Lore, and — the one that
/// would silently poison any name-matching scheme — the spell line called
/// Willpower raises the attribute named Self.
///
///
public static partial class BuffProfile
{
[GeneratedRegex(
@"^Increases (?:the caster's|your) (?.+?)(?\s+skill)? by ",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex IncreasesPattern();
///
/// Retail's spell text says "Assess Monster" where the skill table says
/// "Assess Creature". Without this the skill silently never matches and its
/// buff is quietly dropped from every plan.
///
private static readonly Dictionary SkillNameAliases =
new(StringComparer.OrdinalIgnoreCase)
{
["Assess Monster"] = "Assess Creature",
};
///
/// Group the character's known self-buffs into buff lines, keeping only
/// lines that raise a stat and last long enough to be worth maintaining.
///
public static List Build(IReadOnlyList knownSelfBuffs)
{
var byFamily = new Dictionary();
foreach (PluginSpellInfo spell in knownSelfBuffs)
{
// Instantaneous spells (the vital transfers, heals) are not buffs;
// they also share families across unrelated lines, so grouping them
// by family would be wrong twice over.
if (spell.DurationSeconds <= 0f)
continue;
if (!TryParseTarget(spell.Description, out BuffTargetKind kind, out string target))
continue;
if (!byFamily.TryGetValue(spell.Family, out BuffLine? line))
{
line = new BuffLine(spell.Family, kind, target, new List());
byFamily.Add(spell.Family, line);
}
line.Tiers.Add(spell);
}
foreach (BuffLine line in byFamily.Values)
line.Tiers.Sort(static (a, b) => b.Tier.CompareTo(a.Tier)); // strongest first
return byFamily.Values.ToList();
}
/// Parse "Increases the caster's X [skill] by N points."
public static bool TryParseTarget(
string? description, out BuffTargetKind kind, out string target)
{
kind = BuffTargetKind.Unknown;
target = string.Empty;
if (string.IsNullOrEmpty(description))
return false;
Match match = IncreasesPattern().Match(description);
if (!match.Success)
return false;
target = match.Groups["target"].Value.Trim();
if (target.Length == 0)
return false;
if (SkillNameAliases.TryGetValue(target, out string? alias))
target = alias;
// The word "skill" is what separates a skill buff from an attribute
// buff in retail's own wording.
kind = match.Groups["skill"].Success ? BuffTargetKind.Skill : BuffTargetKind.Attribute;
return true;
}
}