MosswartOverlord/docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md
Erik 10f8d135e5 docs(spec): weapon OD rating — compute at ingest, filter in search
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 07:26:32 +02:00

5.8 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), using the spell-effect subset from UB Dictionaries.cs: MaxDamage — Blood Drinker VI 1616:+20, Infected Caress 2096:+22, Incant. BD 5183/4395:+24, Minor/Major/Epic/Legendary Blood Thirst 2598:+2/2586:+4/4661:+7/6089:+10, Prodigal BD 3688:+300 (bonus = change for all these); ElemVsMonsters (double) — Spirit Drinker VI 3258:+.06, Infected Spirit Caress 3259:+.07, Incant. SD 5182/4414:+.08, Minor/Major/Epic/ Legendary Spirit Thirst 3251:+.01/3250:+.03/4670:+.05/6098:+.07.

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).