using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank; /// What MossTank wants to do about the character's vitals right now. public enum VitalAction { None = 0, /// Convert stamina into mana. StaminaToMana, /// Restore stamina, so stamina-to-mana has something to convert. Revitalize, } /// Thresholds for vital upkeep, following VTank's Recharge-* settings. public sealed class VitalSettings { /// /// Whether to convert vitals at all. VTank does this by default through its /// Recharge-* thresholds, but it is surprising the first time a buff pass /// spends your stamina, so it is worth being able to turn off. /// public bool Enabled { get; set; } = true; /// Convert stamina to mana below this fraction of max mana. public double ManaFloor { get; set; } = 0.50; /// Stop converting once mana is back above this fraction. public double ManaTarget { get; set; } = 0.85; /// Refuse to drain stamina below this fraction — the conversion /// takes half your stamina, and stranding the character at zero is worse /// than being short of mana. public double StaminaFloor { get; set; } = 0.35; } /// /// Picks the vital-upkeep spell to cast, if any. /// /// /// /// The loop the user asked for: when mana runs low, convert stamina into mana; /// when that leaves stamina low, restore stamina with Revitalize, which lets /// the conversion continue. /// /// /// These spells cannot be identified by family. Retail groups the vital /// transfers by source vital, so family 89 contains both "Stamina to /// Health" and "Stamina to Mana", and family 87 both "Health to Mana" and /// "Health to Stamina". Picking the strongest tier in a family would therefore /// convert into the wrong vital roughly half the time. They are identified by /// their retail name stem instead, which is stable and comes from the same /// spell table. /// /// public static class VitalPlan { public const string StaminaToManaStem = "Stamina to Mana"; public const string RevitalizeStem = "Revitalize"; public static VitalAction Decide(ICharacterInfo character, VitalSettings settings) { if (!settings.Enabled) return VitalAction.None; double mana = Fraction(character.CurrentMana, character.MaxMana); double stamina = Fraction(character.CurrentStamina, character.MaxStamina); // Unknown vitals (no session, or nothing published yet) must not be // read as "empty" — that would cast on a character that is fine. if (character.MaxMana == 0 || character.MaxStamina == 0) return VitalAction.None; if (mana >= settings.ManaTarget) return VitalAction.None; if (mana < settings.ManaFloor) { return stamina > settings.StaminaFloor ? VitalAction.StaminaToMana : VitalAction.Revitalize; } return VitalAction.None; } /// /// The strongest castable spell whose name contains . /// public static bool TryFind( IReadOnlyList known, string stem, IReadOnlyDictionary skillLevels, int skillExcess, out PluginSpellInfo pick) { pick = default; bool found = false; foreach (PluginSpellInfo spell in known) { if (spell.Name.IndexOf(stem, StringComparison.OrdinalIgnoreCase) < 0) continue; if (spell.School != 0 && skillLevels.TryGetValue(spell.School, out uint level) && level < spell.Difficulty + skillExcess) { continue; } if (!found || spell.Tier > pick.Tier) { pick = spell; found = true; } } return found; } private static double Fraction(uint current, uint max) => max == 0 ? 1.0 : (double)current / max; }