package main import "math" // od.go computes the Mag-Tools "OD" (over-damage vs best retail) rating for // melee/missile/wand weapons, ported from UtilityBelt's // Lib/ItemInfoHelper/{WeaponMods,MiscCalcs,Dictionaries}.cs and // Tools/ItemInfo.cs (CalcMeleeDamage / CalcMissileDamage / the OD display // lines ~631-647). See docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md. // bestKey mirrors the (Skill, Mastery, MultiStrike, WieldReq) DataTable.Select // key UB uses for an EXACT match lookup — no nearest-tier rounding. type bestKey struct { skill int mastery int multiStrike int wieldReq int } // bestValue mirrors the trailing DataTable columns: // (MaxDmg, MaxVar, MaxDmgMod, MaxElementalDmgBonus, MaxElementalDmgVsMonsters). type bestValue struct { maxDmg float64 maxVar float64 maxDmgMod float64 maxElemBonus float64 maxElemVsMon float64 } // bestValues is transcribed row-for-row from // UtilityBelt/UtilityBelt/Lib/ItemInfoHelper/WeaponMods.cs BestValuesDatatable.GetTable(). // Column order in the source Rows.Add is: // (Skill, Mastery, MultiStrike, WieldReq, MaxDmg, MaxVar, MaxDmgMod, MaxElementalDmgBonus, MaxElementalDmgVsMonsters) // — omitted trailing columns default to 0. 301 rows total (matches // `grep -c "table.Rows.Add" WeaponMods.cs`). var bestValues = map[bestKey]bestValue{ // heavyweaponry (skill 44) http://acpedia.org/wiki/Heavy_Weaponry // axe {44, 3, 0, 0}: {26, .90, 0, 0, 0}, {44, 3, 0, 250}: {33, .90, 0, 0, 0}, {44, 3, 0, 300}: {40, .90, 0, 0, 0}, {44, 3, 0, 325}: {47, .90, 0, 0, 0}, {44, 3, 0, 350}: {54, .90, 0, 0, 0}, {44, 3, 0, 370}: {61, .90, 0, 0, 0}, {44, 3, 0, 400}: {69, .90, 0, 0, 0}, {44, 3, 0, 420}: {71, .90, 0, 0, 0}, {44, 3, 0, 430}: {74, .90, 0, 0, 0}, // dagger {44, 6, 0, 0}: {24, .47, 0, 0, 0}, {44, 6, 0, 250}: {31, .47, 0, 0, 0}, {44, 6, 0, 300}: {38, .47, 0, 0, 0}, {44, 6, 0, 325}: {45, .47, 0, 0, 0}, {44, 6, 0, 350}: {51, .47, 0, 0, 0}, {44, 6, 0, 370}: {58, .47, 0, 0, 0}, {44, 6, 0, 400}: {65, .47, 0, 0, 0}, {44, 6, 0, 420}: {68, .47, 0, 0, 0}, {44, 6, 0, 430}: {71, .47, 0, 0, 0}, // msdagger {44, 6, 1, 0}: {13, .40, 0, 0, 0}, {44, 6, 1, 250}: {16, .40, 0, 0, 0}, {44, 6, 1, 300}: {20, .40, 0, 0, 0}, {44, 6, 1, 325}: {23, .40, 0, 0, 0}, {44, 6, 1, 350}: {26, .40, 0, 0, 0}, {44, 6, 1, 370}: {30, .40, 0, 0, 0}, {44, 6, 1, 400}: {33, .40, 0, 0, 0}, {44, 6, 1, 420}: {36, .40, 0, 0, 0}, {44, 6, 1, 430}: {38, .40, 0, 0, 0}, // mace {44, 4, 0, 0}: {22, .30, 0, 0, 0}, {44, 4, 0, 250}: {29, .30, 0, 0, 0}, {44, 4, 0, 300}: {36, .30, 0, 0, 0}, {44, 4, 0, 325}: {43, .30, 0, 0, 0}, {44, 4, 0, 350}: {49, .30, 0, 0, 0}, {44, 4, 0, 370}: {56, .30, 0, 0, 0}, {44, 4, 0, 400}: {63, .30, 0, 0, 0}, {44, 4, 0, 420}: {66, .30, 0, 0, 0}, {44, 4, 0, 430}: {69, .30, 0, 0, 0}, // spear {44, 5, 0, 0}: {25, .59, 0, 0, 0}, {44, 5, 0, 250}: {32, .59, 0, 0, 0}, {44, 5, 0, 300}: {39, .59, 0, 0, 0}, {44, 5, 0, 325}: {46, .59, 0, 0, 0}, {44, 5, 0, 350}: {58, .59, 0, 0, 0}, {44, 5, 0, 370}: {59, .59, 0, 0, 0}, {44, 5, 0, 400}: {66, .59, 0, 0, 0}, {44, 5, 0, 420}: {69, .59, 0, 0, 0}, {44, 5, 0, 430}: {72, .59, 0, 0, 0}, // sword {44, 2, 0, 0}: {24, .47, 0, 0, 0}, {44, 2, 0, 250}: {31, .47, 0, 0, 0}, {44, 2, 0, 300}: {38, .47, 0, 0, 0}, {44, 2, 0, 325}: {45, .47, 0, 0, 0}, {44, 2, 0, 350}: {51, .47, 0, 0, 0}, {44, 2, 0, 370}: {58, .47, 0, 0, 0}, {44, 2, 0, 400}: {65, .47, 0, 0, 0}, {44, 2, 0, 420}: {68, .47, 0, 0, 0}, {44, 2, 0, 430}: {71, .47, 0, 0, 0}, // mssword {44, 2, 1, 0}: {13, .40, 0, 0, 0}, {44, 2, 1, 250}: {16, .40, 0, 0, 0}, {44, 2, 1, 300}: {20, .40, 0, 0, 0}, {44, 2, 1, 325}: {23, .40, 0, 0, 0}, {44, 2, 1, 350}: {26, .40, 0, 0, 0}, {44, 2, 1, 370}: {30, .40, 0, 0, 0}, {44, 2, 1, 400}: {33, .40, 0, 0, 0}, {44, 2, 1, 420}: {36, .40, 0, 0, 0}, {44, 2, 1, 430}: {38, .40, 0, 0, 0}, // staff {44, 7, 0, 0}: {23, .38, 0, 0, 0}, {44, 7, 0, 250}: {30, .38, 0, 0, 0}, {44, 7, 0, 300}: {36, .38, 0, 0, 0}, {44, 7, 0, 325}: {43, .38, 0, 0, 0}, {44, 7, 0, 350}: {50, .38, 0, 0, 0}, {44, 7, 0, 370}: {56, .38, 0, 0, 0}, {44, 7, 0, 400}: {63, .38, 0, 0, 0}, {44, 7, 0, 420}: {66, .38, 0, 0, 0}, {44, 7, 0, 430}: {70, .38, 0, 0, 0}, // ua {44, 1, 0, 0}: {20, .44, 0, 0, 0}, {44, 1, 0, 250}: {26, .44, 0, 0, 0}, {44, 1, 0, 300}: {31, .44, 0, 0, 0}, {44, 1, 0, 325}: {37, .44, 0, 0, 0}, {44, 1, 0, 350}: {43, .44, 0, 0, 0}, {44, 1, 0, 370}: {48, .44, 0, 0, 0}, {44, 1, 0, 400}: {54, .44, 0, 0, 0}, {44, 1, 0, 420}: {56, .44, 0, 0, 0}, {44, 1, 0, 430}: {59, .44, 0, 0, 0}, // light weaponry (skill 45) http://acpedia.org/wiki/Light_Weaponry // axe {45, 3, 0, 0}: {22, .80, 0, 0, 0}, {45, 3, 0, 250}: {28, .80, 0, 0, 0}, {45, 3, 0, 300}: {33, .80, 0, 0, 0}, {45, 3, 0, 325}: {39, .80, 0, 0, 0}, {45, 3, 0, 350}: {44, .80, 0, 0, 0}, {45, 3, 0, 370}: {50, .80, 0, 0, 0}, {45, 3, 0, 400}: {55, .80, 0, 0, 0}, {45, 3, 0, 420}: {57, .80, 0, 0, 0}, {45, 3, 0, 430}: {61, .80, 0, 0, 0}, // dagger {45, 6, 0, 0}: {18, .42, 0, 0, 0}, {45, 6, 0, 250}: {24, .42, 0, 0, 0}, {45, 6, 0, 300}: {29, .42, 0, 0, 0}, {45, 6, 0, 325}: {35, .42, 0, 0, 0}, {45, 6, 0, 350}: {40, .42, 0, 0, 0}, {45, 6, 0, 370}: {46, .42, 0, 0, 0}, {45, 6, 0, 400}: {51, .42, 0, 0, 0}, {45, 6, 0, 420}: {54, .42, 0, 0, 0}, {45, 6, 0, 430}: {58, .42, 0, 0, 0}, // msdagger {45, 6, 1, 0}: {7, .24, 0, 0, 0}, {45, 6, 1, 250}: {11, .24, 0, 0, 0}, {45, 6, 1, 300}: {13, .24, 0, 0, 0}, {45, 6, 1, 325}: {16, .24, 0, 0, 0}, {45, 6, 1, 350}: {18, .24, 0, 0, 0}, {45, 6, 1, 370}: {21, .24, 0, 0, 0}, {45, 6, 1, 400}: {24, .24, 0, 0, 0}, {45, 6, 1, 420}: {27, .24, 0, 0, 0}, {45, 6, 1, 430}: {28, .24, 0, 0, 0}, // mace {45, 4, 0, 0}: {19, .23, 0, 0, 0}, {45, 4, 0, 250}: {24, .23, 0, 0, 0}, {45, 4, 0, 300}: {29, .23, 0, 0, 0}, {45, 4, 0, 325}: {35, .23, 0, 0, 0}, {45, 4, 0, 350}: {40, .23, 0, 0, 0}, {45, 4, 0, 370}: {45, .23, 0, 0, 0}, {45, 4, 0, 400}: {50, .23, 0, 0, 0}, {45, 4, 0, 420}: {53, .23, 0, 0, 0}, {45, 4, 0, 430}: {57, .23, 0, 0, 0}, // spear {45, 5, 0, 0}: {21, .65, 0, 0, 0}, {45, 5, 0, 250}: {26, .65, 0, 0, 0}, {45, 5, 0, 300}: {32, .65, 0, 0, 0}, {45, 5, 0, 325}: {37, .65, 0, 0, 0}, {45, 5, 0, 350}: {42, .65, 0, 0, 0}, {45, 5, 0, 370}: {48, .65, 0, 0, 0}, {45, 5, 0, 400}: {53, .65, 0, 0, 0}, {45, 5, 0, 420}: {57, .65, 0, 0, 0}, {45, 5, 0, 430}: {60, .65, 0, 0, 0}, // sword {45, 2, 0, 0}: {20, .42, 0, 0, 0}, {45, 2, 0, 250}: {25, .42, 0, 0, 0}, {45, 2, 0, 300}: {31, .42, 0, 0, 0}, {45, 2, 0, 325}: {36, .42, 0, 0, 0}, {45, 2, 0, 350}: {41, .42, 0, 0, 0}, {45, 2, 0, 370}: {47, .42, 0, 0, 0}, {45, 2, 0, 400}: {52, .42, 0, 0, 0}, {45, 2, 0, 420}: {55, .42, 0, 0, 0}, {45, 2, 0, 430}: {58, .42, 0, 0, 0}, // mssword {45, 2, 1, 0}: {7, .24, 0, 0, 0}, {45, 2, 1, 250}: {10, .24, 0, 0, 0}, {45, 2, 1, 300}: {13, .24, 0, 0, 0}, {45, 2, 1, 325}: {16, .24, 0, 0, 0}, {45, 2, 1, 350}: {18, .24, 0, 0, 0}, {45, 2, 1, 370}: {21, .24, 0, 0, 0}, {45, 2, 1, 400}: {24, .24, 0, 0, 0}, {45, 2, 1, 420}: {25, .24, 0, 0, 0}, {45, 2, 1, 430}: {28, .24, 0, 0, 0}, // staff {45, 7, 0, 0}: {19, .325, 0, 0, 0}, {45, 7, 0, 250}: {24, .325, 0, 0, 0}, {45, 7, 0, 300}: {30, .325, 0, 0, 0}, {45, 7, 0, 325}: {35, .325, 0, 0, 0}, {45, 7, 0, 350}: {40, .325, 0, 0, 0}, {45, 7, 0, 370}: {46, .325, 0, 0, 0}, {45, 7, 0, 400}: {51, .325, 0, 0, 0}, {45, 7, 0, 420}: {54, .325, 0, 0, 0}, {45, 7, 0, 430}: {57, .325, 0, 0, 0}, // ua {45, 1, 0, 0}: {17, .43, 0, 0, 0}, {45, 1, 0, 250}: {23, .43, 0, 0, 0}, {45, 1, 0, 300}: {27, .43, 0, 0, 0}, {45, 1, 0, 325}: {31, .43, 0, 0, 0}, {45, 1, 0, 350}: {35, .43, 0, 0, 0}, {45, 1, 0, 370}: {40, .43, 0, 0, 0}, {45, 1, 0, 400}: {44, .43, 0, 0, 0}, {45, 1, 0, 420}: {46, .43, 0, 0, 0}, {45, 1, 0, 430}: {48, .43, 0, 0, 0}, // finesse weaponry (skill 46) http://acpedia.org/wiki/Finesse_Weaponry // axe {46, 3, 0, 0}: {22, .80, 0, 0, 0}, {46, 3, 0, 250}: {28, .80, 0, 0, 0}, {46, 3, 0, 300}: {33, .80, 0, 0, 0}, {46, 3, 0, 325}: {39, .80, 0, 0, 0}, {46, 3, 0, 350}: {44, .80, 0, 0, 0}, {46, 3, 0, 370}: {50, .80, 0, 0, 0}, {46, 3, 0, 400}: {55, .80, 0, 0, 0}, {46, 3, 0, 420}: {57, .80, 0, 0, 0}, {46, 3, 0, 430}: {61, .80, 0, 0, 0}, // dagger {46, 6, 0, 0}: {18, .42, 0, 0, 0}, {46, 6, 0, 250}: {24, .42, 0, 0, 0}, {46, 6, 0, 300}: {29, .42, 0, 0, 0}, {46, 6, 0, 325}: {35, .42, 0, 0, 0}, {46, 6, 0, 350}: {40, .42, 0, 0, 0}, {46, 6, 0, 370}: {46, .42, 0, 0, 0}, {46, 6, 0, 400}: {51, .42, 0, 0, 0}, {46, 6, 0, 420}: {54, .42, 0, 0, 0}, {46, 6, 0, 430}: {58, .42, 0, 0, 0}, // msdagger {46, 6, 1, 0}: {7, .24, 0, 0, 0}, {46, 6, 1, 250}: {11, .24, 0, 0, 0}, {46, 6, 1, 300}: {13, .24, 0, 0, 0}, {46, 6, 1, 325}: {16, .24, 0, 0, 0}, {46, 6, 1, 350}: {18, .24, 0, 0, 0}, {46, 6, 1, 370}: {21, .24, 0, 0, 0}, {46, 6, 1, 400}: {24, .24, 0, 0, 0}, {46, 6, 1, 420}: {27, .24, 0, 0, 0}, {46, 6, 1, 430}: {28, .24, 0, 0, 0}, // mace {46, 4, 0, 0}: {19, .23, 0, 0, 0}, {46, 4, 0, 250}: {24, .23, 0, 0, 0}, {46, 4, 0, 300}: {29, .23, 0, 0, 0}, {46, 4, 0, 325}: {35, .23, 0, 0, 0}, {46, 4, 0, 350}: {40, .23, 0, 0, 0}, {46, 4, 0, 370}: {45, .23, 0, 0, 0}, {46, 4, 0, 400}: {50, .23, 0, 0, 0}, {46, 4, 0, 420}: {53, .23, 0, 0, 0}, {46, 4, 0, 430}: {57, .23, 0, 0, 0}, // spear {46, 5, 0, 0}: {21, .65, 0, 0, 0}, {46, 5, 0, 250}: {26, .65, 0, 0, 0}, {46, 5, 0, 300}: {32, .65, 0, 0, 0}, {46, 5, 0, 325}: {37, .65, 0, 0, 0}, {46, 5, 0, 350}: {42, .65, 0, 0, 0}, {46, 5, 0, 370}: {48, .65, 0, 0, 0}, {46, 5, 0, 400}: {53, .65, 0, 0, 0}, {46, 5, 0, 420}: {57, .65, 0, 0, 0}, {46, 5, 0, 430}: {60, .65, 0, 0, 0}, // sword {46, 2, 0, 0}: {20, .42, 0, 0, 0}, {46, 2, 0, 250}: {25, .42, 0, 0, 0}, {46, 2, 0, 300}: {31, .42, 0, 0, 0}, {46, 2, 0, 325}: {36, .42, 0, 0, 0}, {46, 2, 0, 350}: {41, .42, 0, 0, 0}, {46, 2, 0, 370}: {47, .42, 0, 0, 0}, {46, 2, 0, 400}: {52, .42, 0, 0, 0}, {46, 2, 0, 420}: {55, .42, 0, 0, 0}, {46, 2, 0, 430}: {58, .42, 0, 0, 0}, // mssword {46, 2, 1, 0}: {7, .24, 0, 0, 0}, {46, 2, 1, 250}: {10, .24, 0, 0, 0}, {46, 2, 1, 300}: {13, .24, 0, 0, 0}, {46, 2, 1, 325}: {16, .24, 0, 0, 0}, {46, 2, 1, 350}: {18, .24, 0, 0, 0}, {46, 2, 1, 370}: {21, .24, 0, 0, 0}, {46, 2, 1, 400}: {24, .24, 0, 0, 0}, {46, 2, 1, 420}: {25, .24, 0, 0, 0}, {46, 2, 1, 430}: {28, .24, 0, 0, 0}, // staff {46, 7, 0, 0}: {19, .325, 0, 0, 0}, {46, 7, 0, 250}: {24, .325, 0, 0, 0}, {46, 7, 0, 300}: {30, .325, 0, 0, 0}, {46, 7, 0, 325}: {35, .325, 0, 0, 0}, {46, 7, 0, 350}: {40, .325, 0, 0, 0}, {46, 7, 0, 370}: {46, .325, 0, 0, 0}, {46, 7, 0, 400}: {51, .325, 0, 0, 0}, {46, 7, 0, 420}: {54, .325, 0, 0, 0}, {46, 7, 0, 430}: {57, .325, 0, 0, 0}, // ua {46, 1, 0, 0}: {17, .43, 0, 0, 0}, {46, 1, 0, 250}: {23, .43, 0, 0, 0}, {46, 1, 0, 300}: {27, .43, 0, 0, 0}, {46, 1, 0, 325}: {31, .43, 0, 0, 0}, {46, 1, 0, 350}: {35, .43, 0, 0, 0}, {46, 1, 0, 370}: {40, .43, 0, 0, 0}, {46, 1, 0, 400}: {44, .43, 0, 0, 0}, {46, 1, 0, 420}: {46, .43, 0, 0, 0}, {46, 1, 0, 430}: {48, .43, 0, 0, 0}, // twohanded (skill 41) http://acpedia.org/wiki/Two_Handed_Weaponry // cleaver {41, 11, 1, 0}: {13, .30, 0, 0, 0}, {41, 11, 1, 250}: {17, .30, 0, 0, 0}, {41, 11, 1, 300}: {22, .30, 0, 0, 0}, {41, 11, 1, 325}: {26, .30, 0, 0, 0}, {41, 11, 1, 350}: {30, .30, 0, 0, 0}, {41, 11, 1, 370}: {35, .30, 0, 0, 0}, {41, 11, 1, 400}: {39, .30, 0, 0, 0}, {41, 11, 1, 420}: {42, .30, 0, 0, 0}, {41, 11, 1, 430}: {45, .30, 0, 0, 0}, // spear {41, 11, 0, 0}: {14, .35, 0, 0, 0}, {41, 11, 0, 250}: {19, .35, 0, 0, 0}, {41, 11, 0, 300}: {24, .35, 0, 0, 0}, {41, 11, 0, 325}: {29, .35, 0, 0, 0}, {41, 11, 0, 350}: {33, .35, 0, 0, 0}, {41, 11, 0, 370}: {37, .35, 0, 0, 0}, {41, 11, 0, 400}: {42, .35, 0, 0, 0}, {41, 11, 0, 420}: {45, .35, 0, 0, 0}, {41, 11, 0, 430}: {48, .35, 0, 0, 0}, // missile (skill 47) http://acpedia.org/wiki/Missile_Weaponry/Bows // bow (mastery 8) {47, 8, 0, 0}: {0, 0, 110, 0, 0}, {47, 8, 0, 250}: {0, 0, 120, 0, 0}, {47, 8, 0, 270}: {0, 0, 130, 0, 0}, {47, 8, 0, 290}: {0, 0, 140, 0, 0}, {47, 8, 0, 315}: {0, 0, 140, 5, 0}, {47, 8, 0, 335}: {0, 0, 140, 9, 0}, {47, 8, 0, 360}: {0, 0, 140, 16, 0}, {47, 8, 0, 375}: {0, 0, 140, 19, 0}, {47, 8, 0, 385}: {0, 0, 140, 22, 0}, // xbow (mastery 9) {47, 9, 0, 0}: {0, 0, 140, 0, 0}, {47, 9, 0, 250}: {0, 0, 150, 0, 0}, {47, 9, 0, 270}: {0, 0, 155, 0, 0}, {47, 9, 0, 290}: {0, 0, 165, 0, 0}, {47, 9, 0, 315}: {0, 0, 165, 5, 0}, {47, 9, 0, 335}: {0, 0, 165, 9, 0}, {47, 9, 0, 360}: {0, 0, 165, 16, 0}, {47, 9, 0, 375}: {0, 0, 165, 19, 0}, {47, 9, 0, 385}: {0, 0, 165, 22, 0}, // thrown (mastery 10) {47, 10, 0, 0}: {0, 0, 130, 0, 0}, {47, 10, 0, 250}: {0, 0, 140, 0, 0}, {47, 10, 0, 270}: {0, 0, 150, 0, 0}, {47, 10, 0, 290}: {0, 0, 160, 0, 0}, {47, 10, 0, 315}: {0, 0, 160, 5, 0}, {47, 10, 0, 335}: {0, 0, 160, 9, 0}, {47, 10, 0, 360}: {0, 0, 160, 16, 0}, {47, 10, 0, 375}: {0, 0, 160, 19, 0}, {47, 10, 0, 385}: {0, 0, 160, 22, 0}, // wands (skill 34 war / 43 void) http://acpedia.org/wiki/Magic_Casters {34, 0, 0, 290}: {0, 0, 0, 0, 1.03}, {34, 0, 0, 315}: {0, 0, 0, 0, 1.06}, {34, 0, 0, 335}: {0, 0, 0, 0, 1.09}, {34, 0, 0, 360}: {0, 0, 0, 0, 1.13}, {34, 0, 0, 375}: {0, 0, 0, 0, 1.16}, {34, 0, 0, 385}: {0, 0, 0, 0, 1.18}, {43, 0, 0, 290}: {0, 0, 0, 0, 1.03}, {43, 0, 0, 315}: {0, 0, 0, 0, 1.06}, {43, 0, 0, 335}: {0, 0, 0, 0, 1.09}, {43, 0, 0, 360}: {0, 0, 0, 0, 1.13}, {43, 0, 0, 375}: {0, 0, 0, 0, 1.16}, {43, 0, 0, 385}: {0, 0, 0, 0, 1.18}, // void vr wand - not sure why this is different (mastery 12) {43, 12, 0, 385}: {0, 0, 0, 0, 1.18}, } // spellEffect mirrors UB's SpellInfo(key, change, bonus) (Dictionaries.cs:356): // `change` is what the active enchantment adds to the shown value (subtracted // to un-buff), `bonus` is what the spell grants when innate on the item (0 for // the 2012 aura-converted item spells). type odSpellEffect struct { change float64 bonus float64 } // maxDamageSpellEffects is transcribed from UB Dictionaries.cs // LongValueKeySpellEffects entries keyed on LongValueKey_MaxDamage (218103842). var maxDamageSpellEffects = map[int]odSpellEffect{ 1616: {20, 0}, // Blood Drinker VI 2096: {22, 0}, // Infected Caress 5183: {24, 0}, // Incantation of Blood Drinker (post Feb-2013) 4395: {24, 0}, // Incantation of Blood Drinker (post Feb-2013) 2598: {2, 2}, // Minor Blood Thirst 2586: {4, 4}, // Major Blood Thirst 4661: {7, 7}, // Epic Blood Thirst 6089: {10, 10}, // Legendary Blood Thirst 3688: {300, 0}, // Prodigal Blood Drinker } // elemVsMonSpellEffects is transcribed from UB Dictionaries.cs // DoubleValueKeySpellEffects entries keyed on DoubleValueKey_ElementalDamageVersusMonsters (152). var elemVsMonSpellEffects = map[int]odSpellEffect{ 3258: {.06, 0}, // Spirit Drinker VI 3259: {.07, 0}, // Infected Spirit Caress 5182: {.08, 0}, // Incantation of Spirit Drinker (post Feb-2013) 4414: {.08, 0}, // Incantation of Spirit Drinker (post Feb-2013) 3251: {.01, .01}, // Minor Spirit Thirst 3250: {.03, .03}, // Major Spirit Thirst 4670: {.05, .05}, // Epic Spirit Thirst 6098: {.07, .07}, // Legendary Spirit Thirst } // buffedMaxDamage mirrors GetBuffedIntValueKey(218103842): base value minus // `change` for each active spell (un-buff the enchantment), plus `bonus` for // each innate spell. func buffedMaxDamage(raw, iv map[string]any) int { value := ivI(iv, "218103842", 0) for _, id := range toIntList(raw["ActiveSpells"]) { if e, ok := maxDamageSpellEffects[id]; ok { value -= int(e.change) } } for _, id := range toIntList(raw["Spells"]) { if e, ok := maxDamageSpellEffects[id]; ok { value += int(e.bonus) } } return value } // buffedElemVsMon mirrors GetBuffedDoubleValueKey(152) using the same // subtract-change-for-active / add-bonus-for-innate semantics. func buffedElemVsMon(raw, dv map[string]any) float64 { value := dvF(dv, "152", 0) for _, id := range toIntList(raw["ActiveSpells"]) { if e, ok := elemVsMonSpellEffects[id]; ok { value -= e.change } } for _, id := range toIntList(raw["Spells"]) { if e, ok := elemVsMonSpellEffects[id]; ok { value += e.bonus } } return value } // isMultiStrike mirrors MiscCalcs.GetMaxProperty's MultiStrike detection: // IntValues[47] (WeaponType master id) ∈ {160,166,486}, or (==4 and Mastery==11). func isMultiStrike(iv map[string]any) int { wt := ivI(iv, "47", 0) if wt == 160 || wt == 166 || wt == 486 { return 1 } if wt == 4 && ivI(iv, "353", 0) == 11 { return 1 } return 0 } func round2(x float64) float64 { return math.Round(x*100) / 100 } // computeOD ports UB's OD display formulas (ItemInfo.cs CalcMeleeDamage / // CalcMissileDamage + the OD lines ~631-647). Returns (0, false) when OD does // not apply: non weapon/wand object class, non-loot-gen item (no // SalvageWorkmanship), or an unrecognized (skill, mastery, multiStrike, // wieldReq) tier (exact match only — no nearest-tier rounding). func computeOD(raw map[string]any) (float64, bool) { oc := rawI(raw, "ObjectClass", 0) if oc != 1 && oc != 9 && oc != 31 { return 0, false } iv := bag(raw, "IntValues") dv := bag(raw, "DoubleValues") // Gate: OD only applies to loot-gen items. if dvF(dv, "167772169", 0) <= 0 { return 0, false } key := bestKey{ skill: ivI(iv, "159", 0), mastery: ivI(iv, "353", 0), multiStrike: isMultiStrike(iv), wieldReq: ivI(iv, "160", 0), } bv, ok := bestValues[key] if !ok && oc == 31 { // Wands: our probe showed some wands lack key 353 (mastery), which // defaults to 0 via ivI — but retry explicitly with mastery 0 so the // void VR wand row (43,12,...) stays reachable while items that // genuinely carry a different mastery value still fall back to the // normal (34|43, 0, ...) rows. key.mastery = 0 bv, ok = bestValues[key] } if !ok { return 0, false } switch oc { case 1: // melee variance := dvF(dv, "167772171", 0) if variance <= 0 { return 0, false } // UB ItemInfo.cs:1096: Math.Log(perfectVariance / Variance, 0.8). // Table MaxVar is the tier's baseline variance; lower variance than // baseline yields negative varianceTinks, i.e. a positive OD credit. varianceTinks := round2(math.Log(bv.maxVar/variance) / math.Log(0.8)) od := float64(buffedMaxDamage(raw, iv)) - varianceTinks - bv.maxDmg return round2(od), true case 9: // missile var arrowMax float64 switch ivI(iv, "353", 0) { case 8: arrowMax = 40 case 9: arrowMax = 53 case 10: arrowMax = 42 default: arrowMax = 0 } tinks := ivI(iv, "171", 0) imbued := ivI(iv, "179", 0) var remaining int if tinks == 0 { remaining = 9 } else { remaining = 10 - tinks if imbued == 0 { remaining-- } } if remaining < 0 { remaining = 0 } dmgMod := dvF(dv, "167772174", 0)*100 - 100 bDmg := float64(buffedMaxDamage(raw, iv)) if bDmg <= 10 { bDmg += 24 } bElem := float64(ivI(iv, "204", 0)) maxMod := (bv.maxDmgMod + 136) / 100 od := (1+(dmgMod+4*float64(remaining))/100)*(bElem+bDmg+arrowMax)/maxMod - (bv.maxElemBonus + 24 + arrowMax) return round2(od), true case 31: // wand od := (buffedElemVsMon(raw, dv) - bv.maxElemVsMon) * 100 return round2(od), true } return 0, false }