major fixes for inventory
This commit is contained in:
parent
00ef1d1f4b
commit
4d19e29847
10 changed files with 969 additions and 203 deletions
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,60 +1830,153 @@ 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 = ["""
|
||||||
SELECT DISTINCT
|
WITH items_with_slots AS (
|
||||||
i.id as db_item_id,
|
SELECT DISTINCT
|
||||||
i.character_name,
|
i.id as db_item_id,
|
||||||
i.name,
|
i.character_name,
|
||||||
i.icon,
|
i.name,
|
||||||
i.object_class,
|
i.icon,
|
||||||
i.value,
|
i.object_class,
|
||||||
i.burden,
|
i.value,
|
||||||
i.current_wielded_location,
|
i.burden,
|
||||||
i.bonded,
|
i.current_wielded_location,
|
||||||
i.attuned,
|
i.bonded,
|
||||||
i.unique,
|
i.attuned,
|
||||||
i.stack_size,
|
i.unique,
|
||||||
i.max_stack_size,
|
i.stack_size,
|
||||||
i.structure,
|
i.max_stack_size,
|
||||||
i.max_structure,
|
i.structure,
|
||||||
i.rare_id,
|
i.max_structure,
|
||||||
i.timestamp as last_updated,
|
i.rare_id,
|
||||||
COALESCE(cs.max_damage, -1) as max_damage,
|
i.timestamp as last_updated,
|
||||||
COALESCE(cs.armor_level, -1) as armor_level,
|
COALESCE(cs.max_damage, -1) as max_damage,
|
||||||
COALESCE(cs.attack_bonus, -1.0) as attack_bonus,
|
COALESCE(cs.armor_level, -1) as armor_level,
|
||||||
GREATEST(
|
COALESCE(cs.attack_bonus, -1.0) as attack_bonus,
|
||||||
COALESCE((rd.int_values->>'314')::int, -1),
|
GREATEST(
|
||||||
COALESCE((rd.int_values->>'374')::int, -1)
|
COALESCE((rd.int_values->>'314')::int, -1),
|
||||||
) as crit_damage_rating,
|
COALESCE((rd.int_values->>'374')::int, -1)
|
||||||
GREATEST(
|
) as crit_damage_rating,
|
||||||
COALESCE((rd.int_values->>'307')::int, -1),
|
GREATEST(
|
||||||
COALESCE((rd.int_values->>'370')::int, -1)
|
COALESCE((rd.int_values->>'307')::int, -1),
|
||||||
) as damage_rating,
|
COALESCE((rd.int_values->>'370')::int, -1)
|
||||||
GREATEST(
|
) as damage_rating,
|
||||||
COALESCE((rd.int_values->>'323')::int, -1),
|
GREATEST(
|
||||||
COALESCE((rd.int_values->>'376')::int, -1)
|
COALESCE((rd.int_values->>'323')::int, -1),
|
||||||
) as heal_boost_rating,
|
COALESCE((rd.int_values->>'376')::int, -1)
|
||||||
COALESCE(req.wield_level, -1) as wield_level,
|
) as heal_boost_rating,
|
||||||
COALESCE(enh.material, '') as material,
|
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
||||||
COALESCE(enh.workmanship, -1.0) as workmanship,
|
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
|
||||||
COALESCE(enh.imbue, '') as imbue,
|
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
||||||
COALESCE(enh.tinks, -1) as tinks,
|
COALESCE((rd.int_values->>'316')::int, -1) as crit_damage_resist_rating,
|
||||||
COALESCE(enh.item_set, '') as item_set,
|
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
||||||
rd.original_json
|
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
||||||
FROM items i
|
COALESCE((rd.int_values->>'342')::int, -1) as healing_rating,
|
||||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
COALESCE((rd.int_values->>'350')::int, -1) as dot_resist_rating,
|
||||||
LEFT JOIN item_requirements req ON i.id = req.item_id
|
COALESCE((rd.int_values->>'351')::int, -1) as life_resist_rating,
|
||||||
LEFT JOIN item_enhancements enh ON i.id = enh.item_id
|
COALESCE((rd.int_values->>'356')::int, -1) as sneak_attack_rating,
|
||||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
COALESCE((rd.int_values->>'357')::int, -1) as recklessness_rating,
|
||||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
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,
|
||||||
|
|
||||||
|
-- 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 = []
|
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 (
|
||||||
FROM items i
|
SELECT DISTINCT
|
||||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
i.id as db_item_id,
|
||||||
LEFT JOIN item_requirements req ON i.id = req.item_id
|
i.character_name,
|
||||||
LEFT JOIN item_enhancements enh ON i.id = enh.item_id
|
i.name,
|
||||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
i.object_class,
|
||||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
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
|
# 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
|
||||||
|
|
|
||||||
2
main.py
2
main.py
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue