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"):
|
||||
conds = append(conds, "object_class = 4")
|
||||
case qBool(q, "weapon_only"):
|
||||
conds = append(conds, weaponTypeClause(q.Get("weapon_type")))
|
||||
conds = append(conds, weaponOnlyClause(q))
|
||||
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%'))")
|
||||
}
|
||||
|
|
@ -543,24 +543,36 @@ func underwearCTEWhere(q map[string][]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)
|
||||
}
|
||||
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) {
|
||||
case "heavy":
|
||||
return exists(44)
|
||||
return meleeSkill(44)
|
||||
case "light":
|
||||
return exists(45)
|
||||
return meleeSkill(45)
|
||||
case "finesse":
|
||||
return exists(46)
|
||||
return meleeSkill(46)
|
||||
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":
|
||||
return "(object_class = 9 AND name ILIKE '%bow%' AND name NOT ILIKE '%crossbow%')"
|
||||
return missileMastery(8)
|
||||
case "crossbow":
|
||||
return "(object_class = 9 AND name ILIKE '%crossbow%')"
|
||||
return missileMastery(9)
|
||||
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":
|
||||
return "object_class = 31"
|
||||
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 {
|
||||
switch strings.ToLower(name) {
|
||||
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