MosswartOverlord/docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md
2026-07-15 07:43:26 +02:00

6.4 KiB
Raw Blame History

Weapon OD rating — compute, store, filter

Date: 2026-07-15 Status: Approved

Problem

The inventory search has weak weapon/wand support. Mag-Tools (and UtilityBelt's port of it, the "retail comparison" / OD feature) classifies weapons by OD ("over damage") — how much better a weapon's optimally-tinked output is than the best possible retail loot weapon of its class and wield tier. Users want to filter weapons by OD.

Authoritative algorithm (researched 2026-07-15)

Source: UtilityBelt/UtilityBelt/Tools/ItemInfo.cs (sections titled "Slightly modified MagTools item description classes", OD display at lines ~631-647) + UtilityBelt/UtilityBelt/Lib/ItemInfoHelper/{MiscCalcs,WeaponMods,Dictionaries}.cs in this workspace. WeaponMods.cs holds the best-values table: rows keyed by (Skill, Mastery, MultiStrike, WieldReq) with columns MaxDmg, MaxVar, MaxDmgMod, MaxElementalDmgBonus, MaxElementalDmgVsMonsters (sourced from acpedia; ~330 rows covering heavy/light/finesse/two-handed melee subtypes, bow/xbow/thrown, war/void casters).

Raw item keys (verified present in our item_raw_data):

  • IntValues: 159 EquipSkill, 353 Mastery/WeaponType, 160 WieldReqValue, 218103842 MaxDamage, 204 ElementalDmgBonus, 171 Tinks, 179 Imbued (bitmask, nonzero = imbued), 131 Material.
  • original_json->DoubleValues: 167772171 Variance, 167772174 DamageBonus (missile dmg modifier, e.g. 2.73 = 173%), 152 ElementalDmgVsMonsters (wands), 167772169 SalvageWorkmanship.
  • original_json->Spells / ActiveSpells: innate and active spell id arrays.

Buffed values (GetBuffedIntValueKey / GetBuffedDoubleValueKey semantics): value + Σ bonus(innate Spells) Σ change(ActiveSpells). Each spell entry is a {change, bonus} pair (SpellInfo(key, change, bonus=0), Dictionaries.cs:356). Old-style item spells were converted to player auras in 2012, so their innate bonus is 0 — only their active-enchant change is subtracted; the Blood/Spirit Thirst cantrips have bonus == change:

  • MaxDamage — 1616 BD VI {20,0}, 2096 Infected Caress {22,0}, 5183/4395 Incant. BD {24,0}, 2598 Minor BT {2,2}, 2586 Major BT {4,4}, 4661 Epic BT {7,7}, 6089 Legendary BT {10,10}, 3688 Prodigal BD {300,0}.
  • ElemVsMonsters — 3258 SD VI {.06,0}, 3259 Infected Spirit Caress {.07,0}, 5182/4414 Incant. SD {.08,0}, 3251 Minor ST {.01,.01}, 3250 Major ST {.03,.03}, 4670 Epic ST {.05,.05}, 6098 Legendary ST {.07,.07}.

Melee variance orientation: the table's MaxVar is the type's baseline variance; a weapon with LOWER variance than baseline yields a NEGATIVE varianceTinks (log ratio < 1 over log 0.8) and therefore a HIGHER OD — varianceTinks = round(log(MaxVar_t / variance)/log(0.8), 2) exactly as ItemInfo.cs:1096.

MultiStrike weapons: WeaponType (key 47 in UB code = our int key 47? no — UB reads LongValueKey 47 = WeaponType master id; the multistrike check is: IntValues[47] ∈ {160,166,486} or (47==4 and mastery 353==11)). Verify key 47 exists in our data during implementation; if absent, derive from mastery subtype comments in the table (msdagger/mssword/cleaver rows are the MultiStrike=1 rows).

Formulas (OD, higher = better; retail max = 0):

  • Melee (object_class 1): varianceTinks = round(log(MaxVar_t / variance) / log(0.8), 2); OD = buffedMaxDmg varianceTinks MaxDmg_t.
  • Missile (object_class 9): dmgMod = buffedDmgBonus·100 100; arrowMax by mastery: bow(8)=40, xbow(9)=53, thrown(10)=42; remainingTinks: 10 tinks, 1 more if not imbued, floor 0 (loot-gen only, see gate below; UB nuance: if tinks==0, remainingTinks=9); buffedDmg = buffedMaxDmg; if ≤10 then +24; maxMod = (MaxDmgMod_t + 100 + 36)/100; OD = (1 + (dmgMod + 4·remainingTinks)/100) · (buffedElemBonus + buffedDmg + arrowMax) / maxMod (MaxElemBonus_t + 24 + arrowMax).
  • Wand (object_class 31, skill 34 war / 43 void): OD = (buffedElemVsMonsters MaxElemVsMonsters_t) · 100 (percentage points; e.g. live Frost Baton: (1.40 1.18)·100 = +22).
  • Gate: OD only for loot-gen items (SalvageWorkmanship > 0); otherwise NULL. Table lookup miss (unknown skill/mastery/wieldreq combo) → NULL. Round stored OD to 2 decimals.

Decisions (user-approved)

  1. Raw OD number with a min OD ≥ filter (not a bucketed 0-10 scale).
  2. All classes: melee, missile, wands.
  3. Computed at ingest, stored on items.od_rating DOUBLE PRECISION NULL, with a one-off backfill for existing rows.

Backend (inventory-go)

  • New od.go: best-values table ported from UB WeaponMods.cs (verify row count and spot-check values against the source file), buffed-value helpers, computeOD(raw map[string]any) (float64, bool). TDD with golden values (incl. the live Frost Baton +22 case).
  • processItem sets items["od_rating"] (or omits when NULL) — the dynamic insert in ingest.go picks it up automatically.
  • schema.go: add od_rating DOUBLE PRECISION to the items DDL (fresh installs). Live DB gets a manual ALTER TABLE items ADD COLUMN IF NOT EXISTS od_rating double precision (prod runs SKIP_SCHEMA_INIT=true).
  • Search: i.od_rating added to the CTE; min_od (>=) and max_od (<=) params; sort key od. NULL od never matches the filters (SQL NULL semantics) and sorts NULLS LAST/FIRST as usual.
  • Backfill: POST /admin/backfill-od — iterates items of object_class 1/9/31 joined to item_raw_data, recomputes, UPDATEs od_rating; returns counts. Service is internal-only (127.0.0.1:8772 / compose network).

Frontend

  • RATING_DEFS gains {param:'min_od', label:'Weapon OD', common:true} — flows through the existing ratings record, sidebar inputs, chips, and URL state with no new plumbing.
  • COLUMNS gains {key:'od_rating', label:'OD', sortKey:'od', defaultVisible:false}; cell renders signed 2-decimal (+3.25 / -1.29), for null.
  • InvItem.od_rating?: number | null; DetailPanel row "OD" for weapons.

Verification

  • Go unit tests (golden formula values + table spot checks) gate the image build as usual.
  • Live: backfill count > 0; min_od=20 returns the Frost Baton; melee/missile spot checks against UB in-game values if available; sort by OD desc.

Out of scope

  • Bucketed OD tiers (can be derived later).
  • The old suitbuilder page.
  • Recomputing OD when spell/enchant state changes intra-session (ingest updates overwrite it naturally via the debounced item updates).