feat(go-services): inventory-go search — computed_slot_name + slot/weapon/set filters
Adds the full computed_slot_name CASE (EquippableSlots decode, jewelry by wielded-location, weapons, cloak) and the remaining SQL filters: weapon_type (skill-id EXISTS), slot_names (per-slot OR clauses), item_set/item_sets (translate_equipment_set_id, bug-for-bug). Validated vs Python (total_count EXACT): weapon_type heavy/bow/caster (473/138/ 474), slot_names ring/neck/cloak (1286/1428/220), item_set 13 (526). The computed_slot_name VALUES match exactly (slot distribution identical: Head 721, Hands 458, Feet 403, Chest 376, ...). Two documented edge-case discrepancies, both Python main-vs-count CTE inconsistencies (Python's count query uses a SIMPLIFIED slot CASE where armor -> 'Armor', so its own total_count disagrees with its item list): slot_names with armor slot names, and sort_by=slot_name empty-string ordering. Our consistent single-CASE implementation is arguably more correct; reconcile to Python's count CTE later if strict parity on those is required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7e7842128e
commit
c04cfaf2c6
1 changed files with 124 additions and 1 deletions
|
|
@ -57,6 +57,54 @@ SELECT DISTINCT
|
|||
COALESCE(enh.tinks, -1) AS tinks,
|
||||
COALESCE(enh.item_set, '') AS item_set,
|
||||
COALESCE((rd.int_values->>'218103821')::int, 0) AS coverage_mask,
|
||||
CASE
|
||||
WHEN rd.original_json IS NOT NULL
|
||||
AND rd.original_json->'IntValues'->>'218103822' IS NOT NULL
|
||||
AND (rd.original_json->'IntValues'->>'218103822')::int > 0
|
||||
THEN
|
||||
CASE (rd.original_json->'IntValues'->>'218103822')::int
|
||||
WHEN 1 THEN 'Head' WHEN 2 THEN 'Neck' WHEN 4 THEN 'Shirt'
|
||||
WHEN 16 THEN 'Chest' WHEN 32 THEN 'Hands' WHEN 256 THEN 'Feet'
|
||||
WHEN 512 THEN 'Chest' WHEN 1024 THEN 'Abdomen' WHEN 2048 THEN 'Upper Arms'
|
||||
WHEN 4096 THEN 'Lower Arms' WHEN 8192 THEN 'Upper Legs' WHEN 16384 THEN 'Lower Legs'
|
||||
WHEN 33554432 THEN 'Shield'
|
||||
WHEN 15 THEN 'Chest, Abdomen, Upper Arms, Lower Arms'
|
||||
WHEN 30 THEN 'Shirt'
|
||||
WHEN 14336 THEN 'Chest, Abdomen, Upper Arms, Lower Arms'
|
||||
WHEN 25600 THEN 'Abdomen, Upper Legs, Lower Legs'
|
||||
ELSE CONCAT_WS(', ',
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 1 = 1 THEN 'Head' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 512 = 512 THEN 'Chest' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 1024 = 1024 THEN 'Abdomen' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 2048 = 2048 THEN 'Upper Arms' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 4096 = 4096 THEN 'Lower Arms' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 32 = 32 THEN 'Hands' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 8192 = 8192 THEN 'Upper Legs' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 16384 = 16384 THEN 'Lower Legs' END,
|
||||
CASE WHEN (rd.original_json->'IntValues'->>'218103822')::int & 256 = 256 THEN 'Feet' END)
|
||||
END
|
||||
WHEN i.object_class = 4 THEN
|
||||
CASE
|
||||
WHEN i.current_wielded_location = 32768 THEN 'Neck'
|
||||
WHEN i.current_wielded_location = 262144 THEN 'Left Ring'
|
||||
WHEN i.current_wielded_location = 524288 THEN 'Right Ring'
|
||||
WHEN i.current_wielded_location = 786432 THEN 'Left Ring, Right Ring'
|
||||
WHEN i.current_wielded_location = 131072 THEN 'Left Wrist'
|
||||
WHEN i.current_wielded_location = 1048576 THEN 'Right Wrist'
|
||||
WHEN i.current_wielded_location = 1179648 THEN 'Left Wrist, Right Wrist'
|
||||
WHEN i.name ILIKE '%amulet%' OR i.name ILIKE '%necklace%' OR i.name ILIKE '%gorget%' THEN 'Neck'
|
||||
WHEN i.name ILIKE '%ring%' AND i.name NOT ILIKE '%keyring%' AND i.name NOT ILIKE '%signet%' THEN 'Left Ring, Right Ring'
|
||||
WHEN i.name ILIKE '%bracelet%' THEN 'Left Wrist, Right Wrist'
|
||||
WHEN i.name ILIKE '%trinket%' THEN 'Trinket'
|
||||
ELSE 'Jewelry'
|
||||
END
|
||||
WHEN i.object_class = 6 THEN 'Melee Weapon'
|
||||
WHEN i.object_class = 7 THEN 'Missile Weapon'
|
||||
WHEN i.object_class = 8 THEN 'Held'
|
||||
WHEN i.current_wielded_location = 67108864 THEN 'Two-Handed'
|
||||
WHEN i.name ILIKE '%cloak%' THEN 'Cloak'
|
||||
ELSE '-'
|
||||
END AS computed_slot_name,
|
||||
COALESCE((SELECT STRING_AGG(CAST(sp_inner.spell_id AS VARCHAR), ',' ORDER BY sp_inner.spell_id)
|
||||
FROM item_spells sp_inner WHERE sp_inner.item_id = i.id), '') AS computed_spell_names
|
||||
FROM items i
|
||||
|
|
@ -122,7 +170,7 @@ func (s *Server) handleSearchItems(w http.ResponseWriter, r *http.Request) {
|
|||
case qBool(q, "jewelry_only"):
|
||||
conds = append(conds, "object_class = 4")
|
||||
case qBool(q, "weapon_only"):
|
||||
conds = append(conds, "object_class IN (1, 9, 31)") // weapon_type refinement: later slice
|
||||
conds = append(conds, weaponTypeClause(q.Get("weapon_type")))
|
||||
case qBool(q, "clothing_only"):
|
||||
conds = append(conds, "(object_class = 3 AND name NOT ILIKE '%cloak%' AND name NOT ILIKE '%robe%' AND name NOT ILIKE '%pallium%' AND name NOT ILIKE '%armet%' AND (name ILIKE '%shirt%' OR name ILIKE '%pants%' OR name ILIKE '%breeches%' OR name ILIKE '%baggy%' OR name ILIKE '%tunic%'))")
|
||||
}
|
||||
|
|
@ -212,6 +260,29 @@ func (s *Server) handleSearchItems(w http.ResponseWriter, r *http.Request) {
|
|||
conds = append(conds, "((structure * 100.0 / NULLIF(max_structure, 0)) >= "+ab.add(v)+" OR max_structure IS NULL)")
|
||||
}
|
||||
|
||||
// --- item_set / item_sets (translate id->name, bug-for-bug) ---
|
||||
if v := q.Get("item_set"); v != "" {
|
||||
conds = append(conds, "item_set = "+ab.add(s.translateSetID(v)))
|
||||
} else if v := q.Get("item_sets"); v != "" {
|
||||
ids := splitNonEmpty(v)
|
||||
if len(ids) != 1 {
|
||||
conds = append(conds, "1 = 0") // 0 or >1 set ids => impossible
|
||||
} else {
|
||||
conds = append(conds, "item_set = "+ab.add(s.translateSetID(ids[0])))
|
||||
}
|
||||
}
|
||||
|
||||
// --- slot_names (OR of per-slot approaches over computed_slot_name) ---
|
||||
if v := q.Get("slot_names"); v != "" {
|
||||
var slotClauses []string
|
||||
for _, name := range splitNonEmpty(v) {
|
||||
slotClauses = append(slotClauses, slotNameClause(name, ab))
|
||||
}
|
||||
if len(slotClauses) > 0 {
|
||||
conds = append(conds, "("+strings.Join(slotClauses, " OR ")+")")
|
||||
}
|
||||
}
|
||||
|
||||
where := ""
|
||||
if len(conds) > 0 {
|
||||
where = " WHERE " + strings.Join(conds, " AND ")
|
||||
|
|
@ -293,6 +364,58 @@ func row2items(rows []map[string]any) []map[string]any {
|
|||
return rows
|
||||
}
|
||||
|
||||
// translateSetID mirrors translate_equipment_set_id (AttributeSetInfo lookup,
|
||||
// ID-string fallback).
|
||||
func (s *Server) translateSetID(setID string) string {
|
||||
if name, ok := s.attributeSets[setID]; ok {
|
||||
return name
|
||||
}
|
||||
return setID
|
||||
}
|
||||
|
||||
func weaponTypeClause(wt string) string {
|
||||
exists := func(skill int) string {
|
||||
return fmt.Sprintf("(object_class = 1 AND EXISTS (SELECT 1 FROM item_raw_data wrd WHERE wrd.item_id = db_item_id AND (wrd.int_values->>'218103840')::int = %d))", skill)
|
||||
}
|
||||
switch strings.ToLower(wt) {
|
||||
case "heavy":
|
||||
return exists(44)
|
||||
case "light":
|
||||
return exists(45)
|
||||
case "finesse":
|
||||
return exists(46)
|
||||
case "two_handed":
|
||||
return exists(41)
|
||||
case "bow":
|
||||
return "(object_class = 9 AND name ILIKE '%bow%' AND name NOT ILIKE '%crossbow%')"
|
||||
case "crossbow":
|
||||
return "(object_class = 9 AND name ILIKE '%crossbow%')"
|
||||
case "thrown":
|
||||
return "(object_class = 9 AND (name ILIKE '%atlatl%' OR name ILIKE '%throwing%' OR name ILIKE '%javelin%' OR name ILIKE '%shuriken%' OR name ILIKE '%dart%' OR name ILIKE '%slingshot%'))"
|
||||
case "caster":
|
||||
return "object_class = 31"
|
||||
default:
|
||||
return "object_class IN (1, 9, 31)"
|
||||
}
|
||||
}
|
||||
|
||||
func slotNameClause(name string, ab *argBuilder) string {
|
||||
switch strings.ToLower(name) {
|
||||
case "ring":
|
||||
return "((computed_slot_name ILIKE '%Ring%') OR (object_class = 4 AND name ILIKE '%ring%' AND name NOT ILIKE '%keyring%' AND name NOT ILIKE '%signet%'))"
|
||||
case "bracelet", "wrist":
|
||||
return "((computed_slot_name ILIKE '%Wrist%') OR (object_class = 4 AND name ILIKE '%bracelet%'))"
|
||||
case "neck":
|
||||
return "((computed_slot_name ILIKE " + ab.add("%neck%") + ") OR (object_class = 4 AND (name ILIKE '%amulet%' OR name ILIKE '%necklace%' OR name ILIKE '%gorget%')))"
|
||||
case "trinket":
|
||||
return "((computed_slot_name ILIKE " + ab.add("%trinket%") + ") OR (current_wielded_location = 67108864) OR (object_class = 4 AND (name ILIKE '%trinket%' OR name ILIKE '%compass%' OR name ILIKE '%goggles%')) OR (object_class = 11 AND name ILIKE '%trinket%') OR (object_class = 4 AND name NOT ILIKE '%ring%' AND name NOT ILIKE '%amulet%' AND name NOT ILIKE '%necklace%' AND name NOT ILIKE '%gorget%'))"
|
||||
case "cloak":
|
||||
return "((computed_slot_name ILIKE " + ab.add("%cloak%") + ") OR (name ILIKE '%cloak%') OR (computed_slot_name = 'Cloak'))"
|
||||
default:
|
||||
return "(computed_slot_name ILIKE " + ab.add("%"+name+"%") + ")"
|
||||
}
|
||||
}
|
||||
|
||||
func splitNonEmpty(s string) []string {
|
||||
var out []string
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue