major fixes for inventory

This commit is contained in:
erik 2025-07-02 10:29:36 +00:00
parent 00ef1d1f4b
commit 4d19e29847
10 changed files with 969 additions and 203 deletions

View file

@ -418,9 +418,6 @@ class DiscordRareMonitor:
# Post to Discord
await self.post_rare_to_discord(data, rare_type)
# DEBUG: Also post rare info to aclog for monitoring classification
await self.post_rare_debug_to_aclog(rare_name, rare_type, character_name)
logger.info(f"📬 Posted to Discord: {rare_name} ({rare_type}) from {character_name}")
except Exception as e:
@ -508,20 +505,24 @@ class DiscordRareMonitor:
logger.error(f"❌ Could not find channel {ACLOG_CHANNEL_ID}")
except Exception as e:
logger.error(f"💥 VORTEX WARNING POSTING FAILED: {e}", exc_info=True)
# Also post the original vortex message to sawatolife (raw format)
try:
await self.post_chat_to_discord(data)
logger.info(f"🌪️ Posted raw vortex message to sawatolife")
except Exception as e:
logger.error(f"❌ Error posting vortex to sawatolife: {e}")
return
elif "whirlwind of vortex" in chat_text.lower():
logger.debug(f"🌪️ Found 'whirlwind of vortex' but not exact match in: {repr(chat_text)}")
elif "jeebus" in chat_text.lower():
logger.debug(f"👀 Found 'jeebus' but not vortex pattern in: {repr(chat_text)}")
# Skip if this message contains any rare names (common or great)
if RARE_IN_CHAT_PATTERN.search(chat_text) or self.is_rare_message(chat_text):
logger.debug(f"🎯 Skipping rare message from {character_name}: {chat_text}")
return
# Regular chat logging
# Log all chat messages (no longer filtering rares)
logger.info(f"💬 Chat from {character_name}: {chat_text}")
# Post to AC Log channel
await self.post_chat_to_discord(data)
@ -660,6 +661,21 @@ class DiscordRareMonitor:
except Exception as e:
logger.error(f"❌ Error posting to Discord: {e}")
def clean_and_format_chat_message(self, chat_text: str) -> str:
"""Clean chat message by removing 'Dunking Rares:' prefix and add color coding."""
try:
# Remove "Dunking Rares: " prefix if present
cleaned_text = chat_text
if cleaned_text.startswith("Dunking Rares: "):
cleaned_text = cleaned_text[15:] # Remove "Dunking Rares: "
# Return cleaned text without any special formatting
return cleaned_text
except Exception as e:
logger.warning(f"⚠️ Error cleaning chat message: {e}")
return chat_text # Return original on error
async def post_chat_to_discord(self, data: dict):
"""Post chat message to sawatolife Discord channel."""
try:
@ -679,9 +695,11 @@ class DiscordRareMonitor:
except ValueError:
timestamp = datetime.now()
# Create simple message format similar to your old bot
# Clean and format the message
time_str = timestamp.strftime("%H:%M:%S")
message_content = f"`{time_str}` **{character_name}**: {chat_text}"
cleaned_chat = self.clean_and_format_chat_message(chat_text)
message_content = f"`{time_str}` {cleaned_chat}"
# Get sawatolife channel
channel = self.client.get_channel(SAWATOLIFE_CHANNEL_ID)
@ -695,23 +713,6 @@ class DiscordRareMonitor:
except Exception as e:
logger.error(f"❌ Error posting chat to Discord: {e}")
async def post_rare_debug_to_aclog(self, rare_name: str, rare_type: str, character_name: str):
"""Post rare classification debug info to sawatolife channel."""
try:
# Create debug message showing classification
debug_message = f"🔍 **RARE DEBUG**: `{rare_name}` → **{rare_type.upper()}** (found by {character_name})"
# Get sawatolife channel
channel = self.client.get_channel(SAWATOLIFE_CHANNEL_ID)
if channel:
await channel.send(debug_message)
logger.debug(f"📤 Posted rare debug to #{channel.name}: {rare_name} -> {rare_type}")
else:
logger.error(f"❌ Could not find sawatolife channel for debug: {SAWATOLIFE_CHANNEL_ID}")
except Exception as e:
logger.error(f"❌ Error posting rare debug to Discord: {e}")
async def post_vortex_warning_to_discord(self, data: dict):
"""Post vortex warning as a special alert message to Discord."""

View file

@ -107,6 +107,8 @@ services:
build: ./discord-rare-monitor
depends_on:
- dereth-tracker
volumes:
- "./discord-rare-monitor:/app"
environment:
DISCORD_RARE_BOT_TOKEN: "${DISCORD_RARE_BOT_TOKEN}"
DERETH_TRACKER_WS_URL: "ws://dereth-tracker:8765/ws/live"

View file

@ -190,10 +190,28 @@ class ItemRatings(Base):
# Utility ratings
heal_boost_rating = Column(Integer)
vitality_rating = Column(Integer)
healing_rating = Column(Integer) # Healing rating (enum 342)
mana_conversion_rating = Column(Integer)
weakness_rating = Column(Integer) # Weakness debuff strength
nether_over_time = Column(Integer) # Nether DoT
# Resist ratings
healing_resist_rating = Column(Integer) # Healing resist (enum 317)
nether_resist_rating = Column(Integer) # Nether resist (enum 331)
dot_resist_rating = Column(Integer) # DoT resist (enum 350)
life_resist_rating = Column(Integer) # Life resist (enum 351)
# Specialized ratings
sneak_attack_rating = Column(Integer) # Sneak attack (enum 356)
recklessness_rating = Column(Integer) # Recklessness (enum 357)
deception_rating = Column(Integer) # Deception (enum 358)
# PvP ratings
pk_damage_rating = Column(Integer) # PvP damage (enum 381)
pk_damage_resist_rating = Column(Integer) # PvP damage resist (enum 382)
gear_pk_damage_rating = Column(Integer) # Gear PvP damage (enum 383)
gear_pk_damage_resist_rating = Column(Integer) # Gear PvP damage resist (enum 384)
# Gear totals
gear_damage = Column(Integer) # Total gear damage
gear_damage_resist = Column(Integer) # Total gear damage resist

View file

@ -266,6 +266,17 @@ def derive_item_type_from_object_class(object_class: int, item_data: dict = None
# Fallback to "Misc" if ObjectClass not found in enum
return "Misc"
def translate_equipment_set_id(set_id: str) -> str:
"""Translate equipment set ID to set name using comprehensive database."""
dictionaries = ENUM_MAPPINGS.get('dictionaries', {})
attribute_set_info = dictionaries.get('AttributeSetInfo', {}).get('values', {})
set_name = attribute_set_info.get(str(set_id))
if set_name:
return set_name
else:
# Fallback to just return the ID as string (matches database storage fallback)
return str(set_id)
def translate_object_class(object_class_id: int, item_data: dict = None) -> str:
"""Translate object class ID to human-readable name with context-aware detection."""
# Use the extracted ObjectClass enum first
@ -1011,6 +1022,28 @@ def extract_item_properties(item_data: Dict[str, Any]) -> Dict[str, Any]:
'crit_damage_rating': int_values.get('314', int_values.get(314, -1)), # CritDamageRating
'heal_boost_rating': int_values.get('323', int_values.get(323, -1)), # HealingBoostRating
# Missing critical ratings
'damage_resist_rating': int_values.get('308', int_values.get(308, -1)), # DamageResistRating
'crit_resist_rating': int_values.get('315', int_values.get(315, -1)), # CritResistRating
'crit_damage_resist_rating': int_values.get('316', int_values.get(316, -1)), # CritDamageResistRating
'healing_resist_rating': int_values.get('317', int_values.get(317, -1)), # HealingResistRating
'nether_resist_rating': int_values.get('331', int_values.get(331, -1)), # NetherResistRating
'vitality_rating': int_values.get('341', int_values.get(341, -1)), # VitalityRating
'healing_rating': int_values.get('342', int_values.get(342, -1)), # LumAugHealingRating
'dot_resist_rating': int_values.get('350', int_values.get(350, -1)), # DotResistRating
'life_resist_rating': int_values.get('351', int_values.get(351, -1)), # LifeResistRating
# Specialized ratings
'sneak_attack_rating': int_values.get('356', int_values.get(356, -1)), # SneakAttackRating
'recklessness_rating': int_values.get('357', int_values.get(357, -1)), # RecklessnessRating
'deception_rating': int_values.get('358', int_values.get(358, -1)), # DeceptionRating
# PvP ratings
'pk_damage_rating': int_values.get('381', int_values.get(381, -1)), # PKDamageRating
'pk_damage_resist_rating': int_values.get('382', int_values.get(382, -1)), # PKDamageResistRating
'gear_pk_damage_rating': int_values.get('383', int_values.get(383, -1)), # GearPKDamageRating
'gear_pk_damage_resist_rating': int_values.get('384', int_values.get(384, -1)), # GearPKDamageResistRating
# Additional ratings
'weakness_rating': int_values.get('329', int_values.get(329, -1)),
'nether_over_time': int_values.get('330', int_values.get(330, -1)),
@ -1731,6 +1764,7 @@ async def search_items(
# Equipment filtering
equipment_status: str = Query(None, description="equipped, unequipped, or all"),
equipment_slot: int = Query(None, description="Equipment slot mask (e.g., 1=head, 512=chest)"),
slot_names: str = Query(None, description="Comma-separated list of slot names (e.g., Head,Chest,Ring)"),
# Item category filtering
armor_only: bool = Query(False, description="Show only armor items"),
@ -1751,6 +1785,22 @@ async def search_items(
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
min_damage_rating: int = Query(None, description="Minimum damage rating"),
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
min_vitality_rating: int = Query(None, description="Minimum vitality rating"),
min_damage_resist_rating: int = Query(None, description="Minimum damage resist rating"),
min_crit_resist_rating: int = Query(None, description="Minimum crit resist rating"),
min_crit_damage_resist_rating: int = Query(None, description="Minimum crit damage resist rating"),
min_healing_resist_rating: int = Query(None, description="Minimum healing resist rating"),
min_nether_resist_rating: int = Query(None, description="Minimum nether resist rating"),
min_healing_rating: int = Query(None, description="Minimum healing rating"),
min_dot_resist_rating: int = Query(None, description="Minimum DoT resist rating"),
min_life_resist_rating: int = Query(None, description="Minimum life resist rating"),
min_sneak_attack_rating: int = Query(None, description="Minimum sneak attack rating"),
min_recklessness_rating: int = Query(None, description="Minimum recklessness rating"),
min_deception_rating: int = Query(None, description="Minimum deception rating"),
min_pk_damage_rating: int = Query(None, description="Minimum PvP damage rating"),
min_pk_damage_resist_rating: int = Query(None, description="Minimum PvP damage resist rating"),
min_gear_pk_damage_rating: int = Query(None, description="Minimum gear PvP damage rating"),
min_gear_pk_damage_resist_rating: int = Query(None, description="Minimum gear PvP damage resist rating"),
# Requirements
max_level: int = Query(None, description="Maximum wield level requirement"),
@ -1780,14 +1830,15 @@ async def search_items(
sort_by: str = Query("name", description="Sort field: name, value, damage, armor, workmanship, damage_rating, crit_damage_rating, level"),
sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
page: int = Query(1, ge=1, description="Page number"),
limit: int = Query(200, ge=1, le=2000, description="Items per page")
limit: int = Query(200, ge=1, le=10000, description="Items per page")
):
"""
Search items across characters with comprehensive filtering options.
"""
try:
# Build base query - include raw data for comprehensive translations
# Build base query with CTE for computed slot names
query_parts = ["""
WITH items_with_slots AS (
SELECT DISTINCT
i.id as db_item_id,
i.character_name,
@ -1821,19 +1872,111 @@ async def search_items(
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,
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
COALESCE((rd.int_values->>'316')::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,
rd.original_json
rd.original_json,
-- Compute slot_name in SQL
CASE
-- ARMOR/CLOTHING: Use EquipableSlots_Decal from JSON
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
-- Translate equippable slots to readable names
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'
-- Multi-slot combinations
WHEN 15 THEN 'Chest, Abdomen, Upper Arms, Lower Arms' -- Robes
WHEN 30 THEN 'Shirt' -- Full shirts
WHEN 14336 THEN 'Chest, Abdomen, Upper Arms, Lower Arms' -- Hauberks
WHEN 25600 THEN 'Abdomen, Upper Legs, Lower Legs' -- Tassets
ELSE
-- For other combinations, decode bit flags
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
-- JEWELRY: Check wielded location first, then name patterns
WHEN i.object_class = 4 THEN
CASE
-- Use wielded location if equipped
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'
-- Fallback to name patterns for unequipped
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
-- WEAPONS: Use object class
WHEN i.object_class = 6 THEN 'Melee Weapon'
WHEN i.object_class = 7 THEN 'Missile Weapon'
WHEN i.object_class = 8 THEN 'Held'
-- Check wielded location for two-handed weapons
WHEN i.current_wielded_location = 67108864 THEN 'Two-Handed'
-- DEFAULT
ELSE '-'
END as computed_slot_name
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
)
SELECT * FROM items_with_slots
"""]
conditions = []
@ -1841,7 +1984,7 @@ async def search_items(
# Character filtering
if character:
conditions.append("i.character_name = :character")
conditions.append("character_name = :character")
params["character"] = character
elif characters:
# Handle comma-separated list of characters
@ -1853,7 +1996,7 @@ async def search_items(
param_name = f"char_{i}"
char_params.append(f":{param_name}")
params[param_name] = char_name
conditions.append(f"i.character_name IN ({', '.join(char_params)})")
conditions.append(f"character_name IN ({', '.join(char_params)})")
else:
return {
"error": "Empty characters list provided",
@ -1868,21 +2011,27 @@ async def search_items(
"total_count": 0
}
# Text search (name)
# Text search (name with material support)
if text:
conditions.append("i.name ILIKE :text")
# Search both the concatenated material+name and base name
# This handles searches like "Silver Celdon Girth" or just "silver" for material
conditions.append("""(
CONCAT(COALESCE(material, ''), ' ', name) ILIKE :text OR
name ILIKE :text OR
COALESCE(material, '') ILIKE :text
)""")
params["text"] = f"%{text}%"
# Item category filtering
if armor_only:
# Armor: ObjectClass 2 (Clothing) or 3 (Armor) with armor_level > 0
conditions.append("(i.object_class IN (2, 3) AND COALESCE(cs.armor_level, 0) > 0)")
conditions.append("(object_class IN (2, 3) AND COALESCE(armor_level, 0) > 0)")
elif jewelry_only:
# Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets
conditions.append("i.object_class = 4")
conditions.append("object_class = 4")
elif weapon_only:
# Weapons: ObjectClass 6 (MeleeWeapon), 7 (MissileWeapon), 8 (Caster) with max_damage > 0
conditions.append("(i.object_class IN (6, 7, 8) AND COALESCE(cs.max_damage, 0) > 0)")
conditions.append("(object_class IN (6, 7, 8) AND COALESCE(max_damage, 0) > 0)")
# Spell filtering - need to join with item_spells and use spell database
spell_join_added = False
@ -1953,133 +2102,236 @@ async def search_items(
if matching_spell_ids:
# Remove duplicates
matching_spell_ids = list(set(matching_spell_ids))
spell_conditions.append(f"sp.spell_id IN ({','.join(map(str, matching_spell_ids))})")
logger.info(f"Found {len(matching_spell_ids)} matching spell IDs: {matching_spell_ids}")
# CONSTRAINT SATISFACTION: Items must have ALL selected spells (AND logic)
# Use EXISTS subquery to ensure all required spells are present
num_required_spells = len(cantrip_names) # Number of different cantrips user selected
spell_conditions.append(f"""(
SELECT COUNT(DISTINCT sp2.spell_id)
FROM item_spells sp2
WHERE sp2.item_id = db_item_id
AND sp2.spell_id IN ({','.join(map(str, matching_spell_ids))})
) >= {num_required_spells}""")
logger.info(f"Constraint satisfaction: Items must have ALL {num_required_spells} cantrips from IDs: {matching_spell_ids}")
else:
# If no matching cantrips found, this will return no results
logger.warning("No matching spells found for any cantrips - search will return empty results")
spell_conditions.append("sp.spell_id = -1") # Use impossible condition instead of 1=0
spell_conditions.append("1 = 0") # Use impossible condition
# Combine spell conditions with OR - only add if we have conditions
# Add spell constraints (now using AND logic for constraint satisfaction)
if spell_conditions:
conditions.append(f"({' OR '.join(spell_conditions)})")
conditions.extend(spell_conditions)
# Equipment status
if equipment_status == "equipped":
conditions.append("i.current_wielded_location > 0")
conditions.append("current_wielded_location > 0")
elif equipment_status == "unequipped":
conditions.append("i.current_wielded_location = 0")
conditions.append("current_wielded_location = 0")
# Equipment slot
if equipment_slot is not None:
conditions.append("i.current_wielded_location = :equipment_slot")
conditions.append("current_wielded_location = :equipment_slot")
params["equipment_slot"] = equipment_slot
# Slot names filtering - use multiple approaches for better coverage
if slot_names:
slot_list = [slot.strip() for slot in slot_names.split(',') if slot.strip()]
if slot_list:
slot_conditions = []
for i, slot_name in enumerate(slot_list):
param_name = f"slot_{i}"
# Multiple filtering approaches for better coverage
slot_approaches = []
# Approach 1: Check computed_slot_name
if slot_name.lower() == 'ring':
slot_approaches.append("(computed_slot_name ILIKE '%Ring%')")
elif slot_name.lower() in ['bracelet', 'wrist']:
slot_approaches.append("(computed_slot_name ILIKE '%Wrist%')")
else:
slot_approaches.append(f"(computed_slot_name ILIKE :{param_name})")
params[param_name] = f"%{slot_name}%"
# Approach 2: Specific jewelry logic for common cases
if slot_name.lower() == 'neck':
# For neck: object_class = 4 (jewelry) with amulet/necklace/gorget names
slot_approaches.append("(object_class = 4 AND (name ILIKE '%amulet%' OR name ILIKE '%necklace%' OR name ILIKE '%gorget%'))")
elif slot_name.lower() == 'ring':
# For rings: object_class = 4 with ring in name (excluding keyrings)
slot_approaches.append("(object_class = 4 AND name ILIKE '%ring%' AND name NOT ILIKE '%keyring%' AND name NOT ILIKE '%signet%')")
elif slot_name.lower() in ['bracelet', 'wrist']:
# For bracelets: object_class = 4 with bracelet in name
slot_approaches.append("(object_class = 4 AND name ILIKE '%bracelet%')")
elif slot_name.lower() == 'trinket':
# For trinkets: multiple approaches
# 1. Equipped trinkets have specific wielded_location
slot_approaches.append("(current_wielded_location = 67108864)")
# 2. Jewelry with trinket-related names
slot_approaches.append("(object_class = 4 AND (name ILIKE '%trinket%' OR name ILIKE '%compass%' OR name ILIKE '%goggles%'))")
# 3. Gems with trinket names
slot_approaches.append("(object_class = 11 AND name ILIKE '%trinket%')")
# 4. Jewelry fallback: items that don't match other jewelry patterns
slot_approaches.append("(object_class = 4 AND name NOT ILIKE '%ring%' AND name NOT ILIKE '%bracelet%' AND name NOT ILIKE '%amulet%' AND name NOT ILIKE '%necklace%' AND name NOT ILIKE '%gorget%')")
# Combine approaches with OR (any approach can match)
if slot_approaches:
slot_conditions.append(f"({' OR '.join(slot_approaches)})")
# Use OR logic for slots (items matching ANY selected slot)
if slot_conditions:
conditions.append(f"({' OR '.join(slot_conditions)})")
logger.info(f"Slot filtering: Looking for items with slots: {slot_list}")
# Combat properties
if min_damage is not None:
conditions.append("cs.max_damage >= :min_damage")
conditions.append("max_damage >= :min_damage")
params["min_damage"] = min_damage
if max_damage is not None:
conditions.append("cs.max_damage <= :max_damage")
conditions.append("max_damage <= :max_damage")
params["max_damage"] = max_damage
if min_armor is not None:
conditions.append("cs.armor_level >= :min_armor")
conditions.append("armor_level >= :min_armor")
params["min_armor"] = min_armor
if max_armor is not None:
conditions.append("cs.armor_level <= :max_armor")
conditions.append("armor_level <= :max_armor")
params["max_armor"] = max_armor
if min_attack_bonus is not None:
conditions.append("cs.attack_bonus >= :min_attack_bonus")
conditions.append("attack_bonus >= :min_attack_bonus")
params["min_attack_bonus"] = min_attack_bonus
if min_crit_damage_rating is not None:
# Check both individual rating (314) and gear total (374)
conditions.append("""(
COALESCE((rd.int_values->>'314')::int, 0) >= :min_crit_damage_rating OR
COALESCE((rd.int_values->>'374')::int, 0) >= :min_crit_damage_rating
)""")
conditions.append("crit_damage_rating >= :min_crit_damage_rating")
params["min_crit_damage_rating"] = min_crit_damage_rating
if min_damage_rating is not None:
# Check both individual rating (307) and gear total (370)
conditions.append("""(
COALESCE((rd.int_values->>'307')::int, 0) >= :min_damage_rating OR
COALESCE((rd.int_values->>'370')::int, 0) >= :min_damage_rating
)""")
conditions.append("damage_rating >= :min_damage_rating")
params["min_damage_rating"] = min_damage_rating
if min_heal_boost_rating is not None:
# Check both individual rating (323) and gear total (376)
conditions.append("""(
COALESCE((rd.int_values->>'323')::int, 0) >= :min_heal_boost_rating OR
COALESCE((rd.int_values->>'376')::int, 0) >= :min_heal_boost_rating
)""")
conditions.append("heal_boost_rating >= :min_heal_boost_rating")
params["min_heal_boost_rating"] = min_heal_boost_rating
if min_vitality_rating is not None:
conditions.append("vitality_rating >= :min_vitality_rating")
params["min_vitality_rating"] = min_vitality_rating
if min_damage_resist_rating is not None:
conditions.append("damage_resist_rating >= :min_damage_resist_rating")
params["min_damage_resist_rating"] = min_damage_resist_rating
if min_crit_resist_rating is not None:
conditions.append("crit_resist_rating >= :min_crit_resist_rating")
params["min_crit_resist_rating"] = min_crit_resist_rating
if min_crit_damage_resist_rating is not None:
conditions.append("crit_damage_resist_rating >= :min_crit_damage_resist_rating")
params["min_crit_damage_resist_rating"] = min_crit_damage_resist_rating
if min_healing_resist_rating is not None:
conditions.append("healing_resist_rating >= :min_healing_resist_rating")
params["min_healing_resist_rating"] = min_healing_resist_rating
if min_nether_resist_rating is not None:
conditions.append("nether_resist_rating >= :min_nether_resist_rating")
params["min_nether_resist_rating"] = min_nether_resist_rating
if min_healing_rating is not None:
conditions.append("healing_rating >= :min_healing_rating")
params["min_healing_rating"] = min_healing_rating
if min_dot_resist_rating is not None:
conditions.append("dot_resist_rating >= :min_dot_resist_rating")
params["min_dot_resist_rating"] = min_dot_resist_rating
if min_life_resist_rating is not None:
conditions.append("life_resist_rating >= :min_life_resist_rating")
params["min_life_resist_rating"] = min_life_resist_rating
if min_sneak_attack_rating is not None:
conditions.append("sneak_attack_rating >= :min_sneak_attack_rating")
params["min_sneak_attack_rating"] = min_sneak_attack_rating
if min_recklessness_rating is not None:
conditions.append("recklessness_rating >= :min_recklessness_rating")
params["min_recklessness_rating"] = min_recklessness_rating
if min_deception_rating is not None:
conditions.append("deception_rating >= :min_deception_rating")
params["min_deception_rating"] = min_deception_rating
if min_pk_damage_rating is not None:
conditions.append("pk_damage_rating >= :min_pk_damage_rating")
params["min_pk_damage_rating"] = min_pk_damage_rating
if min_pk_damage_resist_rating is not None:
conditions.append("pk_damage_resist_rating >= :min_pk_damage_resist_rating")
params["min_pk_damage_resist_rating"] = min_pk_damage_resist_rating
if min_gear_pk_damage_rating is not None:
conditions.append("gear_pk_damage_rating >= :min_gear_pk_damage_rating")
params["min_gear_pk_damage_rating"] = min_gear_pk_damage_rating
if min_gear_pk_damage_resist_rating is not None:
conditions.append("gear_pk_damage_resist_rating >= :min_gear_pk_damage_resist_rating")
params["min_gear_pk_damage_resist_rating"] = min_gear_pk_damage_resist_rating
# Requirements
if max_level is not None:
conditions.append("(req.wield_level <= :max_level OR req.wield_level IS NULL)")
conditions.append("(wield_level <= :max_level OR wield_level IS NULL)")
params["max_level"] = max_level
if min_level is not None:
conditions.append("req.wield_level >= :min_level")
conditions.append("wield_level >= :min_level")
params["min_level"] = min_level
# Enhancements
if material:
conditions.append("enh.material ILIKE :material")
conditions.append("material ILIKE :material")
params["material"] = f"%{material}%"
if min_workmanship is not None:
conditions.append("enh.workmanship >= :min_workmanship")
conditions.append("workmanship >= :min_workmanship")
params["min_workmanship"] = min_workmanship
if has_imbue is not None:
if has_imbue:
conditions.append("enh.imbue IS NOT NULL AND enh.imbue != ''")
conditions.append("imbue IS NOT NULL AND imbue != ''")
else:
conditions.append("(enh.imbue IS NULL OR enh.imbue = '')")
conditions.append("(imbue IS NULL OR imbue = '')")
if item_set:
conditions.append("enh.item_set = :item_set")
params["item_set"] = item_set
# Translate set ID to set name for database matching
set_name = translate_equipment_set_id(item_set)
logger.info(f"Translated equipment set ID '{item_set}' to name '{set_name}'")
conditions.append("item_set = :item_set")
params["item_set"] = set_name
elif item_sets:
# Handle comma-separated list of item set IDs
set_list = [set_id.strip() for set_id in item_sets.split(',') if set_id.strip()]
if set_list:
# Create parameterized IN clause
set_params = []
for i, set_id in enumerate(set_list):
param_name = f"set_{i}"
set_params.append(f":{param_name}")
params[param_name] = set_id
conditions.append(f"enh.item_set IN ({', '.join(set_params)})")
# CONSTRAINT SATISFACTION: Multiple equipment sets
if len(set_list) > 1:
# IMPOSSIBLE CONSTRAINT: Item cannot be in multiple sets simultaneously
logger.info(f"Multiple equipment sets selected: {set_list} - This is impossible, returning no results")
conditions.append("1 = 0") # No results for impossible constraint
else:
# Single set selection - normal behavior
set_id = set_list[0]
set_name = translate_equipment_set_id(set_id)
logger.info(f"Translated equipment set ID '{set_id}' to name '{set_name}'")
conditions.append("item_set = :item_set")
params["item_set"] = set_name
else:
# Empty sets list - no results
conditions.append("1 = 0")
if min_tinks is not None:
conditions.append("enh.tinks >= :min_tinks")
conditions.append("tinks >= :min_tinks")
params["min_tinks"] = min_tinks
# Item state
if bonded is not None:
conditions.append("i.bonded > 0" if bonded else "i.bonded = 0")
conditions.append("bonded > 0" if bonded else "bonded = 0")
if attuned is not None:
conditions.append("i.attuned > 0" if attuned else "i.attuned = 0")
conditions.append("attuned > 0" if attuned else "attuned = 0")
if unique is not None:
conditions.append("i.unique = :unique")
conditions.append("unique = :unique")
params["unique"] = unique
if is_rare is not None:
if is_rare:
conditions.append("i.rare_id IS NOT NULL AND i.rare_id > 0")
conditions.append("rare_id IS NOT NULL AND rare_id > 0")
else:
conditions.append("(i.rare_id IS NULL OR i.rare_id <= 0)")
conditions.append("(rare_id IS NULL OR rare_id <= 0)")
if min_condition is not None:
conditions.append("((i.structure * 100.0 / NULLIF(i.max_structure, 0)) >= :min_condition OR i.max_structure IS NULL)")
conditions.append("((structure * 100.0 / NULLIF(max_structure, 0)) >= :min_condition OR max_structure IS NULL)")
params["min_condition"] = min_condition
# Value/utility
if min_value is not None:
conditions.append("i.value >= :min_value")
conditions.append("value >= :min_value")
params["min_value"] = min_value
if max_value is not None:
conditions.append("i.value <= :max_value")
conditions.append("value <= :max_value")
params["max_value"] = max_value
if max_burden is not None:
conditions.append("i.burden <= :max_burden")
conditions.append("burden <= :max_burden")
params["max_burden"] = max_burden
# Build WHERE clause
@ -2088,17 +2340,19 @@ async def search_items(
# Add ORDER BY
sort_mapping = {
"name": "i.name",
"value": "i.value",
"damage": "cs.max_damage",
"armor": "cs.armor_level",
"workmanship": "enh.workmanship",
"level": "req.wield_level",
"name": "name",
"value": "value",
"damage": "max_damage",
"armor": "armor_level",
"workmanship": "workmanship",
"level": "wield_level",
"damage_rating": "damage_rating",
"crit_damage_rating": "crit_damage_rating",
"heal_boost_rating": "heal_boost_rating"
"heal_boost_rating": "heal_boost_rating",
"vitality_rating": "vitality_rating",
"damage_resist_rating": "damage_resist_rating"
}
sort_field = sort_mapping.get(sort_by, "i.name")
sort_field = sort_mapping.get(sort_by, "name")
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
query_parts.append(f"ORDER BY {sort_field} {sort_direction}")
@ -2110,23 +2364,112 @@ async def search_items(
query = "\n".join(query_parts)
rows = await database.fetch_all(query, params)
# Get total count for pagination - build separate count query
count_query = """
SELECT COUNT(DISTINCT i.id)
# Get total count for pagination - use same CTE structure
count_query_parts = ["""
WITH items_with_slots AS (
SELECT DISTINCT
i.id as db_item_id,
i.character_name,
i.name,
i.object_class,
i.value,
i.burden,
i.current_wielded_location,
i.bonded,
i.attuned,
i.unique,
i.structure,
i.max_structure,
i.rare_id,
COALESCE(cs.max_damage, -1) as max_damage,
COALESCE(cs.armor_level, -1) as armor_level,
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,
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
COALESCE((rd.int_values->>'316')::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->>'350')::int, -1) as dot_resist_rating,
COALESCE((rd.int_values->>'351')::int, -1) as life_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,
-- Same computed slot logic for count query
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'
ELSE 'Armor'
END
WHEN i.object_class = 4 THEN
CASE
WHEN i.current_wielded_location = 32768 THEN 'Neck'
WHEN i.current_wielded_location IN (262144, 524288, 786432) THEN 'Ring'
WHEN i.current_wielded_location IN (131072, 1048576, 1179648) THEN 'Bracelet'
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 'Ring'
WHEN i.name ILIKE '%bracelet%' THEN 'Bracelet'
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'
ELSE '-'
END as computed_slot_name
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
"""
"""]
# Add spell join to count query if needed
if spell_join_added:
count_query += "\n LEFT JOIN item_spells sp ON i.id = sp.item_id"
count_query_parts.append("LEFT JOIN item_spells sp ON i.id = sp.item_id")
count_query_parts.append("""
)
SELECT COUNT(DISTINCT db_item_id) FROM items_with_slots
""")
if conditions:
count_query += "\nWHERE " + " AND ".join(conditions)
count_query_parts.append("WHERE " + " AND ".join(conditions))
count_query = "\n".join(count_query_parts)
count_result = await database.fetch_one(count_query, params)
total_count = int(count_result[0]) if count_result else 0

View file

@ -796,7 +796,7 @@ class TelemetrySnapshot(BaseModel):
deaths: int
total_deaths: Optional[int] = None
# Removed from telemetry payload; always enforced to 0 and tracked via rare events
rares_found: int = 0
rares_found: Optional[int] = 0
prismatic_taper_count: int
vt_state: str
# Optional telemetry metrics

View file

@ -88,6 +88,13 @@
</a>
</div>
<!-- Player Dashboard link -->
<div class="player-dashboard-link">
<a href="#" id="playerDashboardBtn" onclick="openPlayerDashboard()">
👥 Player Dashboard
</a>
</div>
<!-- Container for sort and filter controls -->
<div id="sortButtons" class="sort-buttons"></div>

View file

@ -170,6 +170,14 @@
color: #000;
}
.subsection-label {
font-weight: bold;
margin-bottom: 2px;
display: block;
font-size: 9px;
color: #333;
}
.search-actions {
display: flex;
gap: 5px;
@ -325,6 +333,79 @@
.text-right {
text-align: right;
}
/* Column width control */
.narrow-col {
width: 120px;
max-width: 120px;
font-size: 9px;
line-height: 1.1;
}
.medium-col {
width: 150px;
max-width: 150px;
}
.set-col {
width: 100px;
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spells-cell {
font-size: 9px;
line-height: 1.2;
}
.legendary-cantrip {
color: #ffd700;
font-weight: bold;
}
.regular-spell {
color: #88ccff;
}
/* Pagination controls */
.pagination-controls {
padding: 12px 16px;
text-align: center;
background: #f8f8f8;
border-top: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 11px;
}
.pagination-controls button {
padding: 4px 8px;
border: 1px solid #999;
background: #e0e0e0;
font-size: 10px;
cursor: pointer;
border-radius: 3px;
}
.pagination-controls button:hover:not(:disabled) {
background: #d0d0d0;
}
.pagination-controls button:disabled {
background: #f0f0f0;
color: #999;
cursor: not-allowed;
}
.pagination-info {
font-weight: bold;
color: #333;
margin: 0 15px;
}
</style>
</head>
<body>
@ -443,6 +524,18 @@
</div>
</div>
<!-- New Rating Filters -->
<div class="filter-row">
<div class="filter-group">
<label>Vitality:</label>
<input type="number" id="searchMinVitalityRating" placeholder="Min">
</div>
<div class="filter-group">
<label>Dmg Resist:</label>
<input type="number" id="searchMinDamageResistRating" placeholder="Min">
</div>
</div>
<!-- Equipment Sets -->
<div class="filter-section">
<label class="section-label">Set:</label>
@ -545,54 +638,148 @@
</div>
<!-- Legendary Weapon Skills -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_finesse" value="Legendary Finesse Weapons">
<input type="checkbox" id="cantrip_legendary_finesse" value="Legendary Finesse Weapon Aptitude">
<label for="cantrip_legendary_finesse">Finesse</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_heavy" value="Legendary Heavy Weapons">
<input type="checkbox" id="cantrip_legendary_heavy" value="Legendary Heavy Weapon Aptitude">
<label for="cantrip_legendary_heavy">Heavy</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_light" value="Legendary Light Weapons">
<input type="checkbox" id="cantrip_legendary_light" value="Legendary Light Weapon Aptitude">
<label for="cantrip_legendary_light">Light</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_missile" value="Legendary Missile Weapons">
<input type="checkbox" id="cantrip_legendary_missile" value="Legendary Missile Weapon Aptitude">
<label for="cantrip_legendary_missile">Missile</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_twohanded" value="Legendary Two Handed Combat">
<input type="checkbox" id="cantrip_legendary_twohanded" value="Legendary Two Handed Combat Aptitude">
<label for="cantrip_legendary_twohanded">Two-handed</label>
</div>
<!-- Legendary Magic Skills -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_war" value="Legendary War Magic">
<input type="checkbox" id="cantrip_legendary_war" value="Legendary War Magic Aptitude">
<label for="cantrip_legendary_war">War Magic</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_void" value="Legendary Void Magic">
<input type="checkbox" id="cantrip_legendary_void" value="Legendary Void Magic Aptitude">
<label for="cantrip_legendary_void">Void Magic</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_creature" value="Legendary Creature Enchantment">
<input type="checkbox" id="cantrip_legendary_creature" value="Legendary Creature Enchantment Aptitude">
<label for="cantrip_legendary_creature">Creature</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_item" value="Legendary Item Enchantment">
<input type="checkbox" id="cantrip_legendary_item" value="Legendary Item Enchantment Aptitude">
<label for="cantrip_legendary_item">Item</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_life" value="Legendary Life Magic">
<input type="checkbox" id="cantrip_legendary_life" value="Legendary Life Magic Aptitude">
<label for="cantrip_legendary_life">Life Magic</label>
</div>
<!-- Legendary Defense -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_magic_defense" value="Legendary Magic Defense">
<input type="checkbox" id="cantrip_legendary_magic_defense" value="Legendary Magic Resistance">
<label for="cantrip_legendary_magic_defense">Magic Def</label>
</div>
<!-- Combat Skills -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_melee_defense" value="Legendary Melee Defense">
<label for="cantrip_legendary_melee_defense">Melee Def</label>
<input type="checkbox" id="cantrip_legendary_blood_thirst" value="Legendary Blood Thirst">
<label for="cantrip_legendary_blood_thirst">Blood Thirst</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_sneak_attack" value="Legendary Sneak Attack Prowess">
<label for="cantrip_legendary_sneak_attack">Sneak Attack</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_defender" value="Legendary Defender">
<label for="cantrip_legendary_defender">Defender</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_recklessness" value="Legendary Recklessness Prowess">
<label for="cantrip_legendary_recklessness">Recklessness</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_shield" value="Legendary Shield Aptitude">
<label for="cantrip_legendary_shield">Shield</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_dual_wield" value="Legendary Dual Wield Aptitude">
<label for="cantrip_legendary_dual_wield">Dual Wield</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_dirty_fighting" value="Legendary Dirty Fighting Prowess">
<label for="cantrip_legendary_dirty_fighting">Dirty Fighting</label>
</div>
<!-- Magic/Utility Skills -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_summoning" value="Legendary Summoning Prowess">
<label for="cantrip_legendary_summoning">Summoning</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_healing" value="Legendary Healing Prowess">
<label for="cantrip_legendary_healing">Healing</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_arcane" value="Legendary Arcane Prowess">
<label for="cantrip_legendary_arcane">Arcane</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_tinkering" value="Legendary Magic Item Tinkering Expertise">
<label for="cantrip_legendary_tinkering">Tinkering</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_mana_conversion" value="Legendary Mana Conversion Prowess">
<label for="cantrip_legendary_mana_conversion">Mana Convert</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_leadership" value="Legendary Leadership">
<label for="cantrip_legendary_leadership">Leadership</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_deception" value="Legendary Deception Prowess">
<label for="cantrip_legendary_deception">Deception</label>
</div>
<!-- Defensive Spells -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_impenetrability" value="Legendary Impenetrability">
<label for="cantrip_legendary_impenetrability">Impenetrability</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_impregnability" value="Legendary Impregnability">
<label for="cantrip_legendary_impregnability">Impregnability</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_invulnerability" value="Legendary Invulnerability">
<label for="cantrip_legendary_invulnerability">Invulnerability</label>
</div>
<!-- Specialized/Rare -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_hermetic_link" value="Legendary Hermetic Link">
<label for="cantrip_legendary_hermetic_link">Hermetic Link</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_person_attunement" value="Legendary Person Attunement">
<label for="cantrip_legendary_person_attunement">Person Attune</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_spirit_thirst" value="Legendary Spirit Thirst">
<label for="cantrip_legendary_spirit_thirst">Spirit Thirst</label>
</div>
<!-- Bane Spells -->
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_piercing_bane" value="Legendary Piercing Bane">
<label for="cantrip_legendary_piercing_bane">Piercing Bane</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_storm_bane" value="Legendary Storm Bane">
<label for="cantrip_legendary_storm_bane">Storm Bane</label>
</div>
</div>
</div>
@ -636,6 +823,77 @@
</div>
</div>
<!-- Equipment Slots -->
<div class="filter-section">
<label class="section-label">Equipment Slots:</label>
<!-- Armor Slots -->
<div class="checkbox-container" id="armor-slots">
<label class="subsection-label">Armor:</label>
<div class="checkbox-item">
<input type="checkbox" id="slot_head" value="Head">
<label for="slot_head">Head</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_chest" value="Chest">
<label for="slot_chest">Chest</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_abdomen" value="Abdomen">
<label for="slot_abdomen">Abdomen</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_upper_arms" value="Upper Arms">
<label for="slot_upper_arms">Upper Arms</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_lower_arms" value="Lower Arms">
<label for="slot_lower_arms">Lower Arms</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_hands" value="Hands">
<label for="slot_hands">Hands</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_upper_legs" value="Upper Legs">
<label for="slot_upper_legs">Upper Legs</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_lower_legs" value="Lower Legs">
<label for="slot_lower_legs">Lower Legs</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_feet" value="Feet">
<label for="slot_feet">Feet</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_shield" value="Shield">
<label for="slot_shield">Shield</label>
</div>
</div>
<!-- Jewelry Slots -->
<div class="checkbox-container" id="jewelry-slots">
<label class="subsection-label">Jewelry:</label>
<div class="checkbox-item">
<input type="checkbox" id="slot_neck" value="Neck">
<label for="slot_neck">Neck</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_ring" value="Ring">
<label for="slot_ring">Ring</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_bracelet" value="Bracelet">
<label for="slot_bracelet">Bracelet</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="slot_trinket" value="Trinket">
<label for="slot_trinket">Trinket</label>
</div>
</div>
</div>
<div class="search-actions">
<button type="button" class="btn btn-secondary" id="clearBtn">Clear All</button>
<button type="submit" class="btn btn-primary">Search Items</button>

View file

@ -20,6 +20,11 @@ let currentSort = {
// Store current search results for client-side sorting
let currentResultsData = null;
// Pagination state
let currentPage = 1;
let itemsPerPage = 5000; // 5k items per page for good performance
let totalPages = 1;
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
// Get DOM elements after DOM is loaded
@ -39,7 +44,7 @@ function initializeEventListeners() {
// Form submission
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
await performSearch();
await performSearch(true); // Reset to page 1 on new search
});
// Clear button
@ -167,6 +172,10 @@ function clearAllFields() {
// Reset slot filter
document.getElementById('slotFilter').value = '';
// Reset pagination
currentPage = 1;
totalPages = 1;
// Reset results and clear stored data
currentResultsData = null;
searchResults.innerHTML = '<div class="no-results">Enter search criteria above and click "Search Items" to find inventory items.</div>';
@ -196,7 +205,12 @@ function handleSlotFilterChange() {
/**
* Perform the search based on form inputs
*/
async function performSearch() {
async function performSearch(resetPage = false) {
// Reset to page 1 if this is a new search (not pagination)
if (resetPage) {
currentPage = 1;
}
searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
try {
@ -214,11 +228,13 @@ async function performSearch() {
// Store results for client-side re-sorting
currentResultsData = data;
// Update pagination state
updatePaginationState(data);
// Apply client-side slot filtering
applySlotFilter(data);
// Sort the results client-side before displaying
sortResults(data);
// Display results (already sorted by server)
displayResults(data);
} catch (error) {
@ -280,6 +296,8 @@ function buildSearchParameters() {
addParam(params, 'max_damage_rating', 'searchMaxDamageRating');
addParam(params, 'min_heal_boost_rating', 'searchMinHealBoost');
addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost');
addParam(params, 'min_vitality_rating', 'searchMinVitalityRating');
addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating');
// Requirements parameters
addParam(params, 'min_level', 'searchMinLevel');
@ -308,8 +326,19 @@ function buildSearchParameters() {
params.append('legendary_cantrips', allSpells.join(','));
}
// Pagination only - sorting will be done client-side
params.append('limit', '1000'); // Show all items on one page
// Equipment slot filters
const selectedSlots = getSelectedSlots();
if (selectedSlots.length > 0) {
params.append('slot_names', selectedSlots.join(','));
}
// Pagination parameters
params.append('page', currentPage);
params.append('limit', itemsPerPage);
// Sorting parameters
params.append('sort_by', currentSort.field);
params.append('sort_dir', currentSort.direction);
return params;
}
@ -357,6 +386,22 @@ function getSelectedProtections() {
return selectedProtections;
}
/**
* Get selected equipment slots from checkboxes
*/
function getSelectedSlots() {
const selectedSlots = [];
// Get armor slots
document.querySelectorAll('#armor-slots input[type="checkbox"]:checked').forEach(cb => {
selectedSlots.push(cb.value);
});
// Get jewelry slots
document.querySelectorAll('#jewelry-slots input[type="checkbox"]:checked').forEach(cb => {
selectedSlots.push(cb.value);
});
return selectedSlots;
}
/**
* Display search results in the UI
*/
@ -383,12 +428,16 @@ function displayResults(data) {
<th class="sortable" data-sort="character_name">Character${getSortIcon('character_name')}</th>
<th class="sortable" data-sort="name">Item Name${getSortIcon('name')}</th>
<th class="sortable" data-sort="item_type_name">Type${getSortIcon('item_type_name')}</th>
<th class="text-right sortable" data-sort="slot_name">Slot${getSortIcon('slot_name')}</th>
<th class="text-right sortable" data-sort="coverage">Coverage${getSortIcon('coverage')}</th>
<th class="text-right sortable" data-sort="armor_level">Armor${getSortIcon('armor_level')}</th>
<th class="sortable" data-sort="spell_names">Spells/Cantrips${getSortIcon('spell_names')}</th>
<th class="text-right narrow-col sortable" data-sort="slot_name">Slot${getSortIcon('slot_name')}</th>
<th class="text-right narrow-col sortable" data-sort="coverage">Coverage${getSortIcon('coverage')}</th>
<th class="text-right sortable" data-sort="armor">Armor${getSortIcon('armor')}</th>
<th class="set-col sortable" data-sort="item_set">Set${getSortIcon('item_set')}</th>
<th class="medium-col sortable" data-sort="spell_names">Spells/Cantrips${getSortIcon('spell_names')}</th>
<th class="text-right sortable" data-sort="crit_damage_rating">Crit Dmg${getSortIcon('crit_damage_rating')}</th>
<th class="text-right sortable" data-sort="damage_rating">Dmg Rating${getSortIcon('damage_rating')}</th>
<th class="text-right sortable" data-sort="heal_boost_rating">Heal Boost${getSortIcon('heal_boost_rating')}</th>
<th class="text-right sortable" data-sort="vitality_rating">Vitality${getSortIcon('vitality_rating')}</th>
<th class="text-right sortable" data-sort="damage_resist_rating">Dmg Resist${getSortIcon('damage_resist_rating')}</th>
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
</tr>
</thead>
@ -399,14 +448,19 @@ function displayResults(data) {
const armor = item.armor_level > 0 ? item.armor_level : '-';
const critDmg = item.crit_damage_rating > 0 ? item.crit_damage_rating : '-';
const dmgRating = item.damage_rating > 0 ? item.damage_rating : '-';
const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-';
const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-';
const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-';
const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
// Use the slot_name provided by the API instead of incorrect mapping
const slot = item.slot_name || 'Unknown';
// Replace commas with line breaks for better display
const slot = item.slot_name ? item.slot_name.replace(/,\s*/g, '<br>') : 'Unknown';
// Coverage placeholder - will need to be added to backend later
const coverage = item.coverage || '-';
// Replace commas with line breaks for better display
const coverage = item.coverage ? item.coverage.replace(/,\s*/g, '<br>') : '-';
// Format last updated timestamp
const lastUpdated = item.last_updated ?
@ -444,17 +498,30 @@ function displayResults(data) {
// Get item type for display
const itemType = item.item_type_name || '-';
// Format equipment set name
let setDisplay = '-';
if (item.item_set) {
// Remove redundant "Set" prefix if present
setDisplay = item.item_set.replace(/^Set\s+/i, '');
// Also handle if it ends with " Set"
setDisplay = setDisplay.replace(/\s+Set$/i, '');
}
html += `
<tr>
<td>${item.character_name}</td>
<td class="item-name">${displayName}</td>
<td>${itemType}</td>
<td class="text-right">${slot}</td>
<td class="text-right">${coverage}</td>
<td class="text-right narrow-col">${slot}</td>
<td class="text-right narrow-col">${coverage}</td>
<td class="text-right">${armor}</td>
<td class="spells-cell">${spellsDisplay}</td>
<td class="set-col" title="${setDisplay}">${setDisplay}</td>
<td class="spells-cell medium-col">${spellsDisplay}</td>
<td class="text-right">${critDmg}</td>
<td class="text-right">${dmgRating}</td>
<td class="text-right">${healBoostRating}</td>
<td class="text-right">${vitalityRating}</td>
<td class="text-right">${damageResistRating}</td>
<td class="text-right">${lastUpdated}</td>
</tr>
`;
@ -465,11 +532,18 @@ function displayResults(data) {
</table>
`;
// Add pagination info if needed
if (data.total_pages > 1) {
// Add pagination controls if needed
if (totalPages > 1) {
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
html += `
<div style="padding: 16px 24px; text-align: center; color: #5f6368; border-top: 1px solid #e8eaed;">
Showing page ${data.page} of ${data.total_pages} pages
<div class="pagination-controls">
<button onclick="goToPage(1)" ${isFirstPage ? 'disabled' : ''}>First</button>
<button onclick="previousPage()" ${isFirstPage ? 'disabled' : ''}> Previous</button>
<span class="pagination-info">Page ${currentPage} of ${totalPages} (${data.total_count} total items)</span>
<button onclick="nextPage()" ${isLastPage ? 'disabled' : ''}>Next </button>
<button onclick="goToPage(${totalPages})" ${isLastPage ? 'disabled' : ''}>Last</button>
</div>
`;
}
@ -566,20 +640,9 @@ function handleSort(field) {
currentSort.direction = 'asc';
}
// Re-display current results with new sorting (no new search needed)
if (currentResultsData) {
// Reset items to original unfiltered data
const originalData = JSON.parse(JSON.stringify(currentResultsData));
// Apply slot filtering first
applySlotFilter(originalData);
// Then apply sorting
sortResults(originalData);
// Display results
displayResults(originalData);
}
// Reset to page 1 and perform new search with updated sort
currentPage = 1;
performSearch();
}
/**
@ -722,3 +785,41 @@ function displaySetAnalysisResults(data) {
setAnalysisResults.innerHTML = html;
}
/**
* Update pagination state from API response
*/
function updatePaginationState(data) {
totalPages = data.total_pages || 1;
// Current page is already tracked in currentPage
}
/**
* Go to a specific page
*/
function goToPage(page) {
if (page < 1 || page > totalPages || page === currentPage) {
return;
}
currentPage = page;
performSearch();
}
/**
* Go to next page
*/
function nextPage() {
if (currentPage < totalPages) {
goToPage(currentPage + 1);
}
}
/**
* Go to previous page
*/
function previousPage() {
if (currentPage > 1) {
goToPage(currentPage - 1);
}
}

View file

@ -1989,4 +1989,12 @@ function openQuestStatus() {
window.open('/quest-status.html', '_blank');
}
/**
* Opens the Player Dashboard interface in a new browser tab.
*/
function openPlayerDashboard() {
// Open the Player Dashboard page in a new tab
window.open('/player-dashboard.html', '_blank');
}

View file

@ -1460,6 +1460,34 @@ body.noselect, body.noselect * {
margin: -2px -4px;
}
.player-dashboard-link {
margin: 0 0 12px;
padding: 8px 12px;
background: var(--card);
border: 1px solid #88f;
border-radius: 4px;
text-align: center;
}
.player-dashboard-link a {
color: #88f;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
display: block;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.player-dashboard-link a:hover {
color: #fff;
background: rgba(136, 136, 255, 0.1);
border-radius: 2px;
padding: 2px 4px;
margin: -2px -4px;
}
/* Sortable column styles for inventory tables */
.sortable {
cursor: pointer;