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:
parent
cc403240d6
commit
7a11122a26
2 changed files with 380 additions and 0 deletions
|
|
@ -0,0 +1,77 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue