feat(go-services): inventory-go item-processor (extract_item_properties) — exact
Ports the core item processing: raw item JSON -> normalized columns for all 7 tables, with the exact per-table sentinel->NULL rules, material/item_set string translation, the Spells/ActiveSpells union (is_active), and compute_base_values (the spell_effects buff-reversal for base_armor_level/base_max_damage/ base_attack_bonus/etc., with the data embedded and the 167772170-vs-167772172 attack-bonus id discrepancy preserved). loadEnums now also loads MaterialType. A loopback POST /debug/process returns the normalized columns for validation. Validated against production's STORED rows (read-only, no writes): 0 mismatches across 200 items for every sampled column of items, item_enhancements (incl. translated material + set), item_combat_stats (incl. base_* values), and item_ratings. This unlocks ingestion (the processor produces the rows) and the remaining search-response enrichment (spells/weapon/mana from the same extractor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1294ec4418
commit
b90b52c515
2 changed files with 461 additions and 28 deletions
|
|
@ -28,6 +28,7 @@ type Server struct {
|
|||
pool *pgxpool.Pool
|
||||
attributeSets map[string]string // AttributeSetInfo: set-id -> set name
|
||||
objectClasses map[int]string // ObjectClass: id -> name
|
||||
materials map[int]string // MaterialType: id -> name
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
|
|
@ -45,14 +46,15 @@ func main() {
|
|||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
srv := &Server{log: logger, attributeSets: map[string]string{}, objectClasses: map[int]string{}}
|
||||
srv := &Server{log: logger, attributeSets: map[string]string{}, objectClasses: map[int]string{}, materials: map[int]string{}}
|
||||
|
||||
if sets, classes, err := loadEnums(enumPath); err != nil {
|
||||
logger.Warn("could not load enum DB (set/class names will be unknown)", "err", err, "path", enumPath)
|
||||
if e, err := loadEnums(enumPath); err != nil {
|
||||
logger.Warn("could not load enum DB (set/class/material names will be unknown)", "err", err, "path", enumPath)
|
||||
} else {
|
||||
srv.attributeSets = sets
|
||||
srv.objectClasses = classes
|
||||
logger.Info("loaded enum DB", "sets", len(sets), "object_classes", len(classes))
|
||||
srv.attributeSets = e.sets
|
||||
srv.objectClasses = e.objectClasses
|
||||
srv.materials = e.materials
|
||||
logger.Info("loaded enum DB", "sets", len(e.sets), "object_classes", len(e.objectClasses), "materials", len(e.materials))
|
||||
}
|
||||
|
||||
if dsn == "" {
|
||||
|
|
@ -74,6 +76,7 @@ func main() {
|
|||
mux.HandleFunc("GET /sets/list", srv.handleSetsList)
|
||||
mux.HandleFunc("GET /characters/list", srv.handleCharactersList)
|
||||
mux.HandleFunc("GET /search/items", srv.handleSearchItems)
|
||||
mux.HandleFunc("POST /debug/process", srv.handleDebugProcess)
|
||||
|
||||
httpSrv := &http.Server{Addr: addr, Handler: withLogging(mux), ReadHeaderTimeout: 10 * time.Second}
|
||||
go func() {
|
||||
|
|
@ -162,41 +165,50 @@ func (s *Server) dbErr(w http.ResponseWriter, where string, err error) {
|
|||
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": "Internal server error"})
|
||||
}
|
||||
|
||||
type enumMaps struct {
|
||||
sets map[string]string
|
||||
objectClasses map[int]string
|
||||
materials map[int]string
|
||||
}
|
||||
|
||||
// loadEnums reads the comprehensive enum DB and extracts AttributeSetInfo
|
||||
// (set-id -> name) and ObjectClass (id -> name), mirroring
|
||||
// load_comprehensive_enums (dictionaries first, then enums).
|
||||
func loadEnums(path string) (sets map[string]string, classes map[int]string, err error) {
|
||||
// (set-id -> name), ObjectClass (id -> name), and MaterialType (id -> name),
|
||||
// mirroring load_comprehensive_enums (dictionaries first, then enums).
|
||||
func loadEnums(path string) (enumMaps, error) {
|
||||
var em enumMaps
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return em, err
|
||||
}
|
||||
type valmap struct {
|
||||
Values map[string]string `json:"values"`
|
||||
}
|
||||
var db struct {
|
||||
Dictionaries map[string]struct {
|
||||
Values map[string]string `json:"values"`
|
||||
} `json:"dictionaries"`
|
||||
Enums map[string]struct {
|
||||
Values map[string]string `json:"values"`
|
||||
} `json:"enums"`
|
||||
ObjectClasses struct {
|
||||
Values map[string]string `json:"values"`
|
||||
} `json:"object_classes"`
|
||||
Dictionaries map[string]valmap `json:"dictionaries"`
|
||||
Enums map[string]valmap `json:"enums"`
|
||||
ObjectClasses valmap `json:"object_classes"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &db); err != nil {
|
||||
return nil, nil, err
|
||||
return em, err
|
||||
}
|
||||
sets = map[string]string{}
|
||||
em.sets = map[string]string{}
|
||||
if d, ok := db.Dictionaries["AttributeSetInfo"]; ok && len(d.Values) > 0 {
|
||||
sets = d.Values
|
||||
em.sets = d.Values
|
||||
} else if e, ok := db.Enums["AttributeSetInfo"]; ok {
|
||||
sets = e.Values
|
||||
em.sets = e.Values
|
||||
}
|
||||
classes = map[int]string{}
|
||||
for k, v := range db.ObjectClasses.Values {
|
||||
if n, err := strconv.Atoi(k); err == nil {
|
||||
classes[n] = v
|
||||
intMap := func(v valmap) map[int]string {
|
||||
m := map[int]string{}
|
||||
for k, val := range v.Values {
|
||||
if n, err := strconv.Atoi(k); err == nil {
|
||||
m[n] = val
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
return sets, classes, nil
|
||||
em.objectClasses = intMap(db.ObjectClasses)
|
||||
em.materials = intMap(db.Enums["MaterialType"])
|
||||
return em, nil
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
|
|
|
|||
421
go-services/inventory-go/process.go
Normal file
421
go-services/inventory-go/process.go
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Item-processor: a faithful port of inventory-service extract_item_properties +
|
||||
// the process_inventory column population. Given a raw item dict it produces the
|
||||
// normalized rows for the 7 tables, applying the exact per-table sentinel->NULL
|
||||
// rules. Validated against production's stored rows (read-only) via /debug/process.
|
||||
|
||||
// --- value-bag accessors (JSON object keys are strings) ---
|
||||
|
||||
func bag(raw map[string]any, name string) map[string]any {
|
||||
if m, ok := raw[name].(map[string]any); ok {
|
||||
return m
|
||||
}
|
||||
return map[string]any{}
|
||||
}
|
||||
func ivI(iv map[string]any, key string, def int) int {
|
||||
if v, ok := iv[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
func dvF(dv map[string]any, key string, def float64) float64 {
|
||||
if v, ok := dv[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
func rawI(raw map[string]any, key string, def int) int {
|
||||
if v, ok := raw[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
func rawF(raw map[string]any, key string, def float64) float64 {
|
||||
if v, ok := raw[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
func rawS(raw map[string]any, key string) string {
|
||||
if s, ok := raw[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func rawB(raw map[string]any, key string) bool {
|
||||
b, _ := raw[key].(bool)
|
||||
return b
|
||||
}
|
||||
|
||||
// IV first, else top-level field, else default (e.g. max_damage).
|
||||
func ivElseTopI(iv, raw map[string]any, ivKey, topKey string, def int) int {
|
||||
if v, ok := iv[ivKey]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return rawI(raw, topKey, def)
|
||||
}
|
||||
func dvElseTopF(dv, raw map[string]any, dvKey, topKey string, def float64) float64 {
|
||||
if v, ok := dv[dvKey]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return rawF(raw, topKey, def)
|
||||
}
|
||||
|
||||
// translateMaterial: materials[id] else "Unknown_Material_{id}".
|
||||
func (s *Server) translateMaterial(id int) string {
|
||||
if n, ok := s.materials[id]; ok {
|
||||
return n
|
||||
}
|
||||
return "Unknown_Material_" + strconv.Itoa(id)
|
||||
}
|
||||
|
||||
func toIntList(v any) []int {
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]int, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
if f, ok := e.(float64); ok {
|
||||
out = append(out, int(f))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// processItem produces the normalized columns for all 7 tables, post-null.
|
||||
func (s *Server) processItem(raw map[string]any) map[string]any {
|
||||
iv := bag(raw, "IntValues")
|
||||
dv := bag(raw, "DoubleValues")
|
||||
|
||||
items := map[string]any{
|
||||
"item_id": rawValue(raw, "Id"),
|
||||
"name": rawS(raw, "Name"),
|
||||
"icon": rawI(raw, "Icon", 0),
|
||||
"object_class": rawI(raw, "ObjectClass", 0),
|
||||
"value": rawI(raw, "Value", 0),
|
||||
"burden": rawI(raw, "Burden", 0),
|
||||
"has_id_data": rawB(raw, "HasIdData"),
|
||||
"last_id_time": rawI(raw, "LastIdTime", 0),
|
||||
"current_wielded_location": ivI(iv, "10", 0),
|
||||
"container_id": rawI(raw, "ContainerId", 0),
|
||||
"slot": ivI(iv, "231735296", -1),
|
||||
"bonded": ivI(iv, "33", 0),
|
||||
"attuned": ivI(iv, "114", 0),
|
||||
"unique": ivI(iv, "279", 0) != 0,
|
||||
"stack_size": ivI(iv, "12", 1),
|
||||
"max_stack_size": ivI(iv, "11", 1),
|
||||
"items_capacity": nilNeg(ivI(iv, "6", -1)),
|
||||
"containers_capacity": nilNeg(ivI(iv, "7", -1)),
|
||||
"structure": nilNeg(ivI(iv, "92", -1)),
|
||||
"max_structure": nilNeg(ivI(iv, "91", -1)),
|
||||
"rare_id": nilNeg(ivI(iv, "17", -1)),
|
||||
"lifespan": nilNeg(ivI(iv, "267", -1)),
|
||||
"remaining_lifespan": nilNeg(ivI(iv, "268", -1)),
|
||||
}
|
||||
|
||||
// combat (sentinel defaults), then base values merged.
|
||||
wt := ivI(iv, "218103835", -1)
|
||||
if wt > 100 {
|
||||
wt = 100
|
||||
}
|
||||
combat := map[string]any{
|
||||
"max_damage": ivElseTopI(iv, raw, "218103842", "MaxDamage", -1),
|
||||
"damage_type": ivI(iv, "218103832", -1),
|
||||
"damage_bonus": dvElseTopF(dv, raw, "167772174", "DamageBonus", -1.0),
|
||||
"elemental_damage_bonus": ivI(iv, "204", -1),
|
||||
"elemental_damage_vs_monsters": dvF(dv, "152", -1.0),
|
||||
"variance": dvF(dv, "167772171", -1.0),
|
||||
"cleaving": ivI(iv, "292", -1),
|
||||
"crit_damage_rating": ivI(iv, "314", -1),
|
||||
"damage_over_time": ivI(iv, "318", -1),
|
||||
"attack_bonus": dvElseTopF(dv, raw, "167772170", "AttackBonus", -1.0),
|
||||
"weapon_time": wt,
|
||||
"weapon_skill": ivI(iv, "218103840", -1),
|
||||
"armor_level": topElseIvI(raw, iv, "ArmorLevel", "28", -1),
|
||||
"melee_defense_bonus": dvF(dv, "29", -1.0),
|
||||
"missile_defense_bonus": dvF(dv, "149", -1.0),
|
||||
"magic_defense_bonus": dvF(dv, "150", -1.0),
|
||||
"resist_magic": ivI(iv, "36", -1),
|
||||
"crit_resist_rating": ivI(iv, "315", -1),
|
||||
"crit_damage_resist_rating": ivI(iv, "316", -1),
|
||||
"dot_resist_rating": ivI(iv, "350", -1),
|
||||
"life_resist_rating": ivI(iv, "351", -1),
|
||||
"nether_resist_rating": ivI(iv, "331", -1),
|
||||
"heal_over_time": ivI(iv, "312", -1),
|
||||
"healing_resist_rating": ivI(iv, "317", -1),
|
||||
"mana_conversion_bonus": dvF(dv, "144", -1.0),
|
||||
"pk_damage_rating": ivI(iv, "381", -1),
|
||||
"pk_damage_resist_rating": ivI(iv, "382", -1),
|
||||
"gear_pk_damage_rating": ivI(iv, "383", -1),
|
||||
"gear_pk_damage_resist_rating": ivI(iv, "384", -1),
|
||||
}
|
||||
s.mergeBaseValues(raw, combat)
|
||||
|
||||
requirements := map[string]any{
|
||||
"wield_level": rawI(raw, "WieldLevel", -1),
|
||||
"skill_level": rawI(raw, "SkillLevel", -1),
|
||||
"lore_requirement": rawI(raw, "LoreRequirement", -1),
|
||||
"equip_skill": rawValueStr(raw, "EquipSkill"),
|
||||
}
|
||||
|
||||
// material + item_set translated strings.
|
||||
var material any
|
||||
if m := rawS(raw, "Material"); m != "" {
|
||||
material = m
|
||||
} else if v, ok := iv["131"]; ok {
|
||||
if f, ok := v.(float64); ok && int(f) != 0 {
|
||||
name := s.translateMaterial(int(f))
|
||||
if !strings.HasPrefix(name, "Unknown_Material_") {
|
||||
material = name
|
||||
}
|
||||
}
|
||||
}
|
||||
var itemSet any
|
||||
if v, ok := iv["265"]; ok {
|
||||
if f, ok := v.(float64); ok && int(f) != 0 {
|
||||
id := strconv.Itoa(int(f))
|
||||
if n, ok := s.attributeSets[id]; ok {
|
||||
itemSet = n
|
||||
} else {
|
||||
itemSet = id
|
||||
}
|
||||
}
|
||||
}
|
||||
enhancements := map[string]any{
|
||||
"material": material,
|
||||
"imbue": rawValueStr(raw, "Imbue"),
|
||||
"tinks": rawI(raw, "Tinks", -1),
|
||||
"workmanship": rawF(raw, "Workmanship", -1.0),
|
||||
"num_times_tinkered": ivI(iv, "171", -1),
|
||||
"free_tinkers_bitfield": ivI(iv, "264", -1),
|
||||
"num_items_in_material": ivI(iv, "170", -1),
|
||||
"imbue_attempts": ivI(iv, "205", -1),
|
||||
"imbue_successes": ivI(iv, "206", -1),
|
||||
"imbued_effect2": ivI(iv, "303", -1),
|
||||
"imbued_effect3": ivI(iv, "304", -1),
|
||||
"imbued_effect4": ivI(iv, "305", -1),
|
||||
"imbued_effect5": ivI(iv, "306", -1),
|
||||
"imbue_stacking_bits": ivI(iv, "311", -1),
|
||||
"item_set": itemSet,
|
||||
"equipment_set_extra": ivI(iv, "321", -1),
|
||||
"aetheria_bitfield": ivI(iv, "322", -1),
|
||||
"heritage_specific_armor": ivI(iv, "324", -1),
|
||||
"shared_cooldown": ivI(iv, "280", -1),
|
||||
}
|
||||
|
||||
ratingKeys := map[string]string{
|
||||
"damage_rating": "307", "damage_resist_rating": "308", "crit_rating": "313",
|
||||
"crit_resist_rating": "315", "crit_damage_rating": "314", "crit_damage_resist_rating": "316",
|
||||
"heal_boost_rating": "323", "vitality_rating": "341", "healing_rating": "342",
|
||||
"weakness_rating": "329", "nether_over_time": "330", "healing_resist_rating": "317",
|
||||
"nether_resist_rating": "331", "dot_resist_rating": "350", "life_resist_rating": "351",
|
||||
"sneak_attack_rating": "356", "recklessness_rating": "357", "deception_rating": "358",
|
||||
"pk_damage_rating": "381", "pk_damage_resist_rating": "382", "gear_pk_damage_rating": "383",
|
||||
"gear_pk_damage_resist_rating": "384", "gear_damage": "370", "gear_damage_resist": "371",
|
||||
"gear_crit": "372", "gear_crit_resist": "373", "gear_crit_damage": "374",
|
||||
"gear_crit_damage_resist": "375", "gear_healing_boost": "376", "gear_max_health": "379",
|
||||
"gear_nether_resist": "377", "gear_life_resist": "378", "gear_overpower": "388",
|
||||
"gear_overpower_resist": "389",
|
||||
}
|
||||
ratings := map[string]any{}
|
||||
for col, k := range ratingKeys {
|
||||
ratings[col] = ivI(iv, k, -1)
|
||||
}
|
||||
|
||||
// spells: union of Spells + ActiveSpells, is_active = in ActiveSpells.
|
||||
spells := toIntList(raw["Spells"])
|
||||
active := toIntList(raw["ActiveSpells"])
|
||||
activeSet := map[int]bool{}
|
||||
for _, id := range active {
|
||||
activeSet[id] = true
|
||||
}
|
||||
seen := map[int]bool{}
|
||||
var spellRows []map[string]any
|
||||
for _, id := range append(append([]int{}, spells...), active...) {
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
spellRows = append(spellRows, map[string]any{"spell_id": id, "is_active": activeSet[id]})
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"items": items,
|
||||
"combat": nullify(combat, sentinelCombat),
|
||||
"requirements": nullify(requirements, sentinelReq),
|
||||
"enhancements": nullify(enhancements, sentinelEnh), // always present
|
||||
"ratings": nullify(ratings, sentinelRating),
|
||||
"spells": spellRows,
|
||||
}
|
||||
}
|
||||
|
||||
func topElseIvI(raw, iv map[string]any, topKey, ivKey string, def int) int {
|
||||
if v, ok := raw[topKey]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int(f)
|
||||
}
|
||||
}
|
||||
return ivI(iv, ivKey, def)
|
||||
}
|
||||
|
||||
func rawValue(raw map[string]any, key string) any {
|
||||
if v, ok := raw[key]; ok {
|
||||
if f, ok := v.(float64); ok {
|
||||
return int64(f)
|
||||
}
|
||||
return v
|
||||
}
|
||||
if v, ok := raw[strings.ToLower(key)]; ok { // Id -> id fallback
|
||||
if f, ok := v.(float64); ok {
|
||||
return int64(f)
|
||||
}
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func rawValueStr(raw map[string]any, key string) any {
|
||||
if s, ok := raw[key].(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nilNeg(v int) any {
|
||||
if v == -1 {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// per-table sentinel predicates: true => value should become NULL.
|
||||
func sentinelCombat(v any) bool { return isNeg1(v) || isNeg1f(v) }
|
||||
func sentinelReq(v any) bool { return isNeg1(v) || v == nil || v == "" }
|
||||
func sentinelEnh(v any) bool { return isNeg1(v) || isNeg1f(v) || v == nil || v == "" }
|
||||
func sentinelRating(v any) bool { return isNeg1(v) || isNeg1f(v) || v == nil }
|
||||
|
||||
func isNeg1(v any) bool { i, ok := v.(int); return ok && i == -1 }
|
||||
func isNeg1f(v any) bool { f, ok := v.(float64); return ok && f == -1.0 }
|
||||
|
||||
// nullify replaces sentinel values with nil. Returns nil for the whole table if
|
||||
// every value is sentinel (the per-table "skip insert" guard) — EXCEPT
|
||||
// enhancements, which always inserts; we keep its map even if all-null.
|
||||
func nullify(m map[string]any, isSentinel func(any) bool) map[string]any {
|
||||
any_ := false
|
||||
out := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
if isSentinel(v) {
|
||||
out[k] = nil
|
||||
} else {
|
||||
out[k] = v
|
||||
any_ = true
|
||||
}
|
||||
}
|
||||
if !any_ {
|
||||
// caller distinguishes; combat/req/ratings skip (nil), enhancements keeps.
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// mergeBaseValues reverses active-spell buffs into base_* columns (compute_base_values).
|
||||
type spellEffect struct {
|
||||
key int
|
||||
change, bonus float64
|
||||
}
|
||||
|
||||
var intEffects = map[int]spellEffect{
|
||||
1616: {218103842, 20, 0}, 2096: {218103842, 22, 0}, 5183: {218103842, 24, 0}, 4395: {218103842, 24, 0}, 3688: {218103842, 300, 0},
|
||||
2598: {218103842, 2, 2}, 2586: {218103842, 4, 4}, 4661: {218103842, 7, 7}, 6089: {218103842, 10, 10},
|
||||
1486: {28, 200, 0}, 2108: {28, 220, 0}, 4407: {28, 240, 0},
|
||||
2604: {28, 20, 20}, 2592: {28, 40, 40}, 4667: {28, 60, 60}, 6095: {28, 80, 80},
|
||||
}
|
||||
var doubleEffects = map[int]spellEffect{
|
||||
3258: {152, 0.06, 0}, 3259: {152, 0.07, 0}, 5182: {152, 0.08, 0}, 4414: {152, 0.08, 0}, 3735: {152, 0.15, 0},
|
||||
3251: {152, 0.01, 0.01}, 3250: {152, 0.03, 0.03}, 4670: {152, 0.05, 0.05}, 6098: {152, 0.07, 0.07},
|
||||
1592: {167772172, 0.15, 0}, 2106: {167772172, 0.17, 0}, 4405: {167772172, 0.20, 0},
|
||||
2603: {167772172, 0.03, 0.03}, 2591: {167772172, 0.05, 0.05}, 4666: {167772172, 0.07, 0.07}, 6094: {167772172, 0.09, 0.09},
|
||||
1605: {29, 0.15, 0}, 2101: {29, 0.17, 0}, 4400: {29, 0.20, 0}, 3699: {29, 0.25, 0},
|
||||
2600: {29, 0.03, 0.03}, 3985: {29, 0.04, 0.04}, 2588: {29, 0.05, 0.05}, 4663: {29, 0.07, 0.07}, 6091: {29, 0.09, 0.09},
|
||||
1480: {144, 1.60, 0}, 2117: {144, 1.70, 0}, 4418: {144, 1.80, 0},
|
||||
3201: {144, 1.05, 1.05}, 3199: {144, 1.10, 1.10}, 3202: {144, 1.15, 1.15}, 3200: {144, 1.20, 1.20}, 6086: {144, 1.25, 1.25}, 6087: {144, 1.30, 1.30},
|
||||
}
|
||||
|
||||
func (s *Server) mergeBaseValues(raw, combat map[string]any) {
|
||||
spells := toIntList(raw["Spells"])
|
||||
active := toIntList(raw["ActiveSpells"])
|
||||
for _, p := range []struct {
|
||||
prop string
|
||||
key int
|
||||
}{{"max_damage", 218103842}, {"armor_level", 28}} {
|
||||
val, ok := combat[p.prop].(int)
|
||||
if !ok || val == -1 {
|
||||
continue
|
||||
}
|
||||
for _, sid := range active {
|
||||
if e, ok := intEffects[sid]; ok && e.key == p.key {
|
||||
val -= int(e.change)
|
||||
}
|
||||
}
|
||||
for _, sid := range spells {
|
||||
if e, ok := intEffects[sid]; ok && e.key == p.key && e.bonus != 0 {
|
||||
val += int(e.bonus)
|
||||
}
|
||||
}
|
||||
combat["base_"+p.prop] = val
|
||||
}
|
||||
for _, p := range []struct {
|
||||
prop string
|
||||
key int
|
||||
}{{"attack_bonus", 167772172}, {"melee_defense_bonus", 29}, {"elemental_damage_vs_monsters", 152}, {"mana_conversion_bonus", 144}} {
|
||||
val, ok := combat[p.prop].(float64)
|
||||
if !ok || val == -1.0 {
|
||||
continue
|
||||
}
|
||||
for _, sid := range active {
|
||||
if e, ok := doubleEffects[sid]; ok && e.key == p.key {
|
||||
val -= e.change
|
||||
}
|
||||
}
|
||||
for _, sid := range spells {
|
||||
if e, ok := doubleEffects[sid]; ok && e.key == p.key && e.bonus != 0 {
|
||||
val += e.bonus
|
||||
}
|
||||
}
|
||||
combat["base_"+p.prop] = math.Round(val*10000) / 10000
|
||||
}
|
||||
}
|
||||
|
||||
// POST /debug/process — returns the normalized columns for a raw item JSON body
|
||||
// (loopback validation against production's stored rows; never writes).
|
||||
func (s *Server) handleDebugProcess(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(io.LimitReader(r.Body, 8<<20))
|
||||
var raw map[string]any
|
||||
if json.Unmarshal(body, &raw) != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid JSON"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, s.processItem(raw))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue