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:
parent
29d400f86a
commit
b4bee24773
2 changed files with 111 additions and 9 deletions
|
|
@ -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":
|
||||||
|
|
|
||||||
62
go-services/inventory-go/search_test.go
Normal file
62
go-services/inventory-go/search_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue