namespace Aeshnidae.LootTables; /// /// What each table is, in words, for the staff site - from a read of ACE's loot code /// (2026-09-24). A table the game never reads is said to be so, so nobody tunes it and /// wonders why nothing changed. /// public static class Notes { public static readonly Dictionary Classes = new() { ["TreasureProfile_Item"] = "THE top-level table for real drops: which KIND of item each item from a profile's item group is (gem, jewelry, weapon, armour, clothing, ...). A loot profile's item_Treasure_Type_Selection_Chances picks which of these tables (1-11) it uses.", ["TreasureProfile_MagicItem"] = "The same for the magic-item group (1-24): which kind of item each magic item is. Picked by a profile's magic_Item_Treasure_Type_Selection_Chances.", ["TreasureProfile_Mundane"] = "The same for the mundane group (1-8): pyreals, food, healing kits, mana stones, lockpicks, spell components. Picked by a profile's mundane_Item_Type_Selection_Chances.", ["WorkmanshipChance"] = "Workmanship (1-10) by tier - T1 to T6; tiers above 6 use T6. Every item rolls it; it scales value and damage/armour mutations.", ["SpellLevelChance"] = "Spell level (1-8) of an item's spells, by tier.", ["QualityChance"] = "Per-tier quality rolls for weapon speed and burden. Tn_QualityChances are CUMULATIVE thresholds (each value is the running total), not separate chances; QualityChancePerTier is the chance any quality applies at all.", ["ArmorTypeChance"] = "Which armour type drops, by tier (leather, chain, plate, covenant, ...).", ["ArmorModVsTypeChance"] = "Armour's elemental protection mods: TierChances is the chance per tier (1-8) that a piece gets one; the Tn tables are how good it is.", ["WeaponTypeChance"] = "Which weapon type drops. Only RetailChances is used, for every tier. The seven one-handed melee entries (sword, mace, axe, spear, unarmed, staff, dagger) all go to the same code, which ignores which one was rolled - only their combined chance matters.", ["GemClassChance"] = "Gem class (1-6) by tier: how valuable a gem drop is. Tiers above 6 use T6.", ["GemMaterialChance"] = "Which gem within a class. Only the chances can change here (a gem's value is built from this at startup).", ["HeritageChance"] = "Heritage style of armour and clothing, 24 profiles; a loot profile's unknown_Chances picks the profile.", ["Wcids.AetheriaChance"] = "Aetheria item max level (1-5) by tier, T5-T8. Values must stay 1-5.", ["CloakChance"] = "Cloak max level (1-5) by tier. Values must stay 1-5.", ["EquipmentSetChance"] = "Chance an armour piece belongs to an equipment set (true/false).", ["GearRatingChance"] = "Tier-8 gear ratings: the chance of a rating and how high.", ["PetDeviceChance"] = "Pet (summoning essence) level by tier. Values must be one of 50, 80, 100, 125, 150, 180, 200 - anything else breaks the essence roll.", ["ScrollLevelChance"] = "Scroll spell level by tier. T5_T8 is never read: at tier 5 and up a scroll is always level 7.", ["CasterSlotSpells"] = "The built-in spell on orbs, wands and staves, and nether casters.", ["SpellSelectionTable"] = "The 20 spell groups items draw their spells from (which group an item uses is decided in code).", ["CantripChance"] = "How many cantrips an item gets (Tn_NumCantrips) and their level (minor/major/epic/legendary, the _CantripLevel tables), by tier.", ["ArmorCantrips"] = "Which cantrip armour gets.", ["JewelryCantrips"] = "Which cantrip jewelry gets.", ["MeleeCantrips"] = "Which cantrip a melee weapon gets.", ["MissileCantrips"] = "Which cantrip a missile weapon gets.", ["WandCantrips"] = "Which cantrip a caster gets.", ["NumCantrips"] = "NOT USED - an old copy of the cantrip counts; CantripChance is the one the game reads.", ["TreasureItemTypeChances"] = "NOT USED for drops - only the /testlootgen command reads it. TreasureProfile_* are the real ones.", ["LootGenerationFactory"] = "Loot constants: ItemValue_TierMod (value multiplier by tier), EnchantmentChances_* (chance of each spell on armour/weapons and casters, by tier).", ["MissileMagicDefense"] = "Magic-defense bonus on melee, missile and caster weapons (behind a fixed 10% chance), by tier band.", ["Wcids"] = "The actual items: for each tier, which weenie drops and how often.", }; public static string For(Tables.Handle h) { if (h.Class.Contains("Legacy", StringComparison.Ordinal)) return "NOT USED - legacy weapon tables nothing calls."; if (h.Class is "NumCantrips" or "TreasureItemTypeChances") return Classes[h.Class]; if (h.Id is "WeaponTypeChance.T1_T4_Chances" or "WeaponTypeChance.T5_T6_Chances") return "NOT USED - only RetailChances is read."; if (h.Id is "WeaponTypeChance.MeleeChances" or "WeaponTypeChance.MissileChances") return "NOT USED for drops - test commands only."; if (h.Id.StartsWith("ScrollLevelChance.", StringComparison.Ordinal) && h.Field.Contains("T5", StringComparison.Ordinal)) return "NOT USED - tier 5 and up always rolls level 7."; if (h.Class.StartsWith("Spells.", StringComparison.Ordinal) && (h.Class.EndsWith("GemSpells") || h.Class.EndsWith("JewelrySpells"))) return "NOT USED - nothing reads this class."; if (!h.ValueEditable) return $"Values are {h.ValueType.Name} objects: only the chances can be changed here."; return ""; } // ---- what a result means -------------------------------------------------------------- /// /// What a table's results ARE, for a table whose results are bare numbers or true/false: /// the column's name (Head), a sentence (What), words for each value (Labels), and for a /// per-tier list what each position is (IndexHead / IndexLabels). From ACE's loot code, /// 2026-09-24. Null where the values already say what they are (enum names). /// public sealed record Meaning(string Head, string What, Dictionary? Labels = null, string? IndexHead = null, string[]? IndexLabels = null); private static readonly string[] Tiers = ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8"]; private static Dictionary Range(int from, int to, Func label) => Enumerable.Range(from, to - from + 1).ToDictionary(i => i.ToString(), label); private static Dictionary YesNo(string yes, string no) => new() { ["True"] = yes, ["False"] = no }; private static readonly string[] Roman = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII"]; public static Meaning? MeaningFor(Tables.Handle h) { var f = h.Field; switch (h.Class) { case "WorkmanshipChance": return new("workmanship (1-10)", "The item's workmanship. Higher raises its value and the size of its damage/armour improvements.", Range(1, 10, i => $"workmanship {i}")); case "SpellLevelChance": return new("spell level (1-8)", "The level of the item's spells: 1 is level I, 8 is level VIII.", Range(1, 8, i => $"level {Roman[i]}")); case "ScrollLevelChance": return new("scroll level (1-8)", "The spell level of a scroll that drops.", Range(1, 8, i => $"level {Roman[i]}")); case "GemClassChance": return new("gem class (1-6)", "How valuable a dropped gem is. Class 1 gems are worth 10 pyreals, class 6 worth 1,000 (GemMaterialChance's gemClassValue).", new() { ["1"] = "class 1, worth 10", ["2"] = "class 2, worth 50", ["3"] = "class 3, worth 100", ["4"] = "class 4, worth 250", ["5"] = "class 5, worth 500", ["6"] = "class 6, worth 1,000" }); case "GemMaterialChance" when f == "gemClassValue": return new("value (pyreals)", "What a gem of each class is worth. Read once at startup, so a change here only takes effect after a restart.", IndexHead: "gem class", IndexLabels: ["class 1", "class 2", "class 3", "class 4", "class 5", "class 6"]); case "CantripChance" or "NumCantrips" when f.EndsWith("NumCantrips"): return new("number of cantrips", "How many cantrips a magic item gets.", Range(0, 6, i => i == 0 ? "no cantrips" : i == 1 ? "1 cantrip" : $"{i} cantrips")); case "CantripChance" or "NumCantrips" when f.EndsWith("CantripLevel"): return new("cantrip level", "How strong each cantrip is.", new() { ["1"] = "Minor", ["2"] = "Major", ["3"] = "Epic", ["4"] = "Legendary" }); case "CloakChance": return new("max level (1-5)", "The level a dropped cloak can be raised to. Must stay 1-5.", Range(1, 5, i => $"max level {i}")); case "Wcids.AetheriaChance": return new("max level (1-5)", "The level a dropped aetheria can be raised to. Must stay 1-5.", Range(1, 5, i => $"max level {i}")); case "EquipmentSetChance": return new("part of a set?", "Whether a dropped armour piece belongs to an equipment set.", YesNo("yes - an equipment set piece", "no set")); case "GearRatingChance" when f == "RatingChance": return new("gets a rating?", "Tier 8 only: whether a piece of gear gets a rating at all. If yes, ArmorRating or ClothingJewelryRating decides how much.", YesNo("yes - gets a rating", "no rating")); case "GearRatingChance" when f == "ArmorRating": return new("rating", "Armour (and clothing with an armour level, and crowns): +N crit damage rating or +N crit damage resist rating, 50/50.", Range(1, 5, i => $"+{i} crit dmg / crit resist")); case "GearRatingChance" when f == "ClothingJewelryRating": return new("rating", "Clothing and cloaks: +N damage or damage resist rating. Jewelry: +N healing boost or max health rating. 50/50 which.", Range(1, 5, i => $"+{i} rating")); case "PetDeviceChance": return new("essence level", "The level of a summoning essence (the pet's level). Must be one of 50, 80, 100, 125, 150, 180, 200.", new() { ["50"] = "level 50", ["80"] = "level 80", ["100"] = "level 100", ["125"] = "level 125", ["150"] = "level 150", ["180"] = "level 180", ["200"] = "level 200" }); case "ArmorModVsTypeChance" when f == "TierChances": return new("chance (0-1)", "For each elemental protection on an armour piece: the chance, by tier, that it gets improved. How much is the QualityLevel tables.", IndexHead: "tier", IndexLabels: Tiers); case "ArmorModVsTypeChance": return new("quality level", "How much an improved elemental protection goes up: each level adds about 0.15 (plus a random -0.05 to +0.15), so level 3 is about +0.45, roughly one step such as Average to Above Average.", Range(1, 5, i => $"about +{i * 0.15:0.00}")); case "QualityChance" when f == "QualityChancePerTier": return new("chance (0-1)", "The chance, by tier, that a weapon's speed and an item's burden get improved at all. The Tn_QualityChances tables then decide how much.", IndexHead: "tier", IndexLabels: Tiers); case "QualityChance": return new("running total", "How good an improvement is. Each position is a quality level, and each value is a RUNNING TOTAL, not a separate chance: a roll lands on the first level whose total is above it. Positions left at 0 are never rolled, and the last one used must be 1. Weapon speed: each level makes the weapon about 2.5% faster. Burden: a higher level makes the item lighter.", IndexHead: "quality level", IndexLabels: Enumerable.Range(1, 12).Select(i => $"level {i}").ToArray()); case "LootGenerationFactory" when f == "ItemValue_TierMod": return new("value multiplier", "Scales a dropped item's pyreal value by tier.", IndexHead: "tier", IndexLabels: Tiers); case "LootGenerationFactory" when f.StartsWith("EnchantmentChances_"): return new("chance (0-1)", f.EndsWith("Caster") ? "The chance, by tier, that a magic caster (orb, wand, staff) gets its regular (non-cantrip) spell." : "The chance, by tier, that a magic armour piece or melee/missile weapon gets a regular (non-cantrip) spell.", IndexHead: "tier", IndexLabels: Tiers); case "LootGenerationFactory" when f == "numSlagChance": return new("slag count", "Olthoi play only: how much slag drops.", Range(1, 6, i => $"{i} slag")); case "LootGenerationFactory" when f == "rareSlagChance": return new("rare amount?", "Olthoi play only: whether 3 extra slag drop.", YesNo("yes - 3 extra slag", "no")); case "MissileMagicDefense": return new("magic-defense bonus", "For melee, missile and caster weapons, after a fixed 10% roll: the weapon's magic-defense multiplier.", h.Retail.Select(e => e.Value).Distinct().ToDictionary(v => v, v => float.TryParse(v, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var x) ? $"+{(x - 1) * 100:0.#}% magic defense" : v)); case "Wcids.SpellComponentWcids" when f == "level8SpellComponentChance": return new("level-8 component?", "Tier 7-8 spell components: whether it is a level-8 component (quill, ink or glyph) rather than a normal pea.", YesNo("yes - a level-8 component", "no - a normal pea")); } return h.Kind == "list" && h.Retail.Count == 8 ? new("value", "", IndexHead: "tier", IndexLabels: Tiers) : null; } }