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