3.8 KiB
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
ItemInfoPrinterprints the item info plus the matching VirindiTank loot-rule name (GetLootRuleInfoFromItemInfo). - The user's
VirindiTank\Loot.utldefines 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 key218103842), cap 10. 27 buckets (skill218103840, mastery353, 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 key152), baseline 1.18, ×100, cap 7. War bucket: skill159=34, nameBaton|Sceptre|Staff; Void bucket: skill 43, nameNether. 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.gorewritten around an ordered bucket table (extracted fromLoot.utl, committed as data):{objectClass, skill, mastery, nameAlts[], statKey, baseline, cap}.computeODfinds the first bucket whose objectClass+skill+mastery+name match, thenOD = 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_ratingstaysDOUBLE PRECISION NULLbut now holds an integer 1–10 (or 1–7). Frontend already renders it; drop the+/decimals for the new integer (show plain7,10,—). - Search
min_od/max_od/sort unchanged. Backfill via existingPOST /admin/backfill-od. - The best-values table, spell-effect dicts, and variance math in the old
od.goare 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.