package main import ( "net/http" "strings" ) // GET /inventory/{character_name} — full per-character inventory for the React // Inventory window. Port of inventory-service get_character_inventory + // enrich_db_item (main.py:2622 / 2338). The Go cutover omitted this endpoint // (it was assumed unused), but the React InventoryWindow fetches it, so its // absence (404) made the live inventory render empty. // // Returns {character_name, item_count, items:[...]} with the snake_case fields // the frontend normalizeItem consumes: placement via current_wielded_location / // container_id / items_capacity, the mana panel via current_mana / max_mana, // icon overlays, plus tooltip combat/requirement/enhancement/rating stats. Mana // and icon overlays are pulled straight from original_json IntValues (same keys // the plugin/search path use); the rest come from the normalized join tables. func (s *Server) handleCharacterInventory(w http.ResponseWriter, r *http.Request) { name := r.PathValue("character_name") limit := clampInt(qIntDefault(r.URL.Query(), "limit", 1000), 1, 5000) offset := qIntDefault(r.URL.Query(), "offset", 0) if offset < 0 { offset = 0 } const q = ` SELECT i.item_id, i.name, i.icon, i.object_class, i.value, i.burden, i.current_wielded_location, i.container_id, i.items_capacity, i.stack_size, cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus, cs.melee_defense_bonus, cs.magic_defense_bonus, r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement, e.material, e.imbue, e.item_set, e.tinks, e.workmanship, rt.damage_rating, rt.crit_rating, rt.crit_damage_rating, rt.heal_boost_rating, NULLIF((rd.original_json->'IntValues'->>'218103815')::int, 0) AS current_mana, NULLIF((rd.original_json->'IntValues'->>'218103814')::int, 0) AS max_mana, NULLIF((rd.original_json->'IntValues'->>'218103849')::int, 0) AS icon_overlay_id, NULLIF((rd.original_json->'IntValues'->>'218103850')::int, 0) AS icon_underlay_id FROM items i LEFT JOIN item_combat_stats cs ON i.id = cs.item_id LEFT JOIN item_requirements r ON i.id = r.item_id LEFT JOIN item_enhancements e ON i.id = e.item_id LEFT JOIN item_ratings rt ON i.id = rt.item_id LEFT JOIN item_raw_data rd ON i.id = rd.item_id WHERE i.character_name = $1 ORDER BY i.name LIMIT $2 OFFSET $3` rows, err := queryRowsAsMaps(r.Context(), s.pool, q, name, limit, offset) if err != nil { s.dbErr(w, "inventory/"+name, err) return } items := make([]map[string]any, 0, len(rows)) for _, row := range rows { items = append(items, enrichInventoryRow(row)) } // Unlike the Python endpoint (404 on no rows), always return 200 with a // possibly-empty list — the window treats both as empty, and 200 avoids the // frontend's catch-all error path. writeJSON(w, http.StatusOK, map[string]any{ "character_name": name, "item_count": len(items), "items": items, }) } // enrichInventoryRow flattens a joined inventory row into the frontend item // shape: drops NULL columns and applies the material-name prefix to the item // name (enrich_db_item parity, e.g. "Pyreal" + "Chiran Helm" -> "Pyreal Chiran // Helm"), preserving the un-prefixed name in original_name. func enrichInventoryRow(row map[string]any) map[string]any { out := make(map[string]any, len(row)+2) for k, v := range row { if v != nil { out[k] = v } } if mat, ok := out["material"].(string); ok && mat != "" { out["material_name"] = mat if name, ok := out["name"].(string); ok && name != "" && !strings.HasPrefix(strings.ToLower(name), strings.ToLower(mat)) { out["name"] = mat + " " + name out["original_name"] = name } } return out }