docs(spec): weapon OD rating — compute at ingest, filter in search
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
71f235c8b1
commit
10f8d135e5
1 changed files with 116 additions and 0 deletions
116
docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md
Normal file
116
docs/superpowers/specs/2026-07-15-weapon-od-rating-design.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue