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