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(), }; }