added portals, quest tracking, discord monitor etc etc
|
|
@ -37,6 +37,7 @@ This project provides:
|
||||||
- Filter by character, equipment type, material, stats, and more
|
- Filter by character, equipment type, material, stats, and more
|
||||||
- Sort by any column with live results
|
- Sort by any column with live results
|
||||||
- Track item properties including spells, armor level, damage ratings
|
- Track item properties including spells, armor level, damage ratings
|
||||||
|
- **Discord Rare Monitor Bot**: Monitors rare discoveries and posts filtered notifications to Discord channels
|
||||||
- **Sample Data Generator**: `generate_data.py` sends telemetry snapshots over WebSocket for testing.
|
- **Sample Data Generator**: `generate_data.py` sends telemetry snapshots over WebSocket for testing.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
|
||||||
35
db_async.py
|
|
@ -5,6 +5,7 @@ initialization function to set up TimescaleDB hypertable.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from databases import Database
|
from databases import Database
|
||||||
from sqlalchemy import MetaData, Table, Column, Integer, String, Float, DateTime, text
|
from sqlalchemy import MetaData, Table, Column, Integer, String, Float, DateTime, text
|
||||||
from sqlalchemy import Index, BigInteger, JSON, Boolean, UniqueConstraint
|
from sqlalchemy import Index, BigInteger, JSON, Boolean, UniqueConstraint
|
||||||
|
|
@ -126,6 +127,20 @@ character_inventories = Table(
|
||||||
UniqueConstraint("character_name", "item_id", name="uq_char_item"),
|
UniqueConstraint("character_name", "item_id", name="uq_char_item"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Portal discoveries table for 24-hour live tracking
|
||||||
|
portal_discoveries = Table(
|
||||||
|
# Records player portal discoveries with 24-hour retention
|
||||||
|
"portal_discoveries",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True),
|
||||||
|
Column("character_name", String, nullable=False, index=True),
|
||||||
|
Column("portal_name", String, nullable=False),
|
||||||
|
Column("timestamp", DateTime(timezone=True), nullable=False, index=True),
|
||||||
|
Column("ns", Float, nullable=False), # North/South coordinate as float
|
||||||
|
Column("ew", Float, nullable=False), # East/West coordinate as float
|
||||||
|
Column("z", Float, nullable=False), # Elevation as float
|
||||||
|
)
|
||||||
|
|
||||||
# Server health monitoring tables
|
# Server health monitoring tables
|
||||||
server_health_checks = Table(
|
server_health_checks = Table(
|
||||||
# Time-series data for server health checks
|
# Time-series data for server health checks
|
||||||
|
|
@ -213,4 +228,22 @@ async def init_db_async():
|
||||||
"SELECT add_compression_policy('telemetry_events', INTERVAL '1 day')"
|
"SELECT add_compression_policy('telemetry_events', INTERVAL '1 day')"
|
||||||
))
|
))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: failed to set retention/compression policies: {e}")
|
print(f"Warning: failed to set retention/compression policies: {e}")
|
||||||
|
|
||||||
|
async def cleanup_old_portals():
|
||||||
|
"""Clean up portal discoveries older than 24 hours."""
|
||||||
|
try:
|
||||||
|
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||||
|
|
||||||
|
# Delete old portal discoveries
|
||||||
|
result = await database.execute(
|
||||||
|
"DELETE FROM portal_discoveries WHERE timestamp < :cutoff_time",
|
||||||
|
{"cutoff_time": cutoff_time}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Cleaned up {result} portal discoveries older than 24 hours")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: failed to cleanup old portals: {e}")
|
||||||
|
return 0
|
||||||
27
discord-rare-monitor/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Discord Rare Monitor Bot - Dockerfile
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY discord_rare_monitor.py .
|
||||||
|
COPY test_websocket.py .
|
||||||
|
COPY icon_mapping.py .
|
||||||
|
|
||||||
|
# Copy icons directory
|
||||||
|
COPY icons/ ./icons/
|
||||||
|
|
||||||
|
# Default environment variables
|
||||||
|
ENV DISCORD_RARE_BOT_TOKEN="" \
|
||||||
|
DERETH_TRACKER_WS_URL="ws://dereth-tracker:8765/ws/position" \
|
||||||
|
COMMON_RARE_CHANNEL_ID="1355328792184226014" \
|
||||||
|
GREAT_RARE_CHANNEL_ID="1353676584334131211" \
|
||||||
|
LOG_LEVEL="INFO"
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CMD ["python", "discord_rare_monitor.py"]
|
||||||
95
discord-rare-monitor/README.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Discord Rare Monitor Bot
|
||||||
|
|
||||||
|
A Discord bot that monitors the Dereth Tracker WebSocket stream for rare discoveries and posts filtered notifications to Discord channels.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time Monitoring**: Connects to Dereth Tracker WebSocket for instant rare notifications
|
||||||
|
- **Smart Classification**: Automatically classifies rares as "common" or "great" based on keywords
|
||||||
|
- **Rich Embeds**: Posts formatted Discord embeds with location and timestamp information
|
||||||
|
- **Dual Channels**: Posts to separate channels for common and great rares
|
||||||
|
- **Robust Connection**: Automatic reconnection with exponential backoff on connection failures
|
||||||
|
|
||||||
|
## Rare Classification
|
||||||
|
|
||||||
|
### Common Rares
|
||||||
|
Items containing these keywords (except "Frore Crystal"):
|
||||||
|
- Crystal
|
||||||
|
- Jewel
|
||||||
|
- Pearl
|
||||||
|
- Elixir
|
||||||
|
- Kit
|
||||||
|
|
||||||
|
### Great Rares
|
||||||
|
All other rare discoveries not classified as common.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The bot is configured via environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DISCORD_RARE_BOT_TOKEN` | Required | Discord bot token |
|
||||||
|
| `DERETH_TRACKER_WS_URL` | `ws://dereth-tracker:8765/ws/position` | WebSocket URL |
|
||||||
|
| `COMMON_RARE_CHANNEL_ID` | `1355328792184226014` | Discord channel for common rares |
|
||||||
|
| `GREAT_RARE_CHANNEL_ID` | `1353676584334131211` | Discord channel for great rares |
|
||||||
|
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |
|
||||||
|
|
||||||
|
## Docker Usage
|
||||||
|
|
||||||
|
The bot is designed to run as a Docker container alongside the Dereth Tracker services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start all services including the Discord bot
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View bot logs
|
||||||
|
docker-compose logs discord-rare-monitor
|
||||||
|
|
||||||
|
# Restart just the bot
|
||||||
|
docker-compose restart discord-rare-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
1. Create a Discord application and bot at https://discord.com/developers/applications
|
||||||
|
2. Get the bot token and invite the bot to your Discord server
|
||||||
|
3. Set the `DISCORD_RARE_BOT_TOKEN` environment variable
|
||||||
|
4. Ensure the bot has permissions to send messages in the target channels
|
||||||
|
|
||||||
|
## Message Format
|
||||||
|
|
||||||
|
The bot listens for WebSocket messages with this structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "rare",
|
||||||
|
"character_name": "PlayerName",
|
||||||
|
"name": "Dark Heart",
|
||||||
|
"timestamp": "2025-06-22T16:00:00Z",
|
||||||
|
"ew": 12.34,
|
||||||
|
"ns": -56.78,
|
||||||
|
"z": 10.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **WebSocket Client**: Connects to Dereth Tracker's WebSocket stream
|
||||||
|
- **Message Filter**: Only processes `{"type": "rare"}` messages
|
||||||
|
- **Classifier**: Determines rare type based on name keywords
|
||||||
|
- **Discord Client**: Posts formatted embeds to appropriate channels
|
||||||
|
- **Retry Logic**: Automatic reconnection with exponential backoff
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `discord.py>=2.3.0` - Discord API client
|
||||||
|
- `websockets>=11.0.0` - WebSocket client library
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Zero Duplication**: Each rare generates exactly one notification
|
||||||
|
- **Real-time**: Instant notifications via WebSocket stream
|
||||||
|
- **Lightweight**: Minimal resource usage (~50MB RAM)
|
||||||
|
- **Reliable**: Robust error handling and reconnection logic
|
||||||
|
- **Integrated**: Seamlessly works with existing Dereth Tracker infrastructure
|
||||||
67
discord-rare-monitor/config.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
"""
|
||||||
|
Configuration module for Discord Rare Monitor Bot.
|
||||||
|
Centralizes environment variable handling and configuration constants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Configuration class for Discord Rare Monitor Bot."""
|
||||||
|
|
||||||
|
# Discord Configuration
|
||||||
|
DISCORD_TOKEN: str = os.getenv('DISCORD_RARE_BOT_TOKEN', '')
|
||||||
|
COMMON_RARE_CHANNEL_ID: int = int(os.getenv('COMMON_RARE_CHANNEL_ID', '1355328792184226014'))
|
||||||
|
GREAT_RARE_CHANNEL_ID: int = int(os.getenv('GREAT_RARE_CHANNEL_ID', '1353676584334131211'))
|
||||||
|
|
||||||
|
# WebSocket Configuration
|
||||||
|
WEBSOCKET_URL: str = os.getenv('DERETH_TRACKER_WS_URL', 'ws://dereth-tracker:8765/ws/position')
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
LOG_LEVEL: str = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||||
|
|
||||||
|
# Rare Classification Configuration
|
||||||
|
COMMON_RARE_KEYWORDS: list = ["Crystal", "Jewel", "Pearl", "Elixir", "Kit"]
|
||||||
|
|
||||||
|
# WebSocket Retry Configuration
|
||||||
|
INITIAL_RETRY_DELAY: int = 5 # seconds
|
||||||
|
MAX_RETRY_DELAY: int = 300 # 5 minutes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls) -> list:
|
||||||
|
"""Validate configuration and return list of errors."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if not cls.DISCORD_TOKEN:
|
||||||
|
errors.append("DISCORD_RARE_BOT_TOKEN environment variable is required")
|
||||||
|
|
||||||
|
if not cls.WEBSOCKET_URL:
|
||||||
|
errors.append("DERETH_TRACKER_WS_URL environment variable is required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls.COMMON_RARE_CHANNEL_ID = int(cls.COMMON_RARE_CHANNEL_ID)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
errors.append("COMMON_RARE_CHANNEL_ID must be a valid integer")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cls.GREAT_RARE_CHANNEL_ID = int(cls.GREAT_RARE_CHANNEL_ID)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
errors.append("GREAT_RARE_CHANNEL_ID must be a valid integer")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def log_config(cls, logger):
|
||||||
|
"""Log current configuration (excluding sensitive data)."""
|
||||||
|
logger.info("🔧 Discord Rare Monitor Configuration:")
|
||||||
|
logger.info(f" WebSocket URL: {cls.WEBSOCKET_URL}")
|
||||||
|
logger.info(f" Common Rare Channel ID: {cls.COMMON_RARE_CHANNEL_ID}")
|
||||||
|
logger.info(f" Great Rare Channel ID: {cls.GREAT_RARE_CHANNEL_ID}")
|
||||||
|
logger.info(f" Log Level: {cls.LOG_LEVEL}")
|
||||||
|
logger.info(f" Common Keywords: {cls.COMMON_RARE_KEYWORDS}")
|
||||||
|
logger.info(f" Discord Token: {'✅ Set' if cls.DISCORD_TOKEN else '❌ Not Set'}")
|
||||||
|
|
||||||
|
|
||||||
|
# Global config instance
|
||||||
|
config = Config()
|
||||||
880
discord-rare-monitor/discord_rare_monitor.py
Normal file
|
|
@ -0,0 +1,880 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Discord Rare Monitor Bot - Monitors Dereth Tracker WebSocket for rare discoveries
|
||||||
|
and posts filtered notifications to Discord channels.
|
||||||
|
|
||||||
|
Listens for {"type": "rare"} messages from the WebSocket stream and posts them
|
||||||
|
to appropriate Discord channels based on rare classification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import discord
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
# Get log level from environment
|
||||||
|
log_level = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, log_level, logging.INFO),
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[logging.StreamHandler(sys.stdout)]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Configuration from environment variables
|
||||||
|
DISCORD_TOKEN = os.getenv('DISCORD_RARE_BOT_TOKEN')
|
||||||
|
WEBSOCKET_URL = os.getenv('DERETH_TRACKER_WS_URL', 'ws://dereth-tracker:8765/ws/live')
|
||||||
|
SHARED_SECRET = 'your_shared_secret'
|
||||||
|
ACLOG_CHANNEL_ID = int(os.getenv('ACLOG_CHANNEL_ID', '1349649482786275328'))
|
||||||
|
COMMON_RARE_CHANNEL_ID = int(os.getenv('COMMON_RARE_CHANNEL_ID', '1355328792184226014'))
|
||||||
|
GREAT_RARE_CHANNEL_ID = int(os.getenv('GREAT_RARE_CHANNEL_ID', '1353676584334131211'))
|
||||||
|
|
||||||
|
# Character to monitor for allegiance chat
|
||||||
|
MONITOR_CHARACTER = os.getenv('MONITOR_CHARACTER', 'Dunking Rares')
|
||||||
|
|
||||||
|
# Comprehensive rare classification patterns
|
||||||
|
|
||||||
|
# Common Rares - Exact match pattern (70 items)
|
||||||
|
COMMON_RARES_PATTERN = re.compile(r"^(Alchemist's Crystal|Scholar's Crystal|Smithy's Crystal|Hunter's Crystal|Observer's Crystal|Thorsten's Crystal|Elysa's Crystal|Chef's Crystal|Enchanter's Crystal|Oswald's Crystal|Deceiver's Crystal|Fletcher's Crystal|Physician's Crystal|Artificer's Crystal|Tinker's Crystal|Vaulter's Crystal|Monarch's Crystal|Life Giver's Crystal|Thief's Crystal|Adherent's Crystal|Resister's Crystal|Imbuer's Crystal|Converter's Crystal|Evader's Crystal|Dodger's Crystal|Zefir's Crystal|Ben Ten's Crystal|Corruptor's Crystal|Artist's Crystal|T'ing's Crystal|Warrior's Crystal|Brawler's Crystal|Hieromancer's Crystal|Rogue's Crystal|Berzerker's Crystal|Lugian's Pearl|Ursuin's Pearl|Wayfarer's Pearl|Sprinter's Pearl|Magus's Pearl|Lich's Pearl|Warrior's Jewel|Melee's Jewel|Mage's Jewel|Duelist's Jewel|Archer's Jewel|Tusker's Jewel|Olthoi's Jewel|Inferno's Jewel|Gelid's Jewel|Astyrrian's Jewel|Executor's Jewel|Pearl of Blood Drinking|Pearl of Heart Seeking|Pearl of Defending|Pearl of Swift Killing|Pearl of Spirit Drinking|Pearl of Hermetic Linking|Pearl of Blade Baning|Pearl of Pierce Baning|Pearl of Bludgeon Baning|Pearl of Acid Baning|Pearl of Flame Baning|Pearl of Frost Baning|Pearl of Lightning Baning|Pearl of Impenetrability|Refreshing Elixir|Invigorating Elixir|Miraculous Elixir|Medicated Health Kit|Medicated Stamina Kit|Medicated Mana Kit|Casino Exquisite Keyring)$")
|
||||||
|
|
||||||
|
# Combined pattern for detecting any rare in chat messages (simplified for common detection)
|
||||||
|
RARE_IN_CHAT_PATTERN = re.compile(r"(Crystal|Pearl|Jewel|Elixir|Kit|Hieroglyph|Pictograph|Ideograph|Rune|Infinite|Eternal|Perennial|Foolproof|Limitless|Shimmering|Gelidite|Leikotha|Frore|Staff of|Count Renari|Wand of)")
|
||||||
|
|
||||||
|
# Legacy keywords for backward compatibility
|
||||||
|
COMMON_RARE_KEYWORDS = ["Crystal", "Jewel", "Pearl", "Elixir", "Kit"]
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordRareMonitor:
|
||||||
|
"""Discord bot that monitors WebSocket for rare discoveries and posts to Discord."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Discord client setup
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.guilds = True
|
||||||
|
intents.messages = True
|
||||||
|
intents.message_content = True
|
||||||
|
self.client = discord.Client(intents=intents)
|
||||||
|
|
||||||
|
# WebSocket connection tracking
|
||||||
|
self.websocket_task: Optional[asyncio.Task] = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
# Setup Discord event handlers
|
||||||
|
self.setup_discord_handlers()
|
||||||
|
|
||||||
|
def setup_discord_handlers(self):
|
||||||
|
"""Setup Discord client event handlers."""
|
||||||
|
|
||||||
|
@self.client.event
|
||||||
|
async def on_ready():
|
||||||
|
logger.info(f'✅ Discord bot logged in as {self.client.user}')
|
||||||
|
|
||||||
|
# Debug: List all guilds (servers) the bot is in
|
||||||
|
guild_count = len(self.client.guilds)
|
||||||
|
logger.info(f"🏰 Bot is in {guild_count} guild(s)")
|
||||||
|
for guild in self.client.guilds:
|
||||||
|
logger.info(f" - {guild.name} (ID: {guild.id})")
|
||||||
|
# List first 5 channels in each guild for debugging
|
||||||
|
channels = list(guild.channels)[:5]
|
||||||
|
for channel in channels:
|
||||||
|
logger.info(f" Channel: #{channel.name} (ID: {channel.id})")
|
||||||
|
|
||||||
|
# Verify channels exist
|
||||||
|
aclog_channel = self.client.get_channel(ACLOG_CHANNEL_ID)
|
||||||
|
common_channel = self.client.get_channel(COMMON_RARE_CHANNEL_ID)
|
||||||
|
great_channel = self.client.get_channel(GREAT_RARE_CHANNEL_ID)
|
||||||
|
|
||||||
|
logger.info(f"🔍 Looking for aclog channel {ACLOG_CHANNEL_ID}: {'Found' if aclog_channel else 'NOT FOUND'}")
|
||||||
|
logger.info(f"🔍 Looking for common channel {COMMON_RARE_CHANNEL_ID}: {'Found' if common_channel else 'NOT FOUND'}")
|
||||||
|
logger.info(f"🔍 Looking for great channel {GREAT_RARE_CHANNEL_ID}: {'Found' if great_channel else 'NOT FOUND'}")
|
||||||
|
|
||||||
|
if aclog_channel:
|
||||||
|
logger.info(f"📍 AC Log channel: #{aclog_channel.name}")
|
||||||
|
if common_channel:
|
||||||
|
logger.info(f"📍 Common rares channel: #{common_channel.name}")
|
||||||
|
if great_channel:
|
||||||
|
logger.info(f"📍 Great rares channel: #{great_channel.name}")
|
||||||
|
|
||||||
|
logger.info("🎯 Bot ready to receive messages!")
|
||||||
|
|
||||||
|
# Start WebSocket monitoring
|
||||||
|
self.running = True
|
||||||
|
self.websocket_task = asyncio.create_task(self.monitor_websocket())
|
||||||
|
logger.info("🔄 Started WebSocket monitoring task")
|
||||||
|
|
||||||
|
@self.client.event
|
||||||
|
async def on_disconnect():
|
||||||
|
logger.warning("⚠️ Discord client disconnected")
|
||||||
|
self.running = False
|
||||||
|
if self.websocket_task:
|
||||||
|
self.websocket_task.cancel()
|
||||||
|
|
||||||
|
@self.client.event
|
||||||
|
async def on_message(message):
|
||||||
|
# Don't respond to bot's own messages
|
||||||
|
if message.author == self.client.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Debug: log all messages (remove this after testing)
|
||||||
|
logger.info(f"📨 Received message: '{message.content}' from {message.author} in #{message.channel}")
|
||||||
|
|
||||||
|
# Handle !echo command for testing bot responsiveness
|
||||||
|
content = message.content # Don't convert to lowercase for echo
|
||||||
|
if content.startswith('!echo'):
|
||||||
|
# Extract the text after !echo
|
||||||
|
echo_text = message.content[5:].strip()
|
||||||
|
if echo_text:
|
||||||
|
await message.channel.send(f"🤖 Echo: {echo_text}")
|
||||||
|
else:
|
||||||
|
await message.channel.send("🤖 Echo: Hello! Discord Rare Monitor Bot is alive and responding!")
|
||||||
|
logger.info(f"📣 Responded to !echo command from {message.author} in #{message.channel}")
|
||||||
|
|
||||||
|
# Handle !status command for bot status
|
||||||
|
elif message.content.startswith('!status'):
|
||||||
|
status_msg = "🤖 **Discord Rare Monitor Bot Status:**\n"
|
||||||
|
status_msg += f"✅ Bot is online and connected\n"
|
||||||
|
status_msg += f"🔗 WebSocket: {'Connected' if self.websocket_task and not self.websocket_task.done() else 'Disconnected'}\n"
|
||||||
|
status_msg += f"📡 Monitoring: {WEBSOCKET_URL}\n"
|
||||||
|
status_msg += f"📍 Common Rares Channel: <#{COMMON_RARE_CHANNEL_ID}>\n"
|
||||||
|
status_msg += f"📍 Great Rares Channel: <#{GREAT_RARE_CHANNEL_ID}>"
|
||||||
|
|
||||||
|
await message.channel.send(status_msg)
|
||||||
|
logger.info(f"📊 Responded to !status command from {message.author} in #{message.channel}")
|
||||||
|
|
||||||
|
# Handle !icons command to display all rare icons
|
||||||
|
elif message.content.startswith('!icons'):
|
||||||
|
# Parse command arguments
|
||||||
|
args = message.content.split()
|
||||||
|
logger.info(f"🔍 !icons command received with args: {args}")
|
||||||
|
if len(args) > 1:
|
||||||
|
if args[1].lower() == 'all':
|
||||||
|
logger.info("📋 Calling handle_icons_command for 'all'")
|
||||||
|
await self.handle_icons_command(message)
|
||||||
|
elif args[1].lower() == 'grid':
|
||||||
|
await self.handle_icons_grid(message)
|
||||||
|
else:
|
||||||
|
# Search for specific rare item
|
||||||
|
search_term = ' '.join(args[1:])
|
||||||
|
await self.handle_icons_search(message, search_term)
|
||||||
|
else:
|
||||||
|
logger.info("📋 Calling handle_icons_summary (no args)")
|
||||||
|
await self.handle_icons_summary(message)
|
||||||
|
|
||||||
|
async def monitor_websocket(self):
|
||||||
|
"""Monitor Dereth Tracker WebSocket for rare events with robust reconnection."""
|
||||||
|
retry_delay = 5 # seconds
|
||||||
|
max_retry_delay = 300 # 5 minutes
|
||||||
|
consecutive_failures = 0
|
||||||
|
max_consecutive_failures = 10
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
websocket = None
|
||||||
|
last_message_time = time.time()
|
||||||
|
health_check_interval = 60 # Check health every 60 seconds
|
||||||
|
message_timeout = 180 # Consider connection dead if no messages for 3 minutes
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to live endpoint (no authentication needed for browsers)
|
||||||
|
logger.info(f"🔗 Connecting to WebSocket: {WEBSOCKET_URL}")
|
||||||
|
|
||||||
|
# Add connection timeout and ping interval for better connection health
|
||||||
|
websocket = await websockets.connect(
|
||||||
|
WEBSOCKET_URL,
|
||||||
|
ping_interval=30, # Send ping every 30 seconds
|
||||||
|
ping_timeout=10, # Wait 10 seconds for pong
|
||||||
|
close_timeout=10 # Wait 10 seconds for close
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("✅ WebSocket connected successfully")
|
||||||
|
retry_delay = 5 # Reset retry delay on successful connection
|
||||||
|
consecutive_failures = 0 # Reset failure counter
|
||||||
|
last_message_time = time.time() # Reset message timer
|
||||||
|
|
||||||
|
# Send a test message to Discord to indicate connection restored
|
||||||
|
await self.post_status_to_aclog("🔗 WebSocket connection established")
|
||||||
|
|
||||||
|
# Create tasks for message processing and health checking
|
||||||
|
async def process_messages():
|
||||||
|
nonlocal last_message_time
|
||||||
|
async for message in websocket:
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
|
||||||
|
last_message_time = time.time()
|
||||||
|
logger.debug(f"📨 Raw WebSocket message: {message[:100]}...")
|
||||||
|
await self.process_websocket_message(message)
|
||||||
|
|
||||||
|
async def health_check():
|
||||||
|
"""Periodically check connection health and force reconnect if needed."""
|
||||||
|
while self.running and websocket and not websocket.closed:
|
||||||
|
await asyncio.sleep(health_check_interval)
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
time_since_last_message = current_time - last_message_time
|
||||||
|
|
||||||
|
if time_since_last_message > message_timeout:
|
||||||
|
logger.warning(f"⚠️ No messages received for {time_since_last_message:.0f} seconds, forcing reconnect")
|
||||||
|
await self.post_status_to_aclog(f"⚠️ WebSocket appears dead, reconnecting...")
|
||||||
|
if websocket and not websocket.closed:
|
||||||
|
await websocket.close()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.debug(f"💓 WebSocket health check passed (last message {time_since_last_message:.0f}s ago)")
|
||||||
|
|
||||||
|
# Run both tasks concurrently
|
||||||
|
message_task = asyncio.create_task(process_messages())
|
||||||
|
health_task = asyncio.create_task(health_check())
|
||||||
|
|
||||||
|
# Wait for either task to complete (health check failure or message loop exit)
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
[message_task, health_task],
|
||||||
|
return_when=asyncio.FIRST_COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cancel any remaining tasks
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
logger.warning(f"⚠️ WebSocket connection closed: {e}")
|
||||||
|
consecutive_failures += 1
|
||||||
|
await self.post_status_to_aclog(f"⚠️ WebSocket disconnected (attempt {consecutive_failures})")
|
||||||
|
|
||||||
|
except websockets.exceptions.InvalidStatusCode as e:
|
||||||
|
logger.error(f"❌ WebSocket invalid status code: {e}")
|
||||||
|
consecutive_failures += 1
|
||||||
|
await self.post_status_to_aclog(f"❌ WebSocket connection failed: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ WebSocket error: {e}")
|
||||||
|
consecutive_failures += 1
|
||||||
|
await self.post_status_to_aclog(f"❌ WebSocket error: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Ensure websocket is properly closed
|
||||||
|
if websocket and not websocket.closed:
|
||||||
|
try:
|
||||||
|
await websocket.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check if we should keep retrying
|
||||||
|
if consecutive_failures >= max_consecutive_failures:
|
||||||
|
logger.error(f"❌ Too many consecutive failures ({consecutive_failures}). Stopping reconnection attempts.")
|
||||||
|
await self.post_status_to_aclog(f"❌ WebSocket reconnection failed after {consecutive_failures} attempts. Bot may need restart.")
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.running:
|
||||||
|
logger.info(f"🔄 Retrying WebSocket connection in {retry_delay} seconds... (failure #{consecutive_failures})")
|
||||||
|
await asyncio.sleep(retry_delay)
|
||||||
|
|
||||||
|
# Exponential backoff with max delay
|
||||||
|
retry_delay = min(retry_delay * 2, max_retry_delay)
|
||||||
|
|
||||||
|
async def process_websocket_message(self, raw_message: str):
|
||||||
|
"""Process incoming WebSocket message."""
|
||||||
|
try:
|
||||||
|
data = json.loads(raw_message)
|
||||||
|
msg_type = data.get('type')
|
||||||
|
|
||||||
|
# Debug: Log all message types
|
||||||
|
logger.debug(f"📩 WebSocket message type: {msg_type} from {data.get('character_name', 'Unknown')}")
|
||||||
|
|
||||||
|
# Handle rare event messages
|
||||||
|
if msg_type == 'rare':
|
||||||
|
await self.handle_rare_event(data)
|
||||||
|
|
||||||
|
# Handle chat messages from monitored character
|
||||||
|
elif msg_type == 'chat':
|
||||||
|
await self.handle_chat_message(data)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Ignore invalid JSON messages
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error processing WebSocket message: {e}")
|
||||||
|
|
||||||
|
async def handle_rare_event(self, data: dict):
|
||||||
|
"""Handle a rare discovery event."""
|
||||||
|
try:
|
||||||
|
rare_name = data.get('name', 'Unknown Rare')
|
||||||
|
character_name = data.get('character_name', 'Unknown Character')
|
||||||
|
timestamp = data.get('timestamp', '')
|
||||||
|
|
||||||
|
logger.info(f"🎯 RARE EVENT RECEIVED: {rare_name} by {character_name}")
|
||||||
|
logger.debug(f"📦 Full rare data: {data}")
|
||||||
|
|
||||||
|
# Classify rare type
|
||||||
|
rare_type = self.classify_rare(rare_name)
|
||||||
|
|
||||||
|
# Post to Discord
|
||||||
|
await self.post_rare_to_discord(data, rare_type)
|
||||||
|
|
||||||
|
# DEBUG: Also post rare info to aclog for monitoring classification
|
||||||
|
await self.post_rare_debug_to_aclog(rare_name, rare_type, character_name)
|
||||||
|
|
||||||
|
logger.info(f"📬 Posted to Discord: {rare_name} ({rare_type}) from {character_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling rare event: {e}")
|
||||||
|
|
||||||
|
async def handle_chat_message(self, data: dict):
|
||||||
|
"""Handle a chat message from monitored character."""
|
||||||
|
try:
|
||||||
|
character_name = data.get('character_name', '')
|
||||||
|
chat_text = data.get('text', '')
|
||||||
|
|
||||||
|
logger.debug(f"🔍 Checking chat from '{character_name}' (looking for '{MONITOR_CHARACTER}')")
|
||||||
|
|
||||||
|
# Only process messages from the monitored character
|
||||||
|
if character_name != MONITOR_CHARACTER:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip if this message contains any rare names (common or great)
|
||||||
|
if RARE_IN_CHAT_PATTERN.search(chat_text) or self.is_rare_message(chat_text):
|
||||||
|
logger.debug(f"🎯 Skipping rare message from {character_name}: {chat_text}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"💬 Chat from {character_name}: {chat_text}")
|
||||||
|
|
||||||
|
# Post to AC Log channel
|
||||||
|
await self.post_chat_to_discord(data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling chat message: {e}")
|
||||||
|
|
||||||
|
def is_rare_message(self, text: str) -> bool:
|
||||||
|
"""Check if text appears to be a rare discovery message."""
|
||||||
|
# Look for common rare discovery patterns
|
||||||
|
rare_patterns = [
|
||||||
|
"has discovered the",
|
||||||
|
"found a rare",
|
||||||
|
"discovered a",
|
||||||
|
"rare discovery",
|
||||||
|
"Golden Gryphon",
|
||||||
|
"Dark Heart",
|
||||||
|
"Sunstone",
|
||||||
|
]
|
||||||
|
return any(pattern.lower() in text.lower() for pattern in rare_patterns)
|
||||||
|
|
||||||
|
def classify_rare(self, rare_name: str) -> str:
|
||||||
|
"""Classify rare as 'common' or 'great' based on exact name matching."""
|
||||||
|
# Use regex pattern matching for precise classification
|
||||||
|
if COMMON_RARES_PATTERN.match(rare_name):
|
||||||
|
return "common"
|
||||||
|
else:
|
||||||
|
return "great"
|
||||||
|
|
||||||
|
def get_rare_icon_path(self, rare_name: str) -> Optional[str]:
|
||||||
|
"""Get the file path for a rare item's icon if it exists."""
|
||||||
|
try:
|
||||||
|
# Convert rare name to icon filename using same logic as download script
|
||||||
|
filename = rare_name.replace("'", "").replace(" ", "_").replace("-", "_") + "_Icon.png"
|
||||||
|
|
||||||
|
# Get the icons directory path
|
||||||
|
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
icon_path = os.path.join(icons_dir, filename)
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
return icon_path
|
||||||
|
else:
|
||||||
|
logger.debug(f"🔍 Icon not found for '{rare_name}': {filename}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Error getting icon path for '{rare_name}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def post_rare_to_discord(self, data: dict, rare_type: str):
|
||||||
|
"""Post rare discovery to appropriate Discord channel."""
|
||||||
|
try:
|
||||||
|
rare_name = data.get('name', 'Unknown Rare')
|
||||||
|
character_name = data.get('character_name', 'Unknown Character')
|
||||||
|
timestamp_str = data.get('timestamp', '')
|
||||||
|
ew = data.get('ew')
|
||||||
|
ns = data.get('ns')
|
||||||
|
z = data.get('z')
|
||||||
|
|
||||||
|
# Parse timestamp
|
||||||
|
try:
|
||||||
|
if timestamp_str:
|
||||||
|
# Handle both with and without 'Z' suffix
|
||||||
|
if timestamp_str.endswith('Z'):
|
||||||
|
timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
||||||
|
else:
|
||||||
|
timestamp = datetime.fromisoformat(timestamp_str)
|
||||||
|
else:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
except ValueError:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
|
||||||
|
# Try to get icon for this rare
|
||||||
|
icon_path = self.get_rare_icon_path(rare_name)
|
||||||
|
icon_file = None
|
||||||
|
icon_filename = None
|
||||||
|
|
||||||
|
if icon_path:
|
||||||
|
icon_filename = os.path.basename(icon_path)
|
||||||
|
icon_file = discord.File(icon_path, filename=icon_filename)
|
||||||
|
logger.debug(f"🖼️ Found icon for '{rare_name}': {icon_filename}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"📷 No icon available for '{rare_name}'")
|
||||||
|
|
||||||
|
# Create Discord embed
|
||||||
|
if rare_type == "great":
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="💎 Great Rare Discovery!",
|
||||||
|
description=f"**{character_name}** has discovered the **{rare_name}**!",
|
||||||
|
color=discord.Color.gold(),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="🔸 Common Rare Discovery",
|
||||||
|
description=f"**{character_name}** has discovered the **{rare_name}**!",
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add icon image if available (at original size)
|
||||||
|
if icon_file and icon_filename:
|
||||||
|
embed.set_image(url=f"attachment://{icon_filename}")
|
||||||
|
|
||||||
|
# Add location if available
|
||||||
|
if ew is not None and ns is not None:
|
||||||
|
location_str = f"{ew:.1f}E, {ns:.1f}N"
|
||||||
|
if z is not None:
|
||||||
|
location_str += f", {z:.1f}Z"
|
||||||
|
embed.add_field(name="📍 Location", value=location_str, inline=True)
|
||||||
|
|
||||||
|
# Add timestamp in a readable format
|
||||||
|
embed.add_field(
|
||||||
|
name="⏰ Time",
|
||||||
|
value=timestamp.strftime("%H:%M:%S UTC"),
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get appropriate channel
|
||||||
|
if rare_type == "common":
|
||||||
|
channel = self.client.get_channel(COMMON_RARE_CHANNEL_ID)
|
||||||
|
else:
|
||||||
|
channel = self.client.get_channel(GREAT_RARE_CHANNEL_ID)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
# Send with or without icon file
|
||||||
|
if icon_file:
|
||||||
|
await channel.send(file=icon_file, embed=embed)
|
||||||
|
logger.debug(f"📤 Sent embed with icon to #{channel.name}")
|
||||||
|
else:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
logger.debug(f"📤 Sent text-only embed to #{channel.name}")
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ Could not find Discord channel for {rare_type} rares")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error posting to Discord: {e}")
|
||||||
|
|
||||||
|
async def post_chat_to_discord(self, data: dict):
|
||||||
|
"""Post chat message to AC Log Discord channel."""
|
||||||
|
try:
|
||||||
|
character_name = data.get('character_name', 'Unknown Character')
|
||||||
|
chat_text = data.get('text', '')
|
||||||
|
timestamp_str = data.get('timestamp', '')
|
||||||
|
|
||||||
|
# Parse timestamp
|
||||||
|
try:
|
||||||
|
if timestamp_str:
|
||||||
|
if timestamp_str.endswith('Z'):
|
||||||
|
timestamp = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
||||||
|
else:
|
||||||
|
timestamp = datetime.fromisoformat(timestamp_str)
|
||||||
|
else:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
except ValueError:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
|
||||||
|
# Create simple message format similar to your old bot
|
||||||
|
time_str = timestamp.strftime("%H:%M:%S")
|
||||||
|
message_content = f"`{time_str}` **{character_name}**: {chat_text}"
|
||||||
|
|
||||||
|
# Get AC Log channel
|
||||||
|
channel = self.client.get_channel(ACLOG_CHANNEL_ID)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
await channel.send(message_content)
|
||||||
|
logger.debug(f"📤 Posted chat to #{channel.name}: {character_name}")
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ Could not find AC Log channel ID: {ACLOG_CHANNEL_ID}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error posting chat to Discord: {e}")
|
||||||
|
|
||||||
|
async def post_rare_debug_to_aclog(self, rare_name: str, rare_type: str, character_name: str):
|
||||||
|
"""Post rare classification debug info to AC Log channel."""
|
||||||
|
try:
|
||||||
|
# Create debug message showing classification
|
||||||
|
debug_message = f"🔍 **RARE DEBUG**: `{rare_name}` → **{rare_type.upper()}** (found by {character_name})"
|
||||||
|
|
||||||
|
# Get AC Log channel
|
||||||
|
channel = self.client.get_channel(ACLOG_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 AC Log channel for debug: {ACLOG_CHANNEL_ID}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error posting rare debug to Discord: {e}")
|
||||||
|
|
||||||
|
async def post_status_to_aclog(self, status_message: str):
|
||||||
|
"""Post status update to AC Log channel."""
|
||||||
|
try:
|
||||||
|
# Create status message with timestamp
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
message = f"`{timestamp}` **BOT STATUS**: {status_message}"
|
||||||
|
|
||||||
|
# Get AC Log channel
|
||||||
|
channel = self.client.get_channel(ACLOG_CHANNEL_ID)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
await channel.send(message)
|
||||||
|
logger.debug(f"📤 Posted status to #{channel.name}: {status_message}")
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ Could not find AC Log channel for status: {ACLOG_CHANNEL_ID}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error posting status to Discord: {e}")
|
||||||
|
|
||||||
|
async def handle_icons_command(self, message):
|
||||||
|
"""Handle !icons all command to display 10 random rare icons with images."""
|
||||||
|
try:
|
||||||
|
logger.info("🎯 STARTING handle_icons_command - will send 10 separate messages")
|
||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
|
||||||
|
# Get all icon files
|
||||||
|
all_icons = [f for f in os.listdir(icons_dir) if f.endswith("_Icon.png")]
|
||||||
|
logger.info(f"📂 Found {len(all_icons)} total icons")
|
||||||
|
|
||||||
|
# Select 10 random icons
|
||||||
|
random_icons = random.sample(all_icons, min(10, len(all_icons)))
|
||||||
|
logger.info(f"🎲 Selected {len(random_icons)} random icons to display")
|
||||||
|
|
||||||
|
# Send each icon individually - no embeds, just file + text
|
||||||
|
for i, filename in enumerate(random_icons):
|
||||||
|
# Convert filename to display name
|
||||||
|
base_name = filename[:-9]
|
||||||
|
display_name = base_name.replace("_", " ")
|
||||||
|
display_name = display_name.replace("s Crystal", "'s Crystal")
|
||||||
|
display_name = display_name.replace("s Pearl", "'s Pearl")
|
||||||
|
display_name = display_name.replace("s Jewel", "'s Jewel")
|
||||||
|
|
||||||
|
# Classify rare
|
||||||
|
rare_type = "common" if self.classify_rare(display_name) == "common" else "great"
|
||||||
|
emoji = "🔸" if rare_type == "common" else "💎"
|
||||||
|
|
||||||
|
# Send just the file with simple text
|
||||||
|
icon_path = os.path.join(icons_dir, filename)
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
file = discord.File(icon_path, filename=filename)
|
||||||
|
text = f"{emoji} **{display_name}** ({rare_type} rare)"
|
||||||
|
|
||||||
|
logger.info(f"📤 Sending message {i+1}/10: {display_name}")
|
||||||
|
await message.channel.send(content=text, file=file)
|
||||||
|
|
||||||
|
# Small delay between messages to avoid rate limiting
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
|
||||||
|
# Send summary message
|
||||||
|
await message.channel.send(f"📚 Displayed 10 random icons from {len(all_icons)} total rare icons. Use `!icons [name]` to search.")
|
||||||
|
|
||||||
|
logger.info(f"📚 Sent 10 random icon samples to {message.author} in #{message.channel}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling !icons command: {e}")
|
||||||
|
await message.channel.send(f"❌ Error displaying icons: {str(e)}")
|
||||||
|
|
||||||
|
async def handle_icons_summary(self, message):
|
||||||
|
"""Handle !icons command to display summary with example images."""
|
||||||
|
try:
|
||||||
|
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
|
||||||
|
# Count icons by category and get examples
|
||||||
|
common_count = 0
|
||||||
|
great_count = 0
|
||||||
|
icon_files = []
|
||||||
|
common_examples = []
|
||||||
|
great_examples = []
|
||||||
|
|
||||||
|
for filename in os.listdir(icons_dir):
|
||||||
|
if filename.endswith("_Icon.png"):
|
||||||
|
base_name = filename[:-9]
|
||||||
|
display_name = base_name.replace("_", " ")
|
||||||
|
# Fix common patterns
|
||||||
|
display_name = display_name.replace("s Crystal", "'s Crystal")
|
||||||
|
display_name = display_name.replace("s Pearl", "'s Pearl")
|
||||||
|
display_name = display_name.replace("s Jewel", "'s Jewel")
|
||||||
|
|
||||||
|
icon_files.append((filename, display_name))
|
||||||
|
|
||||||
|
# Classify and collect examples
|
||||||
|
if self.classify_rare(display_name) == "common":
|
||||||
|
common_count += 1
|
||||||
|
if len(common_examples) < 3:
|
||||||
|
common_examples.append((filename, display_name))
|
||||||
|
else:
|
||||||
|
great_count += 1
|
||||||
|
if len(great_examples) < 3:
|
||||||
|
great_examples.append((filename, display_name))
|
||||||
|
|
||||||
|
total_count = len(icon_files)
|
||||||
|
|
||||||
|
# Create summary embed with thumbnail
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="📚 Rare Icons Library Summary",
|
||||||
|
description=f"Complete collection of Asheron's Call rare item icons\n\n**{total_count}** total icons ({common_count} common, {great_count} great)",
|
||||||
|
color=discord.Color.gold()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add example icons text
|
||||||
|
examples_text = "**Common Rare Examples:**\n"
|
||||||
|
for _, name in common_examples:
|
||||||
|
examples_text += f"🔸 {name}\n"
|
||||||
|
|
||||||
|
examples_text += "\n**Great Rare Examples:**\n"
|
||||||
|
for _, name in great_examples:
|
||||||
|
examples_text += f"💎 {name}\n"
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="📋 Icon Examples",
|
||||||
|
value=examples_text,
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="💡 Commands",
|
||||||
|
value="`!icons all` - Browse all icons with images\n`!icons [item_name]` - Search for specific item\n`!icons grid` - View icon grids",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_footer(text=f"Coverage: {(total_count/293)*100:.1f}% of all known rares")
|
||||||
|
|
||||||
|
# Select a representative icon for thumbnail (first great rare)
|
||||||
|
if great_examples:
|
||||||
|
thumbnail_file = great_examples[0][0]
|
||||||
|
thumbnail_path = os.path.join(icons_dir, thumbnail_file)
|
||||||
|
if os.path.exists(thumbnail_path):
|
||||||
|
file = discord.File(thumbnail_path, filename=thumbnail_file)
|
||||||
|
embed.set_thumbnail(url=f"attachment://{thumbnail_file}")
|
||||||
|
await message.channel.send(file=file, embed=embed)
|
||||||
|
else:
|
||||||
|
await message.channel.send(embed=embed)
|
||||||
|
else:
|
||||||
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
|
logger.info(f"📊 Sent icon summary with thumbnail to {message.author} in #{message.channel}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling !icons summary: {e}")
|
||||||
|
await message.channel.send(f"❌ Error displaying icon summary: {str(e)}")
|
||||||
|
|
||||||
|
async def handle_icons_search(self, message, search_term):
|
||||||
|
"""Handle !icons [item_name] to search for specific rare item."""
|
||||||
|
try:
|
||||||
|
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||||
|
|
||||||
|
# Load icon mapping
|
||||||
|
icon_mapping = {}
|
||||||
|
for filename in os.listdir(icons_dir):
|
||||||
|
if filename.endswith("_Icon.png"):
|
||||||
|
base_name = filename[:-9]
|
||||||
|
display_name = base_name.replace("_", " ")
|
||||||
|
# Fix common patterns
|
||||||
|
display_name = display_name.replace("s Crystal", "'s Crystal")
|
||||||
|
display_name = display_name.replace("s Pearl", "'s Pearl")
|
||||||
|
display_name = display_name.replace("s Jewel", "'s Jewel")
|
||||||
|
display_name = display_name.replace("s Breath", "'s Breath")
|
||||||
|
display_name = display_name.replace("s Glaive", "'s Glaive")
|
||||||
|
display_name = display_name.replace("s Grip", "'s Grip")
|
||||||
|
display_name = display_name.replace("Tri Blade", "Tri-Blade")
|
||||||
|
display_name = display_name.replace("T ing", "T'ing")
|
||||||
|
if "Renari" in display_name:
|
||||||
|
display_name = display_name.replace("Renaris", "Renari's")
|
||||||
|
if "Leikotha" in display_name:
|
||||||
|
display_name = display_name.replace("Leikothas", "Leikotha's")
|
||||||
|
icon_mapping[filename] = display_name
|
||||||
|
|
||||||
|
# Search for matching items (case-insensitive, partial match)
|
||||||
|
search_lower = search_term.lower()
|
||||||
|
matches = []
|
||||||
|
for filename, display_name in icon_mapping.items():
|
||||||
|
if search_lower in display_name.lower():
|
||||||
|
matches.append((filename, display_name))
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
await message.channel.send(f"❌ No rare items found matching '{search_term}'. Try a different search term.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sort matches by relevance (exact match first, then starts with, then contains)
|
||||||
|
def match_score(item):
|
||||||
|
name = item[1].lower()
|
||||||
|
if name == search_lower:
|
||||||
|
return 0 # Exact match
|
||||||
|
elif name.startswith(search_lower):
|
||||||
|
return 1 # Starts with
|
||||||
|
else:
|
||||||
|
return 2 # Contains
|
||||||
|
|
||||||
|
matches.sort(key=match_score)
|
||||||
|
|
||||||
|
# Limit to first 10 matches to avoid spam
|
||||||
|
matches = matches[:10]
|
||||||
|
|
||||||
|
if len(matches) == 1:
|
||||||
|
# Single match - show full detail with large image
|
||||||
|
filename, display_name = matches[0]
|
||||||
|
icon_path = os.path.join(icons_dir, filename)
|
||||||
|
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
# Get file stats
|
||||||
|
file_size = os.path.getsize(icon_path) / 1024
|
||||||
|
rare_type = "common" if self.classify_rare(display_name) == "common" else "great"
|
||||||
|
emoji = "🔸" if rare_type == "common" else "💎"
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{emoji} {display_name}",
|
||||||
|
description=f"**Type:** {rare_type.title()} Rare\n**File:** `{filename}`\n**Size:** {file_size:.1f} KB",
|
||||||
|
color=discord.Color.blue() if rare_type == "common" else discord.Color.gold()
|
||||||
|
)
|
||||||
|
|
||||||
|
file = discord.File(icon_path, filename=filename)
|
||||||
|
embed.set_image(url=f"attachment://{filename}")
|
||||||
|
|
||||||
|
await message.channel.send(file=file, embed=embed)
|
||||||
|
logger.info(f"🔍 Sent single icon result for '{search_term}' to {message.author}")
|
||||||
|
else:
|
||||||
|
await message.channel.send(f"❌ Icon file not found for {display_name}")
|
||||||
|
else:
|
||||||
|
# Multiple matches - show list with thumbnails
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🔍 Search Results for '{search_term}'",
|
||||||
|
description=f"Found {len(matches)} matching rare items:",
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add matches to embed
|
||||||
|
for i, (filename, display_name) in enumerate(matches):
|
||||||
|
rare_type = "common" if self.classify_rare(display_name) == "common" else "great"
|
||||||
|
emoji = "🔸" if rare_type == "common" else "💎"
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{emoji} {display_name}",
|
||||||
|
value=f"{rare_type.title()} rare",
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach first 5 images
|
||||||
|
files = []
|
||||||
|
for i in range(min(5, len(matches))):
|
||||||
|
filename, _ = matches[i]
|
||||||
|
icon_path = os.path.join(icons_dir, filename)
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
files.append(discord.File(icon_path, filename=filename))
|
||||||
|
|
||||||
|
if files:
|
||||||
|
embed.add_field(
|
||||||
|
name="🖼️ Preview Images",
|
||||||
|
value=f"First {len(files)} results shown as attachments",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
await message.channel.send(files=files, embed=embed)
|
||||||
|
else:
|
||||||
|
await message.channel.send(embed=embed)
|
||||||
|
|
||||||
|
logger.info(f"🔍 Sent {len(matches)} search results for '{search_term}' to {message.author}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling !icons search: {e}")
|
||||||
|
await message.channel.send(f"❌ Error searching icons: {str(e)}")
|
||||||
|
|
||||||
|
async def handle_icons_grid(self, message):
|
||||||
|
"""Handle !icons grid to show icon grid compositions."""
|
||||||
|
await message.channel.send("🚧 **Grid View Coming Soon!**\n\nThis feature will show multiple icons arranged in grids. For now, use:\n• `!icons` - Summary with examples\n• `!icons all` - Browse all icons\n• `!icons [name]` - Search specific items")
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the Discord bot."""
|
||||||
|
if not DISCORD_TOKEN:
|
||||||
|
logger.error("❌ DISCORD_RARE_BOT_TOKEN environment variable not set")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("🚀 Starting Discord Rare Monitor Bot...")
|
||||||
|
await self.client.start(DISCORD_TOKEN)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to start Discord bot: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the Discord bot and cleanup."""
|
||||||
|
logger.info("🛑 Stopping Discord Rare Monitor Bot...")
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
if self.websocket_task:
|
||||||
|
self.websocket_task.cancel()
|
||||||
|
try:
|
||||||
|
await self.websocket_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not self.client.is_closed():
|
||||||
|
await self.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
# Validate required environment variables
|
||||||
|
if not DISCORD_TOKEN:
|
||||||
|
logger.error("❌ Missing required environment variable: DISCORD_RARE_BOT_TOKEN")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Log configuration
|
||||||
|
actual_log_level = logging.getLevelName(logger.getEffectiveLevel())
|
||||||
|
logger.info("🔧 Discord Rare Monitor Configuration:")
|
||||||
|
logger.info(f" WebSocket URL: {WEBSOCKET_URL}")
|
||||||
|
logger.info(f" Monitor Character: {MONITOR_CHARACTER}")
|
||||||
|
logger.info(f" AC Log Channel ID: {ACLOG_CHANNEL_ID}")
|
||||||
|
logger.info(f" Common Rare Channel ID: {COMMON_RARE_CHANNEL_ID}")
|
||||||
|
logger.info(f" Great Rare Channel ID: {GREAT_RARE_CHANNEL_ID}")
|
||||||
|
logger.info(f" Log Level: {actual_log_level} (ENV: {log_level})")
|
||||||
|
|
||||||
|
# Create and start bot
|
||||||
|
bot = DiscordRareMonitor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.start()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("🛑 Received keyboard interrupt")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Unexpected error: {e}")
|
||||||
|
finally:
|
||||||
|
await bot.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
55
discord-rare-monitor/icon_mapping.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate mapping between icon filenames and rare item names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
def generate_icon_mapping():
|
||||||
|
"""Generate mapping from icon filenames to display names."""
|
||||||
|
icons_dir = "/home/erik/MosswartOverlord/discord-rare-monitor/icons"
|
||||||
|
|
||||||
|
# Create reverse mapping from filename to display name
|
||||||
|
icon_mapping = {}
|
||||||
|
|
||||||
|
# List all PNG files in the icons directory
|
||||||
|
for filename in os.listdir(icons_dir):
|
||||||
|
if filename.endswith("_Icon.png"):
|
||||||
|
# Convert filename back to display name
|
||||||
|
# Remove _Icon.png suffix
|
||||||
|
base_name = filename[:-9]
|
||||||
|
|
||||||
|
# Convert underscores to spaces and handle apostrophes
|
||||||
|
display_name = base_name.replace("_", " ")
|
||||||
|
|
||||||
|
# Fix common patterns
|
||||||
|
display_name = display_name.replace("s Crystal", "'s Crystal")
|
||||||
|
display_name = display_name.replace("s Pearl", "'s Pearl")
|
||||||
|
display_name = display_name.replace("s Jewel", "'s Jewel")
|
||||||
|
display_name = display_name.replace("s Breath", "'s Breath")
|
||||||
|
display_name = display_name.replace("s Glaive", "'s Glaive")
|
||||||
|
display_name = display_name.replace("s Grip", "'s Grip")
|
||||||
|
display_name = display_name.replace("Tri Blade", "Tri-Blade")
|
||||||
|
display_name = display_name.replace("T ing", "T'ing")
|
||||||
|
|
||||||
|
# Special cases
|
||||||
|
if "Renari" in display_name:
|
||||||
|
display_name = display_name.replace("Renaris", "Renari's")
|
||||||
|
if "Leikotha" in display_name:
|
||||||
|
display_name = display_name.replace("Leikothas", "Leikotha's")
|
||||||
|
|
||||||
|
icon_mapping[filename] = display_name
|
||||||
|
|
||||||
|
# Save mapping to JSON file
|
||||||
|
with open(os.path.join(os.path.dirname(icons_dir), "icon_name_mapping.json"), "w") as f:
|
||||||
|
json.dump(icon_mapping, f, indent=2, sort_keys=True)
|
||||||
|
|
||||||
|
return icon_mapping
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mapping = generate_icon_mapping()
|
||||||
|
print(f"Generated mapping for {len(mapping)} icons")
|
||||||
|
print("\nFirst 10 mappings:")
|
||||||
|
for i, (filename, display_name) in enumerate(list(mapping.items())[:10]):
|
||||||
|
print(f" {filename} -> {display_name}")
|
||||||
294
discord-rare-monitor/icon_name_mapping.json
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
{
|
||||||
|
"Adepts_Fervor_Icon.png": "Adepts Fervor",
|
||||||
|
"Adherents_Crystal_Icon.png": "Adherent's Crystal",
|
||||||
|
"Alchemists_Crystal_Icon.png": "Alchemist's Crystal",
|
||||||
|
"Aquamarine_Foolproof_Icon.png": "Aquamarine Foolproof",
|
||||||
|
"Archers_Jewel_Icon.png": "Archer's Jewel",
|
||||||
|
"Aristocrats_Bracelet_Icon.png": "Aristocrats Bracelet",
|
||||||
|
"Artificers_Crystal_Icon.png": "Artificer's Crystal",
|
||||||
|
"Artists_Crystal_Icon.png": "Artist's Crystal",
|
||||||
|
"Assassins_Whisper_Icon.png": "Assassins Whisper",
|
||||||
|
"Astyrrians_Jewel_Icon.png": "Astyrrian's Jewel",
|
||||||
|
"Band_of_Elemental_Harmony_Icon.png": "Band of Elemental Harmony",
|
||||||
|
"Baton_of_Tirethas_Icon.png": "Baton of Tirethas",
|
||||||
|
"Bearded_Axe_of_Souia-Vey_Icon.png": "Bearded Axe of Souia-Vey",
|
||||||
|
"Ben_Tens_Crystal_Icon.png": "Ben Ten's Crystal",
|
||||||
|
"Berzerkers_Crystal_Icon.png": "Berzerker's Crystal",
|
||||||
|
"Black_Cloud_Bow_Icon.png": "Black Cloud Bow",
|
||||||
|
"Black_Garnet_Foolproof_Icon.png": "Black Garnet Foolproof",
|
||||||
|
"Black_Opal_Foolproof_Icon.png": "Black Opal Foolproof",
|
||||||
|
"Black_Thistle_Icon.png": "Black Thistle",
|
||||||
|
"Bloodmark_Crossbow_Icon.png": "Bloodmark Crossbow",
|
||||||
|
"Bracelet_of_Binding_Icon.png": "Bracelet of Binding",
|
||||||
|
"Bracers_of_Leikothas_Tears_Icon.png": "Bracers of Leikotha's Tears",
|
||||||
|
"Bradors_Frozen_Eye_Icon.png": "Bradors Frozen Eye",
|
||||||
|
"Brawlers_Crystal_Icon.png": "Brawler's Crystal",
|
||||||
|
"Breastplate_of_Leikothas_Tears_Icon.png": "Breastplate of Leikotha's Tears",
|
||||||
|
"Canfield_Cleaver_Icon.png": "Canfield Cleaver",
|
||||||
|
"Casino_Exquisite_Keyring_Icon.png": "Casino Exquisite Keyring",
|
||||||
|
"Champions_Demise_Icon.png": "Champions Demise",
|
||||||
|
"Chefs_Crystal_Icon.png": "Chef's Crystal",
|
||||||
|
"Chitin_Cracker_Icon.png": "Chitin Cracker",
|
||||||
|
"Circle_of_Pure_Thought_Icon.png": "Circle of Pure Thought",
|
||||||
|
"Converters_Crystal_Icon.png": "Converter's Crystal",
|
||||||
|
"Corruptors_Crystal_Icon.png": "Corruptor's Crystal",
|
||||||
|
"Corsairs_Arc_Icon.png": "Corsairs Arc",
|
||||||
|
"Count_Renaris_Equalizer_Icon.png": "Count Renari's Equalizer",
|
||||||
|
"Dart_Flicker_Icon.png": "Dart Flicker",
|
||||||
|
"Deaths_Grip_Staff_Icon.png": "Death's Grip Staff",
|
||||||
|
"Decapitators_Blade_Icon.png": "Decapitators Blade",
|
||||||
|
"Deceivers_Crystal_Icon.png": "Deceiver's Crystal",
|
||||||
|
"Defiler_of_Milantos_Icon.png": "Defiler of Milantos",
|
||||||
|
"Deru_Limb_Icon.png": "Deru Limb",
|
||||||
|
"Desert_Wyrm_Icon.png": "Desert Wyrm",
|
||||||
|
"Dodgers_Crystal_Icon.png": "Dodger's Crystal",
|
||||||
|
"Dragonspine_Bow_Icon.png": "Dragonspine Bow",
|
||||||
|
"Dread_Marauder_Shield_Icon.png": "Dread Marauder Shield",
|
||||||
|
"Dreamseer_Bangle_Icon.png": "Dreamseer Bangle",
|
||||||
|
"Drifters_Atlatl_Icon.png": "Drifters Atlatl",
|
||||||
|
"Dripping_Death_Icon.png": "Dripping Death",
|
||||||
|
"Duelists_Jewel_Icon.png": "Duelist's Jewel",
|
||||||
|
"Dusk_Coat_Icon.png": "Dusk Coat",
|
||||||
|
"Dusk_Leggings_Icon.png": "Dusk Leggings",
|
||||||
|
"Ebonwood_Shortbow_Icon.png": "Ebonwood Shortbow",
|
||||||
|
"Elysas_Crystal_Icon.png": "Elysa's Crystal",
|
||||||
|
"Emerald_Foolproof_Icon.png": "Emerald Foolproof",
|
||||||
|
"Enchanters_Crystal_Icon.png": "Enchanter's Crystal",
|
||||||
|
"Eternal_Health_Kit_Icon.png": "Eternal Health Kit",
|
||||||
|
"Eternal_Mana_Charge_Icon.png": "Eternal Mana Charge",
|
||||||
|
"Eternal_Mana_Kit_Icon.png": "Eternal Mana Kit",
|
||||||
|
"Eternal_Stamina_Kit_Icon.png": "Eternal Stamina Kit",
|
||||||
|
"Evaders_Crystal_Icon.png": "Evader's Crystal",
|
||||||
|
"Executors_Jewel_Icon.png": "Executor's Jewel",
|
||||||
|
"Eye_of_Muramm_Icon.png": "Eye of Muramm",
|
||||||
|
"Feathered_Razor_Icon.png": "Feathered Razor",
|
||||||
|
"Fire_Opal_Foolproof_Icon.png": "Fire Opal Foolproof",
|
||||||
|
"Fist_of_Three_Principles_Icon.png": "Fist of Three Principles",
|
||||||
|
"Fletchers_Crystal_Icon.png": "Fletcher's Crystal",
|
||||||
|
"Footmans_Boots_Icon.png": "Footmans Boots",
|
||||||
|
"Gauntlets_of_Leikothas_Tears_Icon.png": "Gauntlets of Leikotha's Tears",
|
||||||
|
"Gauntlets_of_the_Crimson_Star_Icon.png": "Gauntlets of the Crimson Star",
|
||||||
|
"Gelidite_Boots_Icon.png": "Gelidite Boots",
|
||||||
|
"Gelidite_Bracers_Icon.png": "Gelidite Bracers",
|
||||||
|
"Gelidite_Breastplate_Icon.png": "Gelidite Breastplate",
|
||||||
|
"Gelidite_Gauntlets_Icon.png": "Gelidite Gauntlets",
|
||||||
|
"Gelidite_Girth_Icon.png": "Gelidite Girth",
|
||||||
|
"Gelidite_Greaves_Icon.png": "Gelidite Greaves",
|
||||||
|
"Gelidite_Mitre_Icon.png": "Gelidite Mitre",
|
||||||
|
"Gelidite_Pauldrons_Icon.png": "Gelidite Pauldrons",
|
||||||
|
"Gelidite_Tassets_Icon.png": "Gelidite Tassets",
|
||||||
|
"Gelids_Jewel_Icon.png": "Gelid's Jewel",
|
||||||
|
"Girth_of_Leikothas_Tears_Icon.png": "Girth of Leikotha's Tears",
|
||||||
|
"Golden_Snake_Choker_Icon.png": "Golden Snake Choker",
|
||||||
|
"Greaves_of_Leikothas_Tears_Icon.png": "Greaves of Leikotha's Tears",
|
||||||
|
"Guardian_of_Pwyll_Icon.png": "Guardian of Pwyll",
|
||||||
|
"Heart_of_Darkest_Flame_Icon.png": "Heart of Darkest Flame",
|
||||||
|
"Helm_of_Leikothas_Tears_Icon.png": "Helm of Leikotha's Tears",
|
||||||
|
"Hevelios_Half-Moon_Icon.png": "Hevelios Half-Moon",
|
||||||
|
"Hieroglyph_of_Alchemy_Mastery_Icon.png": "Hieroglyph of Alchemy Mastery",
|
||||||
|
"Hieroglyph_of_Arcane_Enlightenment_Icon.png": "Hieroglyph of Arcane Enlightenment",
|
||||||
|
"Hieroglyph_of_Armor_Tinkering_Expertise_Icon.png": "Hieroglyph of Armor Tinkering Expertise",
|
||||||
|
"Hieroglyph_of_Cooking_Mastery_Icon.png": "Hieroglyph of Cooking Mastery",
|
||||||
|
"Hieroglyph_of_Creature_Enchantment_Mastery_Icon.png": "Hieroglyph of Creature Enchantment Mastery",
|
||||||
|
"Hieroglyph_of_Deception_Mastery_Icon.png": "Hieroglyph of Deception Mastery",
|
||||||
|
"Hieroglyph_of_Dirty_Fighting_Mastery_Icon.png": "Hieroglyph of Dirty Fighting Mastery",
|
||||||
|
"Hieroglyph_of_Dual_Wield_Mastery_Icon.png": "Hieroglyph of Dual Wield Mastery",
|
||||||
|
"Hieroglyph_of_Fealty_Icon.png": "Hieroglyph of Fealty",
|
||||||
|
"Hieroglyph_of_Finesse_Weapon_Mastery_Icon.png": "Hieroglyph of Finesse Weapon Mastery",
|
||||||
|
"Hieroglyph_of_Fletching_Mastery_Icon.png": "Hieroglyph of Fletching Mastery",
|
||||||
|
"Hieroglyph_of_Healing_Mastery_Icon.png": "Hieroglyph of Healing Mastery",
|
||||||
|
"Hieroglyph_of_Heavy_Weapon_Mastery_Icon.png": "Hieroglyph of Heavy Weapon Mastery",
|
||||||
|
"Hieroglyph_of_Impregnability_Icon.png": "Hieroglyph of Impregnability",
|
||||||
|
"Hieroglyph_of_Invulnerability_Icon.png": "Hieroglyph of Invulnerability",
|
||||||
|
"Hieroglyph_of_Item_Enchantment_Mastery_Icon.png": "Hieroglyph of Item Enchantment Mastery",
|
||||||
|
"Hieroglyph_of_Item_Tinkering_Expertise_Icon.png": "Hieroglyph of Item Tinkering Expertise",
|
||||||
|
"Hieroglyph_of_Jumping_Mastery_Icon.png": "Hieroglyph of Jumping Mastery",
|
||||||
|
"Hieroglyph_of_Leadership_Mastery_Icon.png": "Hieroglyph of Leadership Mastery",
|
||||||
|
"Hieroglyph_of_Life_Magic_Mastery_Icon.png": "Hieroglyph of Life Magic Mastery",
|
||||||
|
"Hieroglyph_of_Light_Weapon_Mastery_Icon.png": "Hieroglyph of Light Weapon Mastery",
|
||||||
|
"Hieroglyph_of_Lockpick_Mastery_Icon.png": "Hieroglyph of Lockpick Mastery",
|
||||||
|
"Hieroglyph_of_Magic_Item_Tinkering_Expertise_Icon.png": "Hieroglyph of Magic Item Tinkering Expertise",
|
||||||
|
"Hieroglyph_of_Magic_Resistance_Icon.png": "Hieroglyph of Magic Resistance",
|
||||||
|
"Hieroglyph_of_Mana_Conversion_Mastery_Icon.png": "Hieroglyph of Mana Conversion Mastery",
|
||||||
|
"Hieroglyph_of_Missile_Weapon_Mastery_Icon.png": "Hieroglyph of Missile Weapon Mastery",
|
||||||
|
"Hieroglyph_of_Monster_Attunement_Icon.png": "Hieroglyph of Monster Attunement",
|
||||||
|
"Hieroglyph_of_Person_Attunement_Icon.png": "Hieroglyph of Person Attunement",
|
||||||
|
"Hieroglyph_of_Recklessness_Mastery_Icon.png": "Hieroglyph of Recklessness Mastery",
|
||||||
|
"Hieroglyph_of_Shield_Mastery_Icon.png": "Hieroglyph of Shield Mastery",
|
||||||
|
"Hieroglyph_of_Sneak_Attack_Mastery_Icon.png": "Hieroglyph of Sneak Attack Mastery",
|
||||||
|
"Hieroglyph_of_Sprint_Icon.png": "Hieroglyph of Sprint",
|
||||||
|
"Hieroglyph_of_Two_Handed_Weapons_Mastery_Icon.png": "Hieroglyph of Two Handed Weapons Mastery",
|
||||||
|
"Hieroglyph_of_Void_Magic_Mastery_Icon.png": "Hieroglyph of Void Magic Mastery",
|
||||||
|
"Hieroglyph_of_War_Magic_Mastery_Icon.png": "Hieroglyph of War Magic Mastery",
|
||||||
|
"Hieroglyph_of_Weapon_Tinkering_Expertise_Icon.png": "Hieroglyph of Weapon Tinkering Expertise",
|
||||||
|
"Hieromancers_Crystal_Icon.png": "Hieromancer's Crystal",
|
||||||
|
"Hooded_Serpent_Slinger_Icon.png": "Hooded Serpent Slinger",
|
||||||
|
"Hunters_Crystal_Icon.png": "Hunter's Crystal",
|
||||||
|
"Huntsmans_Dart-Thrower_Icon.png": "Huntsmans Dart-Thrower",
|
||||||
|
"Ibriyas_Choice_Icon.png": "Ibriyas Choice",
|
||||||
|
"Ideograph_of_Acid_Protection_Icon.png": "Ideograph of Acid Protection",
|
||||||
|
"Ideograph_of_Armor_Icon.png": "Ideograph of Armor",
|
||||||
|
"Ideograph_of_Blade_Protection_Icon.png": "Ideograph of Blade Protection",
|
||||||
|
"Ideograph_of_Bludgeoning_Protection_Icon.png": "Ideograph of Bludgeoning Protection",
|
||||||
|
"Ideograph_of_Fire_Protection_Icon.png": "Ideograph of Fire Protection",
|
||||||
|
"Ideograph_of_Frost_Protection_Icon.png": "Ideograph of Frost Protection",
|
||||||
|
"Ideograph_of_Lightning_Protection_Icon.png": "Ideograph of Lightning Protection",
|
||||||
|
"Ideograph_of_Mana_Renewal_Icon.png": "Ideograph of Mana Renewal",
|
||||||
|
"Ideograph_of_Piercing_Protection_Icon.png": "Ideograph of Piercing Protection",
|
||||||
|
"Ideograph_of_Regeneration_Icon.png": "Ideograph of Regeneration",
|
||||||
|
"Ideograph_of_Revitalization_Icon.png": "Ideograph of Revitalization",
|
||||||
|
"Imbuers_Crystal_Icon.png": "Imbuer's Crystal",
|
||||||
|
"Imperial_Chevairds_Helm_Icon.png": "Imperial Chevairds Helm",
|
||||||
|
"Imperial_Topaz_Foolproof_Icon.png": "Imperial Topaz Foolproof",
|
||||||
|
"Infernos_Jewel_Icon.png": "Inferno's Jewel",
|
||||||
|
"Infinite_Deadly_Acid_Arrowheads_Icon.png": "Infinite Deadly Acid Arrowheads",
|
||||||
|
"Infinite_Deadly_Armor_Piercing_Arrowheads_Icon.png": "Infinite Deadly Armor Piercing Arrowheads",
|
||||||
|
"Infinite_Deadly_Blunt_Arrowheads_Icon.png": "Infinite Deadly Blunt Arrowheads",
|
||||||
|
"Infinite_Deadly_Broad_Arrowheads_Icon.png": "Infinite Deadly Broad Arrowheads",
|
||||||
|
"Infinite_Deadly_Electric_Arrowheads_Icon.png": "Infinite Deadly Electric Arrowheads",
|
||||||
|
"Infinite_Deadly_Fire_Arrowheads_Icon.png": "Infinite Deadly Fire Arrowheads",
|
||||||
|
"Infinite_Deadly_Frog_Crotch_Arrowheads_Icon.png": "Infinite Deadly Frog Crotch Arrowheads",
|
||||||
|
"Infinite_Deadly_Frost_Arrowheads_Icon.png": "Infinite Deadly Frost Arrowheads",
|
||||||
|
"Infinite_Elaborate_Dried_Rations_Icon.png": "Infinite Elaborate Dried Rations",
|
||||||
|
"Infinite_Ivory_Icon.png": "Infinite Ivory",
|
||||||
|
"Infinite_Leather_Icon.png": "Infinite Leather",
|
||||||
|
"Infinite_Simple_Dried_Rations_Icon.png": "Infinite Simple Dried Rations",
|
||||||
|
"Invigorating_Elixir_Icon.png": "Invigorating Elixir",
|
||||||
|
"Iron_Bull_Icon.png": "Iron Bull",
|
||||||
|
"Itakas_Naginata_Icon.png": "Itakas Naginata",
|
||||||
|
"Jet_Foolproof_Icon.png": "Jet Foolproof",
|
||||||
|
"Lichs_Pearl_Icon.png": "Lich's Pearl",
|
||||||
|
"Life_Givers_Crystal_Icon.png": "Life Giver's Crystal",
|
||||||
|
"Limitless_Lockpick_Icon.png": "Limitless Lockpick",
|
||||||
|
"Loop_of_Opposing_Benedictions_Icon.png": "Loop of Opposing Benedictions",
|
||||||
|
"Loves_Favor_Icon.png": "Loves Favor",
|
||||||
|
"Lugians_Pearl_Icon.png": "Lugian's Pearl",
|
||||||
|
"Mages_Jewel_Icon.png": "Mage's Jewel",
|
||||||
|
"Maguss_Pearl_Icon.png": "Magus's Pearl",
|
||||||
|
"Malachite_Slasher_Icon.png": "Malachite Slasher",
|
||||||
|
"Medicated_Health_Kit_Icon.png": "Medicated Health Kit",
|
||||||
|
"Medicated_Mana_Kit_Icon.png": "Medicated Mana Kit",
|
||||||
|
"Medicated_Stamina_Kit_Icon.png": "Medicated Stamina Kit",
|
||||||
|
"Melees_Jewel_Icon.png": "Melee's Jewel",
|
||||||
|
"Miraculous_Elixir_Icon.png": "Miraculous Elixir",
|
||||||
|
"Mirrored_Justice_Icon.png": "Mirrored Justice",
|
||||||
|
"Monarchs_Crystal_Icon.png": "Monarch's Crystal",
|
||||||
|
"Moriharus_Kitchen_Knife_Icon.png": "Moriharus Kitchen Knife",
|
||||||
|
"Morrigans_Vanity_Icon.png": "Morrigans Vanity",
|
||||||
|
"Necklace_of_Iniquity_Icon.png": "Necklace of Iniquity",
|
||||||
|
"Observers_Crystal_Icon.png": "Observer's Crystal",
|
||||||
|
"Olthois_Jewel_Icon.png": "Olthoi's Jewel",
|
||||||
|
"Orb_of_the_Ironsea_Icon.png": "Orb of the Ironsea",
|
||||||
|
"Oswalds_Crystal_Icon.png": "Oswald's Crystal",
|
||||||
|
"Patriarchs_Twilight_Coat_Icon.png": "Patriarchs Twilight Coat",
|
||||||
|
"Patriarchs_Twilight_Tights_Icon.png": "Patriarchs Twilight Tights",
|
||||||
|
"Pauldrons_of_Leikothas_Tears_Icon.png": "Pauldrons of Leikotha's Tears",
|
||||||
|
"Pearl_of_Acid_Baning_Icon.png": "Pearl of Acid Baning",
|
||||||
|
"Pearl_of_Blade_Baning_Icon.png": "Pearl of Blade Baning",
|
||||||
|
"Pearl_of_Blood_Drinking_Icon.png": "Pearl of Blood Drinking",
|
||||||
|
"Pearl_of_Bludgeon_Baning_Icon.png": "Pearl of Bludgeon Baning",
|
||||||
|
"Pearl_of_Defending_Icon.png": "Pearl of Defending",
|
||||||
|
"Pearl_of_Flame_Baning_Icon.png": "Pearl of Flame Baning",
|
||||||
|
"Pearl_of_Frost_Baning_Icon.png": "Pearl of Frost Baning",
|
||||||
|
"Pearl_of_Heart_Seeking_Icon.png": "Pearl of Heart Seeking",
|
||||||
|
"Pearl_of_Hermetic_Linking_Icon.png": "Pearl of Hermetic Linking",
|
||||||
|
"Pearl_of_Impenetrability_Icon.png": "Pearl of Impenetrability",
|
||||||
|
"Pearl_of_Lightning_Baning_Icon.png": "Pearl of Lightning Baning",
|
||||||
|
"Pearl_of_Pierce_Baning_Icon.png": "Pearl of Pierce Baning",
|
||||||
|
"Pearl_of_Spirit_Drinking_Icon.png": "Pearl of Spirit Drinking",
|
||||||
|
"Pearl_of_Swift_Killing_Icon.png": "Pearl of Swift Killing",
|
||||||
|
"Perennial_Argenory_Dye_Icon.png": "Perennial Argenory Dye",
|
||||||
|
"Perennial_Berimphur_Dye_Icon.png": "Perennial Berimphur Dye",
|
||||||
|
"Perennial_Botched_Dye_Icon.png": "Perennial Botched Dye",
|
||||||
|
"Perennial_Colban_Dye_Icon.png": "Perennial Colban Dye",
|
||||||
|
"Perennial_Hennacin_Dye_Icon.png": "Perennial Hennacin Dye",
|
||||||
|
"Perennial_Lapyan_Dye_Icon.png": "Perennial Lapyan Dye",
|
||||||
|
"Perennial_Minalim_Dye_Icon.png": "Perennial Minalim Dye",
|
||||||
|
"Perennial_Relanim_Dye_Icon.png": "Perennial Relanim Dye",
|
||||||
|
"Perennial_Thananim_Dye_Icon.png": "Perennial Thananim Dye",
|
||||||
|
"Perennial_Verdalim_Dye_Icon.png": "Perennial Verdalim Dye",
|
||||||
|
"Peridot_Foolproof_Icon.png": "Peridot Foolproof",
|
||||||
|
"Physicians_Crystal_Icon.png": "Physician's Crystal",
|
||||||
|
"Pictograph_of_Coordination_Icon.png": "Pictograph of Coordination",
|
||||||
|
"Pictograph_of_Endurance_Icon.png": "Pictograph of Endurance",
|
||||||
|
"Pictograph_of_Focus_Icon.png": "Pictograph of Focus",
|
||||||
|
"Pictograph_of_Quickness_Icon.png": "Pictograph of Quickness",
|
||||||
|
"Pictograph_of_Strength_Icon.png": "Pictograph of Strength",
|
||||||
|
"Pictograph_of_Willpower_Icon.png": "Pictograph of Willpower",
|
||||||
|
"Pillar_of_Fearlessness_Icon.png": "Pillar of Fearlessness",
|
||||||
|
"Pitfighters_Edge_Icon.png": "Pitfighters Edge",
|
||||||
|
"Red_Garnet_Foolproof_Icon.png": "Red Garnet Foolproof",
|
||||||
|
"Refreshing_Elixir_Icon.png": "Refreshing Elixir",
|
||||||
|
"Resisters_Crystal_Icon.png": "Resister's Crystal",
|
||||||
|
"Revenants_Scythe_Icon.png": "Revenants Scythe",
|
||||||
|
"Ridgeback_Dagger_Icon.png": "Ridgeback Dagger",
|
||||||
|
"Ring_of_Channeling_Icon.png": "Ring of Channeling",
|
||||||
|
"Rogues_Crystal_Icon.png": "Rogue's Crystal",
|
||||||
|
"Royal_Ladle_Icon.png": "Royal Ladle",
|
||||||
|
"Rune_of_Acid_Bane_Icon.png": "Rune of Acid Bane",
|
||||||
|
"Rune_of_Blade_Bane_Icon.png": "Rune of Blade Bane",
|
||||||
|
"Rune_of_Blood_Drinker_Icon.png": "Rune of Blood Drinker",
|
||||||
|
"Rune_of_Bludgeon_Bane_Icon.png": "Rune of Bludgeon Bane",
|
||||||
|
"Rune_of_Defender_Icon.png": "Rune of Defender",
|
||||||
|
"Rune_of_Dispel_Icon.png": "Rune of Dispel",
|
||||||
|
"Rune_of_Flame_Bane_Icon.png": "Rune of Flame Bane",
|
||||||
|
"Rune_of_Frost_Bane_Icon.png": "Rune of Frost Bane",
|
||||||
|
"Rune_of_Heart_Seeker_Icon.png": "Rune of Heart Seeker",
|
||||||
|
"Rune_of_Hermetic_Link_Icon.png": "Rune of Hermetic Link",
|
||||||
|
"Rune_of_Impenetrability_Icon.png": "Rune of Impenetrability",
|
||||||
|
"Rune_of_Lifestone_Recall_Icon.png": "Rune of Lifestone Recall",
|
||||||
|
"Rune_of_Lightning_Bane_Icon.png": "Rune of Lightning Bane",
|
||||||
|
"Rune_of_Pierce_Bane_Icon.png": "Rune of Pierce Bane",
|
||||||
|
"Rune_of_Portal_Recall_Icon.png": "Rune of Portal Recall",
|
||||||
|
"Rune_of_Spirit_Drinker_Icon.png": "Rune of Spirit Drinker",
|
||||||
|
"Rune_of_Swift_Killer_Icon.png": "Rune of Swift Killer",
|
||||||
|
"Scholars_Crystal_Icon.png": "Scholar's Crystal",
|
||||||
|
"Serpents_Flight_Icon.png": "Serpents Flight",
|
||||||
|
"Shield_of_Engorgement_Icon.png": "Shield of Engorgement",
|
||||||
|
"Shimmering_Skeleton_Key_Icon.png": "Shimmering Skeleton Key",
|
||||||
|
"Skullpuncher_Icon.png": "Skullpuncher",
|
||||||
|
"Smite_Icon.png": "Smite",
|
||||||
|
"Smithys_Crystal_Icon.png": "Smithy's Crystal",
|
||||||
|
"Spear_of_Lost_Truths_Icon.png": "Spear of Lost Truths",
|
||||||
|
"Spirit_Shifting_Staff_Icon.png": "Spirit Shifting Staff",
|
||||||
|
"Sprinters_Pearl_Icon.png": "Sprinter's Pearl",
|
||||||
|
"Squires_Glaive_Icon.png": "Squire's Glaive",
|
||||||
|
"Staff_of_All_Aspects_Icon.png": "Staff of All Aspects",
|
||||||
|
"Staff_of_Fettered_Souls_Icon.png": "Staff of Fettered Souls",
|
||||||
|
"Staff_of_Tendrils_Icon.png": "Staff of Tendrils",
|
||||||
|
"Star_of_Gharun_Icon.png": "Star of Gharun",
|
||||||
|
"Star_of_Tukal_Icon.png": "Star of Tukal",
|
||||||
|
"Steel_Butterfly_Icon.png": "Steel Butterfly",
|
||||||
|
"Steel_Wall_Boots_Icon.png": "Steel Wall Boots",
|
||||||
|
"Subjugator_Icon.png": "Subjugator",
|
||||||
|
"Sunstone_Foolproof_Icon.png": "Sunstone Foolproof",
|
||||||
|
"Swift_Strike_Ring_Icon.png": "Swift Strike Ring",
|
||||||
|
"Tassets_of_Leikothas_Tears_Icon.png": "Tassets of Leikotha's Tears",
|
||||||
|
"Thiefs_Crystal_Icon.png": "Thief's Crystal",
|
||||||
|
"Thorstens_Crystal_Icon.png": "Thorsten's Crystal",
|
||||||
|
"Thunderhead_Icon.png": "Thunderhead",
|
||||||
|
"Tings_Crystal_Icon.png": "Ting's Crystal",
|
||||||
|
"Tinkers_Crystal_Icon.png": "Tinker's Crystal",
|
||||||
|
"Tracker_Boots_Icon.png": "Tracker Boots",
|
||||||
|
"Tri_Blade_Spear_Icon.png": "Tri-Blade Spear",
|
||||||
|
"Tusked_Axe_of_Ayan_Baqur_Icon.png": "Tusked Axe of Ayan Baqur",
|
||||||
|
"Tuskers_Jewel_Icon.png": "Tusker's Jewel",
|
||||||
|
"Twin_Ward_Icon.png": "Twin Ward",
|
||||||
|
"Unchained_Prowess_Ring_Icon.png": "Unchained Prowess Ring",
|
||||||
|
"Ursuins_Pearl_Icon.png": "Ursuin's Pearl",
|
||||||
|
"Valkeers_Helm_Icon.png": "Valkeers Helm",
|
||||||
|
"Vaulters_Crystal_Icon.png": "Vaulter's Crystal",
|
||||||
|
"Wand_of_the_Frore_Crystal_Icon.png": "Wand of the Frore Crystal",
|
||||||
|
"Warriors_Crystal_Icon.png": "Warrior's Crystal",
|
||||||
|
"Warriors_Jewel_Icon.png": "Warrior's Jewel",
|
||||||
|
"Wayfarers_Pearl_Icon.png": "Wayfarer's Pearl",
|
||||||
|
"Weeping_Ring_Icon.png": "Weeping Ring",
|
||||||
|
"White_Sapphire_Foolproof_Icon.png": "White Sapphire Foolproof",
|
||||||
|
"Wings_of_Rakhil_Icon.png": "Wings of Rakhil",
|
||||||
|
"Winters_Heart_Icon.png": "Winters Heart",
|
||||||
|
"Yellow_Topaz_Foolproof_Icon.png": "Yellow Topaz Foolproof",
|
||||||
|
"Zefirs_Breath_Icon.png": "Zefir's Breath",
|
||||||
|
"Zefirs_Crystal_Icon.png": "Zefir's Crystal",
|
||||||
|
"Zharalim_Crookblade_Icon.png": "Zharalim Crookblade",
|
||||||
|
"Zircon_Foolproof_Icon.png": "Zircon Foolproof"
|
||||||
|
}
|
||||||
BIN
discord-rare-monitor/icons/Adepts_Fervor_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Adherents_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Alchemists_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Aquamarine_Foolproof_Icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
discord-rare-monitor/icons/Archers_Jewel_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Aristocrats_Bracelet_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Artificers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Artists_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Assassins_Whisper_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Astyrrians_Jewel_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Band_of_Elemental_Harmony_Icon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
discord-rare-monitor/icons/Baton_of_Tirethas_Icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
discord-rare-monitor/icons/Bearded_Axe_of_Souia-Vey_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Ben_Tens_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Berzerkers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Black_Cloud_Bow_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Black_Garnet_Foolproof_Icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
discord-rare-monitor/icons/Black_Opal_Foolproof_Icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
discord-rare-monitor/icons/Black_Thistle_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Bloodmark_Crossbow_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Bracelet_of_Binding_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Bracers_of_Leikothas_Tears_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Bradors_Frozen_Eye_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Brawlers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
discord-rare-monitor/icons/Canfield_Cleaver_Icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
discord-rare-monitor/icons/Casino_Exquisite_Keyring_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Champions_Demise_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Chefs_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Chitin_Cracker_Icon.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
discord-rare-monitor/icons/Circle_of_Pure_Thought_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Converters_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Corruptors_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Corsairs_Arc_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Count_Renaris_Equalizer_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Dart_Flicker_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Deaths_Grip_Staff_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Decapitators_Blade_Icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
discord-rare-monitor/icons/Deceivers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Defiler_of_Milantos_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Deru_Limb_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Desert_Wyrm_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Dodgers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Dragonspine_Bow_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Dread_Marauder_Shield_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Dreamseer_Bangle_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Drifters_Atlatl_Icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
discord-rare-monitor/icons/Dripping_Death_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Duelists_Jewel_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Dusk_Coat_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Dusk_Leggings_Icon.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
discord-rare-monitor/icons/Ebonwood_Shortbow_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Elysas_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Emerald_Foolproof_Icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
discord-rare-monitor/icons/Enchanters_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Eternal_Health_Kit_Icon.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
discord-rare-monitor/icons/Eternal_Mana_Charge_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Eternal_Mana_Kit_Icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
discord-rare-monitor/icons/Eternal_Stamina_Kit_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Evaders_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Executors_Jewel_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Eye_of_Muramm_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Feathered_Razor_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Fire_Opal_Foolproof_Icon.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
discord-rare-monitor/icons/Fist_of_Three_Principles_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Fletchers_Crystal_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Footmans_Boots_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Gauntlets_of_Leikothas_Tears_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Boots_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Bracers_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Breastplate_Icon.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Gauntlets_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Girth_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Greaves_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Mitre_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Pauldrons_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Gelidite_Tassets_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Gelids_Jewel_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Girth_of_Leikothas_Tears_Icon.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
discord-rare-monitor/icons/Golden_Snake_Choker_Icon.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
discord-rare-monitor/icons/Greaves_of_Leikothas_Tears_Icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
discord-rare-monitor/icons/Guardian_of_Pwyll_Icon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
discord-rare-monitor/icons/Heart_of_Darkest_Flame_Icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
discord-rare-monitor/icons/Helm_of_Leikothas_Tears_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
discord-rare-monitor/icons/Hevelios_Half-Moon_Icon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |