77 lines
3.8 KiB
Markdown
77 lines
3.8 KiB
Markdown
# Weapon OD — recompute from the VTank loot profile (0–10 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 **0–10** (casters 0–7)
|
||
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 45–70 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
|
||
1–10 (or 1–7). 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
|
||
0–10/0–7 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.
|