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