docs: spec + plan to recompute OD from VTank loot profile (0-10 scale)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-15 19:06:02 +02:00
parent cc403240d6
commit 7a11122a26
2 changed files with 380 additions and 0 deletions

View file

@ -0,0 +1,77 @@
# Weapon OD — recompute from the VTank loot profile (010 scale)
**Date:** 2026-07-15
**Status:** Approved (user directed the rebuild)
## Problem
The shipped OD (`od.go`) ports UtilityBelt's unbounded "over-retail damage"
float (16 … +43). The user's actual in-game OD is a **010** (casters 07)
integer they see on ident. Investigation (this session) proved:
- Mag-Tools has **no** OD calculation. Its `ItemInfoPrinter` prints the item
info **plus the matching VirindiTank loot-rule name** (`GetLootRuleInfoFromItemInfo`).
- The user's `VirindiTank\Loot.utl` defines a ladder of rules named
`(H) Axe (OD +10)`, `(OD +9)`, … per weapon bucket. Mag-Tools prints whichever
matched — **that** is the OD the user sees.
The thresholds are the same across all the user's profiles (user-confirmed), so
one table drives every weapon and caster.
## The rule (decoded from Loot.utl)
Each bucket is `OD = clamp(stat baseline, 1, cap)`, where the bucket is matched
by **weapon skill + mastery + (optional) weapon-name filter**, and `stat` is a
raw captured value. Ladders step by 1 per tier.
- **Melee** (object_class 1): `stat = MaxDamage` (int key `218103842`), cap 10.
27 buckets (skill `218103840`, mastery `353`, name filter, baseline) — see the
table in the plan. Example: 2H Cleaving baseline 45 → Tetsubo (maxdmg 85) = OD
**+10** (matches in-game); Heavy Axe baseline 74.
- **Crossbow** (object_class 9, mastery `353`=9): `stat = int key 218103839`
("effective missile damage"), baseline **77**, cap 10 (OD+10=87 … OD+1=78,
verified on Fire Compound Crossbow k839=89 → +10).
- **Casters** (object_class 31): `stat = ElementalDamageVersusMonsters` (double
key `152`), baseline **1.18**, ×100, cap **7**. War bucket: skill `159`=34,
name `Baton|Sceptre|Staff`; Void bucket: skill 43, name `Nether`. OD+7 at
elemVsMon 1.25, OD+1 at 1.19. Frost Baton (1.40) → +7.
- **No match / stat below baseline+1 → NULL** (item shows no OD, exactly like
in-game when no OD rule matches).
## Deferred: Bows & Thrown (mastery 8 / 10)
Their OD rules threshold a **computed decimal** damage stat (bow OD+10 = 78.6,
thrown = 85.3) on a key not reliably identifiable from the profile alone (bow
`218103839` values 4570 don't reach 78.6; the rule uses a DoubleValKeyGE the
`.utl` encodes indirectly). To avoid showing a *wrong* number (the very problem
we're fixing), **bows and thrown get NULL OD** in this pass. Reopened once the
user supplies one in-game bow OD reading to calibrate the stat.
## Design
- **`od.go` rewritten** around an ordered bucket table (extracted from
`Loot.utl`, committed as data): `{objectClass, skill, mastery, nameAlts[],
statKey, baseline, cap}`. `computeOD` finds the first bucket whose
objectClass+skill+mastery+name match, then `OD = clamp(stat baseline, 1,
cap)`; returns null if none match or stat ≤ baseline. Name match = any
alternative is a substring of the item name (VTank substring semantics).
- **Type**: `od_rating` stays `DOUBLE PRECISION NULL` but now holds an integer
110 (or 17). Frontend already renders it; drop the `+`/decimals for the new
integer (show plain `7`, `10`, `—`).
- Search `min_od`/`max_od`/sort unchanged. Backfill via existing
`POST /admin/backfill-od`.
- The best-values table, spell-effect dicts, and variance math in the old
`od.go` are **deleted** (wrong metric).
## Testing
Go golden tests from real items: Tetsubo → 10, Frost Baton → 7, Fire Compound
Crossbow → 10, a mid melee → its expected tier, a below-baseline weapon → null,
a bow → null (deferred). Then live backfill + spot-checks; the column must read
010/07 and the user confirms against ident.
## Out of scope
- Bows/thrown OD (deferred, null).
- The loot-rule name filters are reproduced as substring lists; exotic renamed
items may differ from a live VTank match — acceptable.