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

132 lines
6.7 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). 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},
3735 Prodigal Spirit Drinker {.15,0}.
**Missile dmgMod key**: UB's literal CalcMissileDamage reads `DoubleValues[63]`,
but no DoubleValueKey ordinal 63 exists in Decal — the populated key is
`167772174` (DamageBonus), consistent with UB's own CalcedBuffedMissileDamage.
We use 167772174 deliberately.
**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.18100 = +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).