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

116 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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