fix(inventory-go): OD variance orientation per UB source; exact SpellInfo change/bonus semantics; missile golden test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-15 07:46:07 +02:00
parent 16077633c5
commit f75a236f54
2 changed files with 105 additions and 46 deletions

View file

@ -382,63 +382,72 @@ var bestValues = map[bestKey]bestValue{
{43, 12, 0, 385}: {0, 0, 0, 0, 1.18},
}
// maxDamageSpellBonus is transcribed from UB Dictionaries.cs
// 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 maxDamageSpellBonus = map[int]int{
1616: 20, // Blood Drinker VI
2096: 22, // Infected Caress
5183: 24, // Incantation of Blood Drinker (post Feb-2013)
4395: 24, // Incantation of Blood Drinker (post Feb-2013)
2598: 2, // Minor Blood Thirst
2586: 4, // Major Blood Thirst
4661: 7, // Epic Blood Thirst
6089: 10, // Legendary Blood Thirst
3688: 300, // Prodigal Blood Drinker
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
}
// elemVsMonSpellBonus is transcribed from UB Dictionaries.cs
// elemVsMonSpellEffects is transcribed from UB Dictionaries.cs
// DoubleValueKeySpellEffects entries keyed on DoubleValueKey_ElementalDamageVersusMonsters (152).
var elemVsMonSpellBonus = map[int]float64{
3258: .06, // Spirit Drinker VI
3259: .07, // Infected Spirit Caress
5182: .08, // Incantation of Spirit Drinker (post Feb-2013)
4414: .08, // Incantation of Spirit Drinker (post Feb-2013)
3251: .01, // Minor Spirit Thirst
3250: .03, // Major Spirit Thirst
4670: .05, // Epic Spirit Thirst
6098: .07, // Legendary Spirit Thirst
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 plus
// bonus for each innate spell (raw["Spells"]), minus bonus for each active
// spell (raw["ActiveSpells"]).
// 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["Spells"]) {
if b, ok := maxDamageSpellBonus[id]; ok {
value += b
for _, id := range toIntList(raw["ActiveSpells"]) {
if e, ok := maxDamageSpellEffects[id]; ok {
value -= int(e.change)
}
}
for _, id := range toIntList(raw["ActiveSpells"]) {
if b, ok := maxDamageSpellBonus[id]; ok {
value -= b
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
// innate-add / active-subtract semantics.
// 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["Spells"]) {
if b, ok := elemVsMonSpellBonus[id]; ok {
value += b
for _, id := range toIntList(raw["ActiveSpells"]) {
if e, ok := elemVsMonSpellEffects[id]; ok {
value -= e.change
}
}
for _, id := range toIntList(raw["ActiveSpells"]) {
if b, ok := elemVsMonSpellBonus[id]; ok {
value -= b
for _, id := range toIntList(raw["Spells"]) {
if e, ok := elemVsMonSpellEffects[id]; ok {
value += e.bonus
}
}
return value
@ -506,11 +515,10 @@ func computeOD(raw map[string]any) (float64, bool) {
if variance <= 0 {
return 0, false
}
// NOTE: literal ratio is variance/bv.maxVar, not bv.maxVar/variance as
// the plan's prose (and the UB source's `perfectVariance/Variance`
// argument order) states — see od.go deviation note below and the
// implementation report for why.
varianceTinks := round2(math.Log(variance/bv.maxVar) / math.Log(0.8))
// 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