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 # Post to Discord
await self.post_rare_to_discord(data, rare_type) 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}") logger.info(f"📬 Posted to Discord: {rare_name} ({rare_type}) from {character_name}")
except Exception as e: except Exception as e:
@ -508,20 +505,24 @@ class DiscordRareMonitor:
logger.error(f"❌ Could not find channel {ACLOG_CHANNEL_ID}") logger.error(f"❌ Could not find channel {ACLOG_CHANNEL_ID}")
except Exception as e: except Exception as e:
logger.error(f"💥 VORTEX WARNING POSTING FAILED: {e}", exc_info=True) 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 return
elif "whirlwind of vortex" in chat_text.lower(): elif "whirlwind of vortex" in chat_text.lower():
logger.debug(f"🌪️ Found 'whirlwind of vortex' but not exact match in: {repr(chat_text)}") logger.debug(f"🌪️ Found 'whirlwind of vortex' but not exact match in: {repr(chat_text)}")
elif "jeebus" in chat_text.lower(): elif "jeebus" in chat_text.lower():
logger.debug(f"👀 Found 'jeebus' but not vortex pattern in: {repr(chat_text)}") logger.debug(f"👀 Found 'jeebus' but not vortex pattern in: {repr(chat_text)}")
# Skip if this message contains any rare names (common or great) # Log all chat messages (no longer filtering rares)
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
logger.info(f"💬 Chat from {character_name}: {chat_text}") logger.info(f"💬 Chat from {character_name}: {chat_text}")
# Post to AC Log channel # Post to AC Log channel
await self.post_chat_to_discord(data) await self.post_chat_to_discord(data)
@ -660,6 +661,21 @@ class DiscordRareMonitor:
except Exception as e: except Exception as e:
logger.error(f"❌ Error posting to Discord: {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): async def post_chat_to_discord(self, data: dict):
"""Post chat message to sawatolife Discord channel.""" """Post chat message to sawatolife Discord channel."""
try: try:
@ -679,9 +695,11 @@ class DiscordRareMonitor:
except ValueError: except ValueError:
timestamp = datetime.now() timestamp = datetime.now()
# Create simple message format similar to your old bot # Clean and format the message
time_str = timestamp.strftime("%H:%M:%S") 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 # Get sawatolife channel
channel = self.client.get_channel(SAWATOLIFE_CHANNEL_ID) channel = self.client.get_channel(SAWATOLIFE_CHANNEL_ID)
@ -695,23 +713,6 @@ class DiscordRareMonitor:
except Exception as e: except Exception as e:
logger.error(f"❌ Error posting chat to Discord: {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): async def post_vortex_warning_to_discord(self, data: dict):
"""Post vortex warning as a special alert message to Discord.""" """Post vortex warning as a special alert message to Discord."""

View file

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

View file

@ -190,10 +190,28 @@ class ItemRatings(Base):
# Utility ratings # Utility ratings
heal_boost_rating = Column(Integer) heal_boost_rating = Column(Integer)
vitality_rating = Column(Integer) vitality_rating = Column(Integer)
healing_rating = Column(Integer) # Healing rating (enum 342)
mana_conversion_rating = Column(Integer) mana_conversion_rating = Column(Integer)
weakness_rating = Column(Integer) # Weakness debuff strength weakness_rating = Column(Integer) # Weakness debuff strength
nether_over_time = Column(Integer) # Nether DoT 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 totals
gear_damage = Column(Integer) # Total gear damage gear_damage = Column(Integer) # Total gear damage
gear_damage_resist = Column(Integer) # Total gear damage resist 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 # Fallback to "Misc" if ObjectClass not found in enum
return "Misc" 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: 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.""" """Translate object class ID to human-readable name with context-aware detection."""
# Use the extracted ObjectClass enum first # 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 '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 '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 # Additional ratings
'weakness_rating': int_values.get('329', int_values.get(329, -1)), 'weakness_rating': int_values.get('329', int_values.get(329, -1)),
'nether_over_time': int_values.get('330', int_values.get(330, -1)), 'nether_over_time': int_values.get('330', int_values.get(330, -1)),
@ -1731,6 +1764,7 @@ async def search_items(
# Equipment filtering # Equipment filtering
equipment_status: str = Query(None, description="equipped, unequipped, or all"), 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)"), 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 # Item category filtering
armor_only: bool = Query(False, description="Show only armor items"), 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_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
min_damage_rating: int = Query(None, description="Minimum 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_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 # Requirements
max_level: int = Query(None, description="Maximum wield level requirement"), 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_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"), sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
page: int = Query(1, ge=1, description="Page number"), 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. Search items across characters with comprehensive filtering options.
""" """
try: try:
# Build base query - include raw data for comprehensive translations # Build base query with CTE for computed slot names
query_parts = [""" query_parts = ["""
WITH items_with_slots AS (
SELECT DISTINCT SELECT DISTINCT
i.id as db_item_id, i.id as db_item_id,
i.character_name, i.character_name,
@ -1821,19 +1872,111 @@ async def search_items(
COALESCE((rd.int_values->>'323')::int, -1), COALESCE((rd.int_values->>'323')::int, -1),
COALESCE((rd.int_values->>'376')::int, -1) COALESCE((rd.int_values->>'376')::int, -1)
) as heal_boost_rating, ) 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(req.wield_level, -1) as wield_level,
COALESCE(enh.material, '') as material, COALESCE(enh.material, '') as material,
COALESCE(enh.workmanship, -1.0) as workmanship, COALESCE(enh.workmanship, -1.0) as workmanship,
COALESCE(enh.imbue, '') as imbue, COALESCE(enh.imbue, '') as imbue,
COALESCE(enh.tinks, -1) as tinks, COALESCE(enh.tinks, -1) as tinks,
COALESCE(enh.item_set, '') as item_set, 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 FROM items i
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id 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_requirements req ON i.id = req.item_id
LEFT JOIN item_enhancements enh ON i.id = enh.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_ratings rt ON i.id = rt.item_id
LEFT JOIN item_raw_data rd ON i.id = rd.item_id LEFT JOIN item_raw_data rd ON i.id = rd.item_id
)
SELECT * FROM items_with_slots
"""] """]
conditions = [] conditions = []
@ -1841,7 +1984,7 @@ async def search_items(
# Character filtering # Character filtering
if character: if character:
conditions.append("i.character_name = :character") conditions.append("character_name = :character")
params["character"] = character params["character"] = character
elif characters: elif characters:
# Handle comma-separated list of characters # Handle comma-separated list of characters
@ -1853,7 +1996,7 @@ async def search_items(
param_name = f"char_{i}" param_name = f"char_{i}"
char_params.append(f":{param_name}") char_params.append(f":{param_name}")
params[param_name] = char_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: else:
return { return {
"error": "Empty characters list provided", "error": "Empty characters list provided",
@ -1868,21 +2011,27 @@ async def search_items(
"total_count": 0 "total_count": 0
} }
# Text search (name) # Text search (name with material support)
if text: 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}%" params["text"] = f"%{text}%"
# Item category filtering # Item category filtering
if armor_only: if armor_only:
# Armor: ObjectClass 2 (Clothing) or 3 (Armor) with armor_level > 0 # 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: elif jewelry_only:
# Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets # Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets
conditions.append("i.object_class = 4") conditions.append("object_class = 4")
elif weapon_only: elif weapon_only:
# Weapons: ObjectClass 6 (MeleeWeapon), 7 (MissileWeapon), 8 (Caster) with max_damage > 0 # 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 filtering - need to join with item_spells and use spell database
spell_join_added = False spell_join_added = False
@ -1953,133 +2102,236 @@ async def search_items(
if matching_spell_ids: if matching_spell_ids:
# Remove duplicates # Remove duplicates
matching_spell_ids = list(set(matching_spell_ids)) 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: else:
# If no matching cantrips found, this will return no results # If no matching cantrips found, this will return no results
logger.warning("No matching spells found for any cantrips - search will return empty 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: if spell_conditions:
conditions.append(f"({' OR '.join(spell_conditions)})") conditions.extend(spell_conditions)
# Equipment status # Equipment status
if equipment_status == "equipped": if equipment_status == "equipped":
conditions.append("i.current_wielded_location > 0") conditions.append("current_wielded_location > 0")
elif equipment_status == "unequipped": elif equipment_status == "unequipped":
conditions.append("i.current_wielded_location = 0") conditions.append("current_wielded_location = 0")
# Equipment slot # Equipment slot
if equipment_slot is not None: 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 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 # Combat properties
if min_damage is not None: if min_damage is not None:
conditions.append("cs.max_damage >= :min_damage") conditions.append("max_damage >= :min_damage")
params["min_damage"] = min_damage params["min_damage"] = min_damage
if max_damage is not None: if max_damage is not None:
conditions.append("cs.max_damage <= :max_damage") conditions.append("max_damage <= :max_damage")
params["max_damage"] = max_damage params["max_damage"] = max_damage
if min_armor is not None: if min_armor is not None:
conditions.append("cs.armor_level >= :min_armor") conditions.append("armor_level >= :min_armor")
params["min_armor"] = min_armor params["min_armor"] = min_armor
if max_armor is not None: if max_armor is not None:
conditions.append("cs.armor_level <= :max_armor") conditions.append("armor_level <= :max_armor")
params["max_armor"] = max_armor params["max_armor"] = max_armor
if min_attack_bonus is not None: 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 params["min_attack_bonus"] = min_attack_bonus
if min_crit_damage_rating is not None: if min_crit_damage_rating is not None:
# Check both individual rating (314) and gear total (374) conditions.append("crit_damage_rating >= :min_crit_damage_rating")
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
)""")
params["min_crit_damage_rating"] = min_crit_damage_rating params["min_crit_damage_rating"] = min_crit_damage_rating
if min_damage_rating is not None: if min_damage_rating is not None:
# Check both individual rating (307) and gear total (370) conditions.append("damage_rating >= :min_damage_rating")
conditions.append("""(
COALESCE((rd.int_values->>'307')::int, 0) >= :min_damage_rating OR
COALESCE((rd.int_values->>'370')::int, 0) >= :min_damage_rating
)""")
params["min_damage_rating"] = min_damage_rating params["min_damage_rating"] = min_damage_rating
if min_heal_boost_rating is not None: if min_heal_boost_rating is not None:
# Check both individual rating (323) and gear total (376) conditions.append("heal_boost_rating >= :min_heal_boost_rating")
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
)""")
params["min_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 # Requirements
if max_level is not None: 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 params["max_level"] = max_level
if min_level is not None: if min_level is not None:
conditions.append("req.wield_level >= :min_level") conditions.append("wield_level >= :min_level")
params["min_level"] = min_level params["min_level"] = min_level
# Enhancements # Enhancements
if material: if material:
conditions.append("enh.material ILIKE :material") conditions.append("material ILIKE :material")
params["material"] = f"%{material}%" params["material"] = f"%{material}%"
if min_workmanship is not None: if min_workmanship is not None:
conditions.append("enh.workmanship >= :min_workmanship") conditions.append("workmanship >= :min_workmanship")
params["min_workmanship"] = min_workmanship params["min_workmanship"] = min_workmanship
if has_imbue is not None: if has_imbue is not None:
if has_imbue: if has_imbue:
conditions.append("enh.imbue IS NOT NULL AND enh.imbue != ''") conditions.append("imbue IS NOT NULL AND imbue != ''")
else: else:
conditions.append("(enh.imbue IS NULL OR enh.imbue = '')") conditions.append("(imbue IS NULL OR imbue = '')")
if item_set: if item_set:
conditions.append("enh.item_set = :item_set") # Translate set ID to set name for database matching
params["item_set"] = item_set 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: elif item_sets:
# Handle comma-separated list of item set IDs # Handle comma-separated list of item set IDs
set_list = [set_id.strip() for set_id in item_sets.split(',') if set_id.strip()] set_list = [set_id.strip() for set_id in item_sets.split(',') if set_id.strip()]
if set_list: if set_list:
# Create parameterized IN clause # CONSTRAINT SATISFACTION: Multiple equipment sets
set_params = [] if len(set_list) > 1:
for i, set_id in enumerate(set_list): # IMPOSSIBLE CONSTRAINT: Item cannot be in multiple sets simultaneously
param_name = f"set_{i}" logger.info(f"Multiple equipment sets selected: {set_list} - This is impossible, returning no results")
set_params.append(f":{param_name}") conditions.append("1 = 0") # No results for impossible constraint
params[param_name] = set_id else:
conditions.append(f"enh.item_set IN ({', '.join(set_params)})") # 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: else:
# Empty sets list - no results # Empty sets list - no results
conditions.append("1 = 0") conditions.append("1 = 0")
if min_tinks is not None: if min_tinks is not None:
conditions.append("enh.tinks >= :min_tinks") conditions.append("tinks >= :min_tinks")
params["min_tinks"] = min_tinks params["min_tinks"] = min_tinks
# Item state # Item state
if bonded is not None: 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: 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: if unique is not None:
conditions.append("i.unique = :unique") conditions.append("unique = :unique")
params["unique"] = unique params["unique"] = unique
if is_rare is not None: if is_rare is not None:
if is_rare: 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: 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: 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 params["min_condition"] = min_condition
# Value/utility # Value/utility
if min_value is not None: if min_value is not None:
conditions.append("i.value >= :min_value") conditions.append("value >= :min_value")
params["min_value"] = min_value params["min_value"] = min_value
if max_value is not None: if max_value is not None:
conditions.append("i.value <= :max_value") conditions.append("value <= :max_value")
params["max_value"] = max_value params["max_value"] = max_value
if max_burden is not None: if max_burden is not None:
conditions.append("i.burden <= :max_burden") conditions.append("burden <= :max_burden")
params["max_burden"] = max_burden params["max_burden"] = max_burden
# Build WHERE clause # Build WHERE clause
@ -2088,17 +2340,19 @@ async def search_items(
# Add ORDER BY # Add ORDER BY
sort_mapping = { sort_mapping = {
"name": "i.name", "name": "name",
"value": "i.value", "value": "value",
"damage": "cs.max_damage", "damage": "max_damage",
"armor": "cs.armor_level", "armor": "armor_level",
"workmanship": "enh.workmanship", "workmanship": "workmanship",
"level": "req.wield_level", "level": "wield_level",
"damage_rating": "damage_rating", "damage_rating": "damage_rating",
"crit_damage_rating": "crit_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" sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
query_parts.append(f"ORDER BY {sort_field} {sort_direction}") query_parts.append(f"ORDER BY {sort_field} {sort_direction}")
@ -2110,23 +2364,112 @@ async def search_items(
query = "\n".join(query_parts) query = "\n".join(query_parts)
rows = await database.fetch_all(query, params) rows = await database.fetch_all(query, params)
# Get total count for pagination - build separate count query # Get total count for pagination - use same CTE structure
count_query = """ count_query_parts = ["""
SELECT COUNT(DISTINCT i.id) 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 FROM items i
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id 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_requirements req ON i.id = req.item_id
LEFT JOIN item_enhancements enh ON i.id = enh.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_ratings rt ON i.id = rt.item_id
LEFT JOIN item_raw_data rd ON i.id = rd.item_id LEFT JOIN item_raw_data rd ON i.id = rd.item_id
""" """]
# Add spell join to count query if needed # Add spell join to count query if needed
if spell_join_added: 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: 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) count_result = await database.fetch_one(count_query, params)
total_count = int(count_result[0]) if count_result else 0 total_count = int(count_result[0]) if count_result else 0

View file

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

View file

@ -88,6 +88,13 @@
</a> </a>
</div> </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 --> <!-- Container for sort and filter controls -->
<div id="sortButtons" class="sort-buttons"></div> <div id="sortButtons" class="sort-buttons"></div>

View file

@ -170,6 +170,14 @@
color: #000; color: #000;
} }
.subsection-label {
font-weight: bold;
margin-bottom: 2px;
display: block;
font-size: 9px;
color: #333;
}
.search-actions { .search-actions {
display: flex; display: flex;
gap: 5px; gap: 5px;
@ -325,6 +333,79 @@
.text-right { .text-right {
text-align: 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> </style>
</head> </head>
<body> <body>
@ -443,6 +524,18 @@
</div> </div>
</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 --> <!-- Equipment Sets -->
<div class="filter-section"> <div class="filter-section">
<label class="section-label">Set:</label> <label class="section-label">Set:</label>
@ -545,54 +638,148 @@
</div> </div>
<!-- Legendary Weapon Skills --> <!-- Legendary Weapon Skills -->
<div class="checkbox-item"> <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> <label for="cantrip_legendary_finesse">Finesse</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_heavy">Heavy</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_light">Light</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_missile">Missile</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_twohanded">Two-handed</label>
</div> </div>
<!-- Legendary Magic Skills --> <!-- Legendary Magic Skills -->
<div class="checkbox-item"> <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> <label for="cantrip_legendary_war">War Magic</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_void">Void Magic</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_creature">Creature</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_item">Item</label>
</div> </div>
<div class="checkbox-item"> <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> <label for="cantrip_legendary_life">Life Magic</label>
</div> </div>
<!-- Legendary Defense --> <!-- Legendary Defense -->
<div class="checkbox-item"> <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> <label for="cantrip_legendary_magic_defense">Magic Def</label>
</div> </div>
<!-- Combat Skills -->
<div class="checkbox-item"> <div class="checkbox-item">
<input type="checkbox" id="cantrip_legendary_melee_defense" value="Legendary Melee Defense"> <input type="checkbox" id="cantrip_legendary_blood_thirst" value="Legendary Blood Thirst">
<label for="cantrip_legendary_melee_defense">Melee Def</label> <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> </div>
</div> </div>
@ -636,6 +823,77 @@
</div> </div>
</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"> <div class="search-actions">
<button type="button" class="btn btn-secondary" id="clearBtn">Clear All</button> <button type="button" class="btn btn-secondary" id="clearBtn">Clear All</button>
<button type="submit" class="btn btn-primary">Search Items</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 // Store current search results for client-side sorting
let currentResultsData = null; let currentResultsData = null;
// Pagination state
let currentPage = 1;
let itemsPerPage = 5000; // 5k items per page for good performance
let totalPages = 1;
// Initialize the application // Initialize the application
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Get DOM elements after DOM is loaded // Get DOM elements after DOM is loaded
@ -39,7 +44,7 @@ function initializeEventListeners() {
// Form submission // Form submission
searchForm.addEventListener('submit', async (e) => { searchForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
await performSearch(); await performSearch(true); // Reset to page 1 on new search
}); });
// Clear button // Clear button
@ -167,6 +172,10 @@ function clearAllFields() {
// Reset slot filter // Reset slot filter
document.getElementById('slotFilter').value = ''; document.getElementById('slotFilter').value = '';
// Reset pagination
currentPage = 1;
totalPages = 1;
// Reset results and clear stored data // Reset results and clear stored data
currentResultsData = null; currentResultsData = null;
searchResults.innerHTML = '<div class="no-results">Enter search criteria above and click "Search Items" to find inventory items.</div>'; 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 * 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>'; searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
try { try {
@ -214,11 +228,13 @@ async function performSearch() {
// Store results for client-side re-sorting // Store results for client-side re-sorting
currentResultsData = data; currentResultsData = data;
// Update pagination state
updatePaginationState(data);
// Apply client-side slot filtering // Apply client-side slot filtering
applySlotFilter(data); applySlotFilter(data);
// Sort the results client-side before displaying // Display results (already sorted by server)
sortResults(data);
displayResults(data); displayResults(data);
} catch (error) { } catch (error) {
@ -280,6 +296,8 @@ function buildSearchParameters() {
addParam(params, 'max_damage_rating', 'searchMaxDamageRating'); addParam(params, 'max_damage_rating', 'searchMaxDamageRating');
addParam(params, 'min_heal_boost_rating', 'searchMinHealBoost'); addParam(params, 'min_heal_boost_rating', 'searchMinHealBoost');
addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost'); addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost');
addParam(params, 'min_vitality_rating', 'searchMinVitalityRating');
addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating');
// Requirements parameters // Requirements parameters
addParam(params, 'min_level', 'searchMinLevel'); addParam(params, 'min_level', 'searchMinLevel');
@ -308,8 +326,19 @@ function buildSearchParameters() {
params.append('legendary_cantrips', allSpells.join(',')); params.append('legendary_cantrips', allSpells.join(','));
} }
// Pagination only - sorting will be done client-side // Equipment slot filters
params.append('limit', '1000'); // Show all items on one page 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; return params;
} }
@ -357,6 +386,22 @@ function getSelectedProtections() {
return selectedProtections; 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 * 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="character_name">Character${getSortIcon('character_name')}</th>
<th class="sortable" data-sort="name">Item Name${getSortIcon('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="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 narrow-col 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 narrow-col sortable" data-sort="coverage">Coverage${getSortIcon('coverage')}</th>
<th class="text-right sortable" data-sort="armor_level">Armor${getSortIcon('armor_level')}</th> <th class="text-right sortable" data-sort="armor">Armor${getSortIcon('armor')}</th>
<th class="sortable" data-sort="spell_names">Spells/Cantrips${getSortIcon('spell_names')}</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="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="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> <th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
</tr> </tr>
</thead> </thead>
@ -399,14 +448,19 @@ function displayResults(data) {
const armor = item.armor_level > 0 ? item.armor_level : '-'; const armor = item.armor_level > 0 ? item.armor_level : '-';
const critDmg = item.crit_damage_rating > 0 ? item.crit_damage_rating : '-'; const critDmg = item.crit_damage_rating > 0 ? item.crit_damage_rating : '-';
const dmgRating = item.damage_rating > 0 ? item.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 status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory'; const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
// Use the slot_name provided by the API instead of incorrect mapping // 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 // 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 // Format last updated timestamp
const lastUpdated = item.last_updated ? const lastUpdated = item.last_updated ?
@ -444,17 +498,30 @@ function displayResults(data) {
// Get item type for display // Get item type for display
const itemType = item.item_type_name || '-'; 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 += ` html += `
<tr> <tr>
<td>${item.character_name}</td> <td>${item.character_name}</td>
<td class="item-name">${displayName}</td> <td class="item-name">${displayName}</td>
<td>${itemType}</td> <td>${itemType}</td>
<td class="text-right">${slot}</td> <td class="text-right narrow-col">${slot}</td>
<td class="text-right">${coverage}</td> <td class="text-right narrow-col">${coverage}</td>
<td class="text-right">${armor}</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">${critDmg}</td>
<td class="text-right">${dmgRating}</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> <td class="text-right">${lastUpdated}</td>
</tr> </tr>
`; `;
@ -465,11 +532,18 @@ function displayResults(data) {
</table> </table>
`; `;
// Add pagination info if needed // Add pagination controls if needed
if (data.total_pages > 1) { if (totalPages > 1) {
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
html += ` html += `
<div style="padding: 16px 24px; text-align: center; color: #5f6368; border-top: 1px solid #e8eaed;"> <div class="pagination-controls">
Showing page ${data.page} of ${data.total_pages} pages <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> </div>
`; `;
} }
@ -566,20 +640,9 @@ function handleSort(field) {
currentSort.direction = 'asc'; currentSort.direction = 'asc';
} }
// Re-display current results with new sorting (no new search needed) // Reset to page 1 and perform new search with updated sort
if (currentResultsData) { currentPage = 1;
// Reset items to original unfiltered data performSearch();
const originalData = JSON.parse(JSON.stringify(currentResultsData));
// Apply slot filtering first
applySlotFilter(originalData);
// Then apply sorting
sortResults(originalData);
// Display results
displayResults(originalData);
}
} }
/** /**
@ -722,3 +785,41 @@ function displaySetAnalysisResults(data) {
setAnalysisResults.innerHTML = html; 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'); 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; 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 column styles for inventory tables */
.sortable { .sortable {
cursor: pointer; cursor: pointer;