116 lines
5.8 KiB
Markdown
116 lines
5.8 KiB
Markdown
# 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).
|