feat(inventory-go): weapon_types multi-select filter by skill (melee/missile/war/void)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-15 08:41:43 +02:00
parent 29d400f86a
commit b4bee24773
2 changed files with 111 additions and 9 deletions

View file

@ -191,7 +191,7 @@ func (s *Server) runSearch(ctx context.Context, q url.Values) (map[string]any, e
case qBool(q, "jewelry_only"): case qBool(q, "jewelry_only"):
conds = append(conds, "object_class = 4") conds = append(conds, "object_class = 4")
case qBool(q, "weapon_only"): case qBool(q, "weapon_only"):
conds = append(conds, weaponTypeClause(q.Get("weapon_type"))) conds = append(conds, weaponOnlyClause(q))
case qBool(q, "clothing_only"): 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%'))") 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%'))")
} }
@ -543,24 +543,36 @@ func underwearCTEWhere(q map[string][]string) string {
} }
func weaponTypeClause(wt string) string { func weaponTypeClause(wt string) string {
exists := func(skill int) string { meleeSkill := 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) 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)
} }
missileMastery := func(mastery int) string {
return fmt.Sprintf("(object_class = 9 AND EXISTS (SELECT 1 FROM item_raw_data wrd WHERE wrd.item_id = db_item_id AND (wrd.int_values->>'218103840')::int = 47 AND (wrd.int_values->>'353')::int = %d))", mastery)
}
casterSkill := func(skill int) string {
return fmt.Sprintf("(object_class = 31 AND EXISTS (SELECT 1 FROM item_raw_data wrd WHERE wrd.item_id = db_item_id AND (wrd.int_values->>'159')::int = %d))", skill)
}
switch strings.ToLower(wt) { switch strings.ToLower(wt) {
case "heavy": case "heavy":
return exists(44) return meleeSkill(44)
case "light": case "light":
return exists(45) return meleeSkill(45)
case "finesse": case "finesse":
return exists(46) return meleeSkill(46)
case "two_handed": case "two_handed":
return exists(41) return meleeSkill(41)
case "missile":
return "(object_class = 9 AND EXISTS (SELECT 1 FROM item_raw_data wrd WHERE wrd.item_id = db_item_id AND (wrd.int_values->>'218103840')::int = 47))"
case "bow": case "bow":
return "(object_class = 9 AND name ILIKE '%bow%' AND name NOT ILIKE '%crossbow%')" return missileMastery(8)
case "crossbow": case "crossbow":
return "(object_class = 9 AND name ILIKE '%crossbow%')" return missileMastery(9)
case "thrown": 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%'))" return missileMastery(10)
case "war":
return casterSkill(34)
case "void":
return casterSkill(43)
case "caster": case "caster":
return "object_class = 31" return "object_class = 31"
default: default:
@ -568,6 +580,34 @@ func weaponTypeClause(wt string) string {
} }
} }
// weaponOnlyClause builds the WHERE fragment for weapon_only searches: an OR of
// the per-type clauses named in weapon_types (CSV) plus the legacy single
// weapon_type. Empty selection = all weapons. Types are deduped.
func weaponOnlyClause(q url.Values) string {
var types []string
types = append(types, splitNonEmpty(q.Get("weapon_types"))...)
if wt := strings.TrimSpace(q.Get("weapon_type")); wt != "" {
types = append(types, wt)
}
seen := map[string]bool{}
var clauses []string
for _, t := range types {
key := strings.ToLower(strings.TrimSpace(t))
if key == "" || seen[key] {
continue
}
seen[key] = true
clauses = append(clauses, weaponTypeClause(key))
}
if len(clauses) == 0 {
return "object_class IN (1, 9, 31)"
}
if len(clauses) == 1 {
return clauses[0]
}
return "(" + strings.Join(clauses, " OR ") + ")"
}
func slotNameClause(name string, ab *argBuilder) string { func slotNameClause(name string, ab *argBuilder) string {
switch strings.ToLower(name) { switch strings.ToLower(name) {
case "ring": case "ring":

View file

@ -0,0 +1,62 @@
package main
import (
"net/url"
"strings"
"testing"
)
func TestWeaponTypeClause(t *testing.T) {
cases := map[string][]string{
"heavy": {"object_class = 1", "'218103840')::int = 44"},
"light": {"object_class = 1", "= 45"},
"finesse": {"object_class = 1", "= 46"},
"two_handed": {"object_class = 1", "= 41"},
"missile": {"object_class = 9", "'218103840')::int = 47"},
"bow": {"object_class = 9", "'218103840')::int = 47", "'353')::int = 8"},
"crossbow": {"object_class = 9", "'353')::int = 9"},
"thrown": {"object_class = 9", "'353')::int = 10"},
"war": {"object_class = 31", "'159')::int = 34"},
"void": {"object_class = 31", "'159')::int = 43"},
"caster": {"object_class = 31"},
}
for wt, subs := range cases {
got := weaponTypeClause(wt)
for _, s := range subs {
if !strings.Contains(got, s) {
t.Errorf("weaponTypeClause(%q) = %q, missing %q", wt, got, s)
}
}
}
if got := weaponTypeClause("nonsense"); !strings.Contains(got, "object_class IN (1, 9, 31)") {
t.Errorf("unknown type = %q, want all-weapons fallback", got)
}
}
func TestWeaponOnlyClause(t *testing.T) {
// No types → all weapons.
if got := weaponOnlyClause(url.Values{}); !strings.Contains(got, "object_class IN (1, 9, 31)") {
t.Errorf("empty = %q, want all weapons", got)
}
// Single type via weapon_types.
got := weaponOnlyClause(url.Values{"weapon_types": {"heavy"}})
if !strings.Contains(got, "= 44") || strings.Contains(got, " OR ") {
t.Errorf("single = %q, want just heavy, no OR", got)
}
// Multiple → parenthesized OR, one clause per type.
got = weaponOnlyClause(url.Values{"weapon_types": {"heavy,two_handed,war"}})
if !strings.HasPrefix(got, "(") || !strings.HasSuffix(got, ")") ||
strings.Count(got, " OR ") != 2 ||
!strings.Contains(got, "= 44") || !strings.Contains(got, "= 41") || !strings.Contains(got, "= 34") {
t.Errorf("multi = %q, want (heavy OR two_handed OR war)", got)
}
// Legacy weapon_type still honored, deduped against weapon_types.
got = weaponOnlyClause(url.Values{"weapon_type": {"missile"}})
if !strings.Contains(got, "object_class = 9") {
t.Errorf("legacy weapon_type = %q, want missile", got)
}
got = weaponOnlyClause(url.Values{"weapon_types": {"heavy"}, "weapon_type": {"heavy"}})
if strings.Contains(got, " OR ") {
t.Errorf("dedup = %q, want single heavy clause", got)
}
}