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>
116 lines
5.2 KiB
Go
116 lines
5.2 KiB
Go
package main
|
||
|
||
import "strings"
|
||
|
||
// Weapon OD = the user's VirindiTank Loot.utl classification (0–10, casters
|
||
// 0–7), 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
|
||
}
|