MosswartOverlord/docs/superpowers/specs/2026-07-15-weapon-od-loot-profile-design.md
Erik 7a11122a26 docs: spec + plan to recompute OD from VTank loot profile (0-10 scale)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 19:06:02 +02:00

3.8 KiB
Raw Blame History

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.