enrichRows now applies the material-name prefix to name (material is already a
translated string in the DB), sets material_name + original_name, and resolves
item_set_name via the AttributeSetInfo enum (fallback "Set {id}").
Validated vs Python position-by-position: 0 mismatches across 60 armor + 60
jewelry rows for name, material_name, item_set_name, original_name, value,
object_class. Sample names match exactly (e.g. "Gold Alduressa Coat").
Remaining enrichment slices: object_class_name (gem context), spells/spell_names
(needs the spells enum map), slot_name (sophisticated), weapon damage/speed/mana,
rating gear-total fallbacks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
535 lines
21 KiB
Go
535 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// /search/items — port of inventory-service main.py:2892. This slice implements
|
|
// the search QUERY (the CTE + all SQL filters + sort + pagination + count) and
|
|
// returns each row's direct DB columns plus the computed booleans. The deep
|
|
// per-row translation (material_name, spells, slot_name, ...) from
|
|
// extract_item_properties is layered on in a later slice; the filter/count logic
|
|
// — "which items match" — is validated here against the Python service.
|
|
|
|
// cteSelect is the items_with_slots CTE body (everything up to FROM/JOINs). The
|
|
// rating columns are extracted from the item_raw_data int_values JSONB exactly
|
|
// as Python does (paired ids via GREATEST, singletons via COALESCE).
|
|
const cteSelect = `
|
|
SELECT DISTINCT
|
|
i.id AS db_item_id, i.character_name, i.name, i.icon, i.object_class, i.value, i.burden,
|
|
i.current_wielded_location, i.bonded, i.attuned, i."unique", i.stack_size, i.max_stack_size,
|
|
i.structure, i.max_structure, i.rare_id, i.timestamp AS last_updated,
|
|
COALESCE(cs.max_damage, -1) AS max_damage,
|
|
COALESCE(cs.armor_level, -1) AS armor_level,
|
|
COALESCE(cs.attack_bonus, -1.0) AS attack_bonus,
|
|
COALESCE(cs.melee_defense_bonus, -1.0) AS melee_defense_bonus,
|
|
COALESCE(cs.weapon_time, -1) AS weapon_time,
|
|
COALESCE(cs.base_armor_level, cs.armor_level, -1) AS base_armor_level,
|
|
COALESCE(cs.base_max_damage, cs.max_damage, -1) AS base_max_damage,
|
|
GREATEST(COALESCE((rd.int_values->>'314')::int, -1), COALESCE((rd.int_values->>'374')::int, -1)) AS crit_damage_rating,
|
|
GREATEST(COALESCE((rd.int_values->>'307')::int, -1), COALESCE((rd.int_values->>'370')::int, -1)) AS damage_rating,
|
|
GREATEST(COALESCE((rd.int_values->>'323')::int, -1), COALESCE((rd.int_values->>'376')::int, -1)) AS heal_boost_rating,
|
|
COALESCE((rd.int_values->>'379')::int, -1) AS vitality_rating,
|
|
GREATEST(COALESCE((rd.int_values->>'308')::int, -1), COALESCE((rd.int_values->>'371')::int, -1)) AS damage_resist_rating,
|
|
COALESCE((rd.int_values->>'315')::int, -1) AS crit_resist_rating,
|
|
GREATEST(COALESCE((rd.int_values->>'316')::int, -1), COALESCE((rd.int_values->>'375')::int, -1)) AS crit_damage_resist_rating,
|
|
COALESCE((rd.int_values->>'317')::int, -1) AS healing_resist_rating,
|
|
COALESCE((rd.int_values->>'331')::int, -1) AS nether_resist_rating,
|
|
COALESCE((rd.int_values->>'342')::int, -1) AS healing_rating,
|
|
COALESCE((rd.int_values->>'350')::int, -1) AS dot_resist_rating,
|
|
COALESCE((rd.int_values->>'351')::int, -1) AS life_resist_rating,
|
|
COALESCE((rd.int_values->>'356')::int, -1) AS sneak_attack_rating,
|
|
COALESCE((rd.int_values->>'357')::int, -1) AS recklessness_rating,
|
|
COALESCE((rd.int_values->>'358')::int, -1) AS deception_rating,
|
|
COALESCE((rd.int_values->>'381')::int, -1) AS pk_damage_rating,
|
|
COALESCE((rd.int_values->>'382')::int, -1) AS pk_damage_resist_rating,
|
|
COALESCE((rd.int_values->>'383')::int, -1) AS gear_pk_damage_rating,
|
|
COALESCE((rd.int_values->>'384')::int, -1) AS gear_pk_damage_resist_rating,
|
|
COALESCE(req.wield_level, -1) AS wield_level,
|
|
COALESCE(enh.material, '') AS material,
|
|
COALESCE(enh.workmanship, -1.0) AS workmanship,
|
|
COALESCE(enh.imbue, '') AS imbue,
|
|
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
|
|
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
|
LEFT JOIN item_requirements req ON i.id = req.item_id
|
|
LEFT JOIN item_enhancements enh ON i.id = enh.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`
|
|
|
|
var sortMapping = map[string]string{
|
|
"name": "name", "character_name": "character_name", "value": "value",
|
|
"damage": "max_damage", "armor": "armor_level", "armor_level": "armor_level",
|
|
"workmanship": "workmanship", "level": "wield_level", "damage_rating": "damage_rating",
|
|
"crit_damage_rating": "crit_damage_rating", "heal_boost_rating": "heal_boost_rating",
|
|
"vitality_rating": "vitality_rating", "damage_resist_rating": "damage_resist_rating",
|
|
"crit_damage_resist_rating": "crit_damage_resist_rating", "item_set": "item_set",
|
|
"coverage": "coverage_mask", "item_type_name": "object_class",
|
|
"last_updated": "last_updated", "spell_names": "computed_spell_names",
|
|
}
|
|
|
|
// argBuilder accumulates positional ($N) query args.
|
|
type argBuilder struct{ args []any }
|
|
|
|
func (b *argBuilder) add(v any) string {
|
|
b.args = append(b.args, v)
|
|
return "$" + strconv.Itoa(len(b.args))
|
|
}
|
|
|
|
func (s *Server) handleSearchItems(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
ab := &argBuilder{}
|
|
var conds []string
|
|
|
|
// --- character (mutually exclusive cascade) ---
|
|
if c := q.Get("character"); c != "" {
|
|
conds = append(conds, "character_name = "+ab.add(c))
|
|
} else if cs := q.Get("characters"); cs != "" {
|
|
names := splitNonEmpty(cs)
|
|
if len(names) == 0 {
|
|
writeJSON(w, http.StatusOK, map[string]any{"error": "Empty characters list provided", "items": []any{}, "total_count": 0})
|
|
return
|
|
}
|
|
ph := make([]string, len(names))
|
|
for i, n := range names {
|
|
ph[i] = ab.add(n)
|
|
}
|
|
conds = append(conds, "character_name IN ("+strings.Join(ph, ", ")+")")
|
|
} else if !qBool(q, "include_all_characters") {
|
|
writeJSON(w, http.StatusOK, map[string]any{"error": "Must specify character, characters, or set include_all_characters=true", "items": []any{}, "total_count": 0})
|
|
return
|
|
}
|
|
|
|
// --- text ---
|
|
if t := q.Get("text"); t != "" {
|
|
p := ab.add("%" + t + "%")
|
|
conds = append(conds, "(CONCAT(COALESCE(material,''),' ',name) ILIKE "+p+" OR name ILIKE "+p+" OR COALESCE(material,'') ILIKE "+p+")")
|
|
}
|
|
|
|
// --- category (mutually exclusive) ---
|
|
switch {
|
|
case qBool(q, "armor_only"):
|
|
conds = append(conds, "(object_class = 2 AND COALESCE(armor_level,0) > 0)")
|
|
case qBool(q, "jewelry_only"):
|
|
conds = append(conds, "object_class = 4")
|
|
case qBool(q, "weapon_only"):
|
|
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%'))")
|
|
}
|
|
|
|
// --- equipment status / slot ---
|
|
switch q.Get("equipment_status") {
|
|
case "equipped":
|
|
conds = append(conds, "current_wielded_location > 0")
|
|
case "unequipped":
|
|
conds = append(conds, "current_wielded_location = 0")
|
|
}
|
|
if v, ok := qInt(q, "equipment_slot"); ok {
|
|
conds = append(conds, "current_wielded_location = "+ab.add(v))
|
|
}
|
|
|
|
// --- combat + all rating filters (column >= :param) ---
|
|
geFilters := []struct{ param, col string }{
|
|
{"min_damage", "max_damage"}, {"min_armor", "armor_level"},
|
|
{"min_crit_damage_rating", "crit_damage_rating"}, {"min_damage_rating", "damage_rating"},
|
|
{"min_heal_boost_rating", "heal_boost_rating"}, {"min_vitality_rating", "vitality_rating"},
|
|
{"min_damage_resist_rating", "damage_resist_rating"}, {"min_crit_resist_rating", "crit_resist_rating"},
|
|
{"min_crit_damage_resist_rating", "crit_damage_resist_rating"}, {"min_healing_resist_rating", "healing_resist_rating"},
|
|
{"min_nether_resist_rating", "nether_resist_rating"}, {"min_healing_rating", "healing_rating"},
|
|
{"min_dot_resist_rating", "dot_resist_rating"}, {"min_life_resist_rating", "life_resist_rating"},
|
|
{"min_sneak_attack_rating", "sneak_attack_rating"}, {"min_recklessness_rating", "recklessness_rating"},
|
|
{"min_deception_rating", "deception_rating"}, {"min_pk_damage_rating", "pk_damage_rating"},
|
|
{"min_pk_damage_resist_rating", "pk_damage_resist_rating"}, {"min_gear_pk_damage_rating", "gear_pk_damage_rating"},
|
|
{"min_gear_pk_damage_resist_rating", "gear_pk_damage_resist_rating"}, {"min_tinks", "tinks"},
|
|
{"min_value", "value"}, {"min_workmanship", "workmanship"},
|
|
}
|
|
for _, f := range geFilters {
|
|
if v := q.Get(f.param); v != "" {
|
|
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
|
conds = append(conds, f.col+" >= "+ab.add(n))
|
|
}
|
|
}
|
|
}
|
|
leFilters := []struct{ param, col string }{
|
|
{"max_damage", "max_damage"}, {"max_armor", "armor_level"},
|
|
{"max_value", "value"}, {"max_burden", "burden"},
|
|
}
|
|
for _, f := range leFilters {
|
|
if v, ok := qInt(q, f.param); ok {
|
|
conds = append(conds, f.col+" <= "+ab.add(v))
|
|
}
|
|
}
|
|
if v := q.Get("min_attack_bonus"); v != "" {
|
|
if n, err := strconv.ParseFloat(v, 64); err == nil {
|
|
conds = append(conds, "attack_bonus >= "+ab.add(n))
|
|
}
|
|
}
|
|
|
|
// --- requirements (wield level) ---
|
|
if v, ok := qInt(q, "max_level"); ok {
|
|
conds = append(conds, "(wield_level <= "+ab.add(v)+" OR wield_level IS NULL)")
|
|
}
|
|
if v, ok := qInt(q, "min_level"); ok {
|
|
conds = append(conds, "wield_level >= "+ab.add(v))
|
|
}
|
|
|
|
// --- enhancements ---
|
|
if m := q.Get("material"); m != "" {
|
|
conds = append(conds, "material ILIKE "+ab.add("%"+m+"%"))
|
|
}
|
|
if v := q.Get("has_imbue"); v != "" {
|
|
if qBool(q, "has_imbue") {
|
|
conds = append(conds, "(imbue IS NOT NULL AND imbue != '')")
|
|
} else {
|
|
conds = append(conds, "(imbue IS NULL OR imbue = '')")
|
|
}
|
|
}
|
|
|
|
// --- item state ---
|
|
if v := q.Get("bonded"); v != "" {
|
|
conds = append(conds, ternary(qBool(q, "bonded"), "bonded > 0", "bonded = 0"))
|
|
}
|
|
if v := q.Get("attuned"); v != "" {
|
|
conds = append(conds, ternary(qBool(q, "attuned"), "attuned > 0", "attuned = 0"))
|
|
}
|
|
if v := q.Get("unique"); v != "" {
|
|
conds = append(conds, `"unique" = `+ab.add(qBool(q, "unique")))
|
|
}
|
|
if v := q.Get("is_rare"); v != "" {
|
|
conds = append(conds, ternary(qBool(q, "is_rare"), "rare_id IS NOT NULL AND rare_id > 0", "(rare_id IS NULL OR rare_id <= 0)"))
|
|
}
|
|
if v, ok := qInt(q, "min_condition"); ok {
|
|
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 ")
|
|
}
|
|
|
|
// --- sort ---
|
|
sortField, ok := sortMapping[q.Get("sort_by")]
|
|
if !ok {
|
|
sortField = "name"
|
|
}
|
|
dir, nulls := "ASC", "NULLS LAST"
|
|
if strings.EqualFold(q.Get("sort_dir"), "desc") {
|
|
dir, nulls = "DESC", "NULLS FIRST"
|
|
}
|
|
orderBy := fmt.Sprintf(" ORDER BY %s %s %s, character_name, db_item_id", sortField, dir, nulls)
|
|
|
|
// --- pagination ---
|
|
page := clampInt(qIntDefault(q, "page", 1), 1, 1<<30)
|
|
limit := clampInt(qIntDefault(q, "limit", 200), 1, 50000)
|
|
offset := (page - 1) * limit
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
cte := "WITH items_with_slots AS (" + cteSelect + ")\n"
|
|
mainSQL := cte + "SELECT * FROM items_with_slots" + where + orderBy +
|
|
" LIMIT " + ab.add(limit) + " OFFSET " + ab.add(offset)
|
|
rows, err := queryRowsAsMaps(ctx, s.pool, mainSQL, ab.args...)
|
|
if err != nil {
|
|
s.dbErr(w, "search/items", err)
|
|
return
|
|
}
|
|
|
|
// count uses the same CTE + conditions (full CTE is correct; Python trims it
|
|
// only as an optimization). LIMIT/OFFSET args are unused here.
|
|
countSQL := cte + "SELECT COUNT(DISTINCT db_item_id) FROM items_with_slots" + where
|
|
var totalCount int64
|
|
if err := s.pool.QueryRow(ctx, countSQL, ab.args[:len(ab.args)-2]...).Scan(&totalCount); err != nil {
|
|
s.dbErr(w, "search/items count", err)
|
|
return
|
|
}
|
|
|
|
items := s.enrichRows(rows)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"items": items,
|
|
"total_count": totalCount,
|
|
"page": page,
|
|
"limit": limit,
|
|
"has_next": int64(page*limit) < totalCount,
|
|
"has_previous": page > 1,
|
|
})
|
|
}
|
|
|
|
// enrichRows applies the direct-column transforms (computed booleans, condition,
|
|
// timestamp), the material-name prefix, and the item_set name, then strips
|
|
// internal columns. Deeper enrichment (spells, slot_name, weapon damage/mana,
|
|
// rating fallbacks) is a later slice.
|
|
func (s *Server) enrichRows(rows []map[string]any) []map[string]any {
|
|
out := make([]map[string]any, 0, len(rows))
|
|
for _, row := range rows {
|
|
row["is_equipped"] = toInt64(row["current_wielded_location"]) > 0
|
|
row["is_bonded"] = toInt64(row["bonded"]) > 0
|
|
row["is_attuned"] = toInt64(row["attuned"]) > 0
|
|
row["is_rare"] = toInt64(row["rare_id"]) > 0
|
|
st, mx := row["structure"], row["max_structure"]
|
|
if st != nil && mx != nil && toFloat(mx) != 0 {
|
|
row["condition_percent"] = roundTo(toFloat(st)*100/toFloat(mx), 1)
|
|
} else {
|
|
row["condition_percent"] = nil
|
|
}
|
|
if t, ok := row["last_updated"].(time.Time); ok {
|
|
row["last_updated"] = pyISO(t)
|
|
}
|
|
|
|
// material_name + material prefix on name (material is already a
|
|
// translated string in the DB; enrich_db_item:2371-2602).
|
|
if mat := toStr(row["material"]); mat != "" {
|
|
row["material_name"] = mat
|
|
if name := toStr(row["name"]); name != "" &&
|
|
!strings.HasPrefix(strings.ToLower(name), strings.ToLower(mat)) {
|
|
row["original_name"] = name
|
|
row["name"] = mat + " " + name
|
|
}
|
|
}
|
|
// item_set_name (enrich_db_item:2551-2562).
|
|
if iset := strings.TrimSpace(toStr(row["item_set"])); iset != "" {
|
|
if n, ok := s.attributeSets[iset]; ok {
|
|
row["item_set_name"] = n
|
|
} else {
|
|
row["item_set_name"] = "Set " + iset
|
|
}
|
|
}
|
|
|
|
delete(row, "db_item_id")
|
|
out = append(out, row)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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, ",") {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func qBool(q map[string][]string, key string) bool {
|
|
v := ""
|
|
if vs, ok := q[key]; ok && len(vs) > 0 {
|
|
v = vs[0]
|
|
}
|
|
switch strings.ToLower(v) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func qInt(q map[string][]string, key string) (int, bool) {
|
|
if vs, ok := q[key]; ok && len(vs) > 0 && vs[0] != "" {
|
|
if n, err := strconv.Atoi(vs[0]); err == nil {
|
|
return n, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func qIntDefault(q map[string][]string, key string, def int) int {
|
|
if n, ok := qInt(q, key); ok {
|
|
return n
|
|
}
|
|
return def
|
|
}
|
|
|
|
func ternary(c bool, a, b string) string {
|
|
if c {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func clampInt(v, lo, hi int) int {
|
|
if v < lo {
|
|
return lo
|
|
}
|
|
if v > hi {
|
|
return hi
|
|
}
|
|
return v
|
|
}
|
|
|
|
func toInt64(v any) int64 {
|
|
switch x := v.(type) {
|
|
case int64:
|
|
return x
|
|
case int32:
|
|
return int64(x)
|
|
case int:
|
|
return int64(x)
|
|
case float64:
|
|
return int64(x)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func toFloat(v any) float64 {
|
|
switch x := v.(type) {
|
|
case float64:
|
|
return x
|
|
case float32:
|
|
return float64(x)
|
|
case int64:
|
|
return float64(x)
|
|
case int32:
|
|
return float64(x)
|
|
case int:
|
|
return float64(x)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func roundTo(v float64, places int) float64 {
|
|
p := 1.0
|
|
for i := 0; i < places; i++ {
|
|
p *= 10
|
|
}
|
|
r := v * p
|
|
if r < 0 {
|
|
r -= 0.5
|
|
} else {
|
|
r += 0.5
|
|
}
|
|
return float64(int64(r)) / p
|
|
}
|