namespace Aeshnidae.BuffBot;
///
/// The tier ladder: which of a warden's spells a town has paid to have land, at what level,
/// and what the next tier costs.
///
/// Nothing here knows a spell id. A warden's Use emote names 68 spells and every one of
/// them is sorted into one of four groups from its own spell CATEGORY - the thing that
/// makes "Strength Other I" and "Strength Other VII" the same buff - so adding a spell to
/// the emote set, or retiring one, needs no code change:
///
/// cantrip the category's enum name ends in Settings.CantripCategorySuffix ("Rare"):
/// the Prodigal cantrips, power 600
/// impen ACE's own Spell.IsImpenBaneType: Impenetrability and the seven Banes
/// protection a LifeMagic category whose name contains "Protection" and not "Ward":
/// the seven Protections. (The Ward spells are a different family and are
/// deliberately not counted as protections.)
/// buff everything else - the 53 attribute, vital, skill, weapon and aura buffs
///
/// The hub bot's 68 sort as 53 buff + 7 protection + 8 impen + 0 cantrip, which is why
/// tier 10 adds the cantrips itself (see ).
///
public static class Tiers
{
public const string Buff = "buff";
public const string Protection = "protection";
public const string ImpenBane = "impen";
public const string Cantrip = "cantrip";
/// The highest level any of these spells comes in. ACE's SpellFormula.MaxSpellLevel.
public const uint TopSpellLevel = 8;
private static readonly Dictionary _groups = new();
public static void Forget()
{
lock (_groups)
_groups.Clear();
}
/// Which group a spell belongs to, cached by category.
public static string GroupOf(Spell spell)
{
lock (_groups)
{
if (_groups.TryGetValue(spell.Category, out var known))
return known;
var name = spell.Category.ToString();
var suffix = Mod.Settings.CantripCategorySuffix;
var group =
!string.IsNullOrEmpty(suffix) && name.EndsWith(suffix, StringComparison.Ordinal) ? Cantrip
: spell.IsImpenBaneType ? ImpenBane
: name.Contains("Protection", StringComparison.Ordinal) && !name.Contains("Ward", StringComparison.Ordinal) ? Protection
: Buff;
_groups[spell.Category] = group;
return group;
}
}
///
/// The cantrip family of a spell: the category named "<this category>Rare", if the
/// client has one. Returns 0 when it does not - Dirty Fighting, Recklessness, Sneak
/// Attack, Dual Wield, Shield and Summoning mastery, Spirit Drinker and Hermetic Link
/// have no cantrip under that name, and get none.
///
public static SpellCategory CantripFor(Spell spell)
{
var suffix = Mod.Settings.CantripCategorySuffix;
if (string.IsNullOrEmpty(suffix) || GroupOf(spell) == Cantrip)
return 0;
return Enum.TryParse(spell.Category + suffix, out var rare) ? rare : 0;
}
/// The ladder step for a tier, clamped to what the settings describe.
public static Settings.TierStep Step(int tier)
{
var steps = Mod.Settings.Tiers;
if (steps is null || steps.Count == 0)
return new Settings.TierStep { Tier = tier, SpellLevel = TopSpellLevel, Groups = new() { Buff, Protection, ImpenBane } };
var exact = steps.FirstOrDefault(s => s.Tier == tier);
if (exact is not null)
return exact;
// Above the last written step, the last step applies; below the first, nothing does.
return tier > steps.Max(s => s.Tier)
? steps.OrderByDescending(s => s.Tier).First()
: steps.OrderBy(s => s.Tier).First();
}
/// Does a town at this tier let this group of spells land at all?
public static bool Allows(int tier, string group) => Step(tier).Groups?.Contains(group) == true;
/// The spell level a town at this tier allows. 0 = nothing.
public static uint LevelAt(int tier) => Step(tier).SpellLevel;
// ---------------------------------------------------------------- what it costs
///
/// What the step from - 1 up to costs.
/// Geometric: the first tier's price times the growth multiplier once per tier after
/// it, both currencies owed in full. Tier 0 and anything past MaxTier cost nothing
/// because they cannot be bought.
///
public static (long Radiance, long Pyreals) Price(int tier)
{
if (tier <= 0 || tier > Mod.Settings.MaxTier)
return (0, 0);
var steps = tier - 1;
var radiance = Mod.Settings.RadianceFirstTier * Math.Pow(Math.Max(1.0, Mod.Settings.RadianceGrowth), steps);
var pyreals = Mod.Settings.PyrealsFirstTier * Math.Pow(Math.Max(1.0, Mod.Settings.PyrealsGrowth), steps);
return (Round(radiance), Round(pyreals));
}
/// Prices are shown to players, so they are rounded to something a player can read.
private static long Round(double value)
{
if (value <= 0 || double.IsNaN(value) || double.IsInfinity(value))
return 0;
if (value >= long.MaxValue / 2.0)
return long.MaxValue / 2;
var scale = value >= 1_000_000 ? 100_000.0 : value >= 10_000 ? 1_000.0 : 1.0;
return (long)(Math.Round(value / scale) * scale);
}
/// One line a player can read: what a tier gives.
public static string Describe(int tier)
{
var step = Step(tier);
if (step.SpellLevel == 0 || step.Groups is null || step.Groups.Count == 0)
return "nothing at all";
var parts = new List { $"the standard buffs at level {Roman(step.SpellLevel)}" };
if (step.Groups.Contains(Protection)) parts.Add("the seven protections");
if (step.Groups.Contains(ImpenBane)) parts.Add("Impenetrability and the banes");
if (step.Groups.Contains(Cantrip)) parts.Add("cantrips");
return string.Join(", ", parts);
}
public static string Roman(uint level) => level switch
{
1 => "I", 2 => "II", 3 => "III", 4 => "IV",
5 => "V", 6 => "VI", 7 => "VII", 8 => "VIII",
_ => level.ToString(),
};
}