MosswartOverlord/go-services/inventory-go/od.go
Erik cbc8785c47 feat(inventory-go): recompute OD from VTank loot-profile ladder (0-10 / casters 0-7)
Replaces the UtilityBelt over-retail float with the user's actual in-game OD:
per-weapon-type MaxDamage/elemVsMon thresholds from Loot.utl. Bows/thrown
deferred (null) pending a computed-stat calibration. (L) Dagger MS bucket
folded into (L) Dagger (indistinguishable by captured fields).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 19:09:22 +02:00

116 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import "strings"
// Weapon OD = the user's VirindiTank Loot.utl classification (010, casters
// 07), printed by Mag-Tools on ident. OD = clamp(stat baseline, 1, cap) for
// the first matching bucket, else null. Buckets/baselines transcribed from
// Loot.utl (see design doc 2026-07-15-weapon-od-loot-profile).
type odBucket struct {
objectClass int // 1 melee, 9 missile, 31 caster
skill int // 218103840 for melee/missile, 159 for casters; 0 = any
mastery int // IntValues 353; -1 = not checked
nameAlts []string // OR of case-sensitive substrings; empty = no name gate
statKey string // IntValues key (melee/missile) or ""
dblStatKey string // DoubleValues key (casters) or ""
baseline float64
cap int
}
// Ordered: name-gated buckets are effectively specific; first match wins.
var odBuckets = []odBucket{
// --- Melee (object_class 1, stat MaxDamage 218103842, cap 10) ---
{1, 44, 3, []string{"Silifi", "Lugian", "War Axe", "Battle"}, "218103842", "", 74, 10}, // (H) Axe
{1, 45, 3, []string{"Dolabra", "Ono", "Hand", "War Hammer"}, "218103842", "", 61, 10}, // (L) Axe
{1, 46, 3, []string{"Hatchet", "Shou-ono", "Tungi", "Hammer"}, "218103842", "", 61, 10}, // (F) Axe
{1, 44, 6, []string{"Stiletto", "Jambiya"}, "218103842", "", 38, 10}, // (H) Dagger MS
{1, 44, 6, nil, "218103842", "", 71, 10}, // (H) Dagger
{1, 45, 6, nil, "218103842", "", 58, 10}, // (L) Dagger (MS shares names=nil; see note)
{1, 46, 6, []string{"Knife", "Lancet"}, "218103842", "", 28, 10}, // (F) Dagger MS
{1, 46, 6, nil, "218103842", "", 58, 10}, // (F) Dagger
{1, 44, 4, []string{"Mazule", "Mace", "Morning"}, "218103842", "", 69, 10}, // (H) Mace
{1, 45, 4, []string{"Club", "Kasrullah"}, "218103842", "", 56, 10}, // (L) Mace
{1, 46, 4, []string{"Board", "Tofun", "Dabus"}, "218103842", "", 56, 10}, // (F) Mace
{1, 44, 5, []string{"Glaive", "Trident", "Partizan"}, "218103842", "", 72, 10}, // (H) Spear
{1, 45, 5, []string{"Spear", "Yari"}, "218103842", "", 60, 10}, // (L) Spear
{1, 44, 7, []string{"Nabut", "Stick"}, "218103842", "", 69, 10}, // (H) Staff
{1, 45, 7, nil, "218103842", "", 57, 10}, // (L) Staff
{1, 46, 7, []string{"Jo", "Bastone"}, "218103842", "", 57, 10}, // (F) Staff
{1, 44, 2, []string{"Takuba", "Flamberge", "Ken", "Long", "Tachi"}, "218103842", "", 71, 10}, // (H) Sword
{1, 45, 2, []string{"Dericost", "Broad", "Shamshir", "Kaskara", "Spada"}, "218103842", "", 58, 10}, // (L) Sword
{1, 46, 2, []string{"Scimitar", "Yaoji", "Short", "Sabra", "Simi"}, "218103842", "", 58, 10}, // (F) Sword
{1, 44, 1, []string{"Nekode", "Cestus"}, "218103842", "", 59, 10}, // (H) UA
{1, 45, 1, []string{"Katar", "Knuckles"}, "218103842", "", 48, 10}, // (L) UA
{1, 46, 1, []string{"Claw", "Wraps"}, "218103842", "", 48, 10}, // (F) UA
{1, 41, -1, []string{"Nodachi", "Shashqa", "Spadone", "Greataxe", "Quadrelle", "Khanda-handled", "Tetsubo", "Star"}, "218103842", "", 45, 10}, // (2H) Cleaving
{1, 41, -1, []string{"Assagai", "Pike", "Magari", "Corsesca"}, "218103842", "", 48, 10}, // (2H) Spear
// --- Crossbow (object_class 9, mastery 9, stat 218103839, cap 10) ---
{9, 47, 9, nil, "218103839", "", 77, 10},
// --- Casters (object_class 31, stat elemVsMon 152, cap 7) ---
{31, 34, -1, []string{"Baton", "Sceptre", "Staff"}, "", "152", 1.18, 7}, // War
{31, 43, -1, []string{"Nether"}, "", "152", 1.18, 7}, // Void
// Bows (mastery 8) and Thrown (mastery 10) are intentionally ABSENT → null.
}
func nameMatches(name string, alts []string) bool {
if len(alts) == 0 {
return true
}
for _, a := range alts {
if strings.Contains(name, a) {
return true
}
}
return false
}
// computeOD returns the loot-profile OD (integer, as float64 for the DB column)
// and ok=false when no bucket matches or the stat is at/below baseline.
func computeOD(raw map[string]any) (float64, bool) {
oc := rawI(raw, "ObjectClass", 0)
iv := bag(raw, "IntValues")
dv := bag(raw, "DoubleValues")
name := rawS(raw, "Name")
skillMelee := ivI(iv, "218103840", 0)
skillCaster := ivI(iv, "159", 0)
mastery := ivI(iv, "353", -999)
for _, b := range odBuckets {
if b.objectClass != oc {
continue
}
sk := skillMelee
if b.objectClass == 31 {
sk = skillCaster
}
if b.skill != 0 && b.skill != sk {
continue
}
if b.mastery != -1 && b.mastery != mastery {
continue
}
if !nameMatches(name, b.nameAlts) {
continue
}
var stat float64
if b.dblStatKey != "" {
stat = dvF(dv, b.dblStatKey, 0)
} else {
stat = float64(ivI(iv, b.statKey, 0))
}
od := stat - b.baseline
if b.dblStatKey != "" {
od = od * 100 // caster elemVsMon: percentage points
}
odInt := int(od + 0.5) // round
if odInt < 1 {
return 0, false
}
if odInt > b.cap {
odInt = b.cap
}
return float64(odInt), true
}
return 0, false
}