diff --git a/TODO.md b/TODO.md index c6111cb..e1ddd16 100644 --- a/TODO.md +++ b/TODO.md @@ -3,10 +3,10 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas for the PetBot project. ## 📊 Summary -- **✅ Completed**: 17 items +- **✅ Completed**: 18 items - **🐛 Bugs**: 0 items - **🔧 Enhancements**: 3 items -- **💡 Ideas**: 10 items +- **💡 Ideas**: 9 items - **📋 Total**: 30 items tracked --- @@ -163,6 +163,13 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f - Optimize web interface assets and loading times - Implement caching where appropriate +- [ ] **Improve admin weather control system** + - Enhanced argument parsing for more intuitive command usage + - Better error messages and validation feedback + - Add weather presets and quick-change options + - Implement weather history and logging + - Add bulk weather operations for multiple locations + --- ## 💡 FEATURE IDEAS @@ -173,10 +180,13 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f - Touch-friendly drag-and-drop alternatives - Mobile-optimized navigation and layouts -- [ ] **Enhance leaderboard with more categories (gym badges, rare pets, achievements)** - - Multiple leaderboard categories - - Filtering and sorting options - - Achievement-based rankings +- [x] **Enhance leaderboard with more categories (gym badges, rare pets, achievements)** + - ✅ Multiple leaderboard categories with 8 different rankings + - ✅ Interactive category switching with responsive navigation + - ✅ Achievement-based rankings and specialized stats + - ✅ Comprehensive player statistics (Level, Experience, Money, Pet Count, Achievements, Gym Badges, Highest Pet, Rare Pets) + - ✅ Responsive design with gold/silver/bronze highlighting for top 3 + - ✅ Real-time data from database with proper SQL optimization - [ ] **Add auto-save draft functionality to team builder to prevent data loss** - Local storage for unsaved team changes diff --git a/config/weather_patterns.json b/config/weather_patterns.json index 2713076..93b14cc 100644 --- a/config/weather_patterns.json +++ b/config/weather_patterns.json @@ -1,36 +1,36 @@ { "weather_types": { - "Sunny": { + "sunny": { "description": "Bright sunshine increases Fire and Grass-type spawns", "spawn_modifier": 1.5, "affected_types": ["Fire", "Grass"], "duration_minutes": [60, 120] }, - "Rainy": { + "rainy": { "description": "Heavy rain boosts Water-type spawns significantly", "spawn_modifier": 2.0, "affected_types": ["Water"], "duration_minutes": [45, 90] }, - "Thunderstorm": { + "storm": { "description": "Electric storms double Electric-type spawn rates", "spawn_modifier": 2.0, "affected_types": ["Electric"], "duration_minutes": [30, 60] }, - "Blizzard": { + "blizzard": { "description": "Harsh snowstorm increases Ice and Water-type spawns", "spawn_modifier": 1.7, "affected_types": ["Ice", "Water"], "duration_minutes": [60, 120] }, - "Earthquake": { + "earthquake": { "description": "Ground tremors bring Rock-type pets to the surface", "spawn_modifier": 1.8, "affected_types": ["Rock"], "duration_minutes": [30, 90] }, - "Calm": { + "calm": { "description": "Perfect weather with normal spawn rates", "spawn_modifier": 1.0, "affected_types": [], @@ -38,11 +38,11 @@ } }, "location_weather_chances": { - "Starter Town": ["Sunny", "Calm", "Rainy"], - "Whispering Woods": ["Sunny", "Rainy", "Calm"], - "Electric Canyon": ["Thunderstorm", "Sunny", "Calm"], - "Crystal Caves": ["Earthquake", "Calm"], - "Frozen Tundra": ["Blizzard", "Calm"], - "Dragon's Peak": ["Thunderstorm", "Sunny", "Calm"] + "Starter Town": ["sunny", "calm", "rainy"], + "Whispering Woods": ["sunny", "rainy", "calm"], + "Electric Canyon": ["storm", "sunny", "calm"], + "Crystal Caves": ["earthquake", "calm"], + "Frozen Tundra": ["blizzard", "calm"], + "Dragon's Peak": ["storm", "sunny", "calm"] } } \ No newline at end of file diff --git a/modules/admin.py b/modules/admin.py index d623a3a..8631ade 100644 --- a/modules/admin.py +++ b/modules/admin.py @@ -21,7 +21,7 @@ class Admin(BaseModule): """Handles admin-only commands like reload""" def get_commands(self): - return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset"] + return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather"] async def handle_command(self, channel, nickname, command, args): if command == "reload": @@ -34,6 +34,10 @@ class Admin(BaseModule): await self.cmd_rate_unban(channel, nickname, args) elif command == "rate_reset": await self.cmd_rate_reset(channel, nickname, args) + elif command == "weather": + await self.cmd_weather(channel, nickname, args) + elif command == "setweather": + await self.cmd_setweather(channel, nickname, args) async def cmd_reload(self, channel, nickname): """Reload bot modules (admin only)""" @@ -168,4 +172,149 @@ class Admin(BaseModule): self.send_message(channel, f"{nickname}: â„šī¸ No violations found for user {target_user}.") except Exception as e: - self.send_message(channel, f"{nickname}: ❌ Error resetting violations: {str(e)}") \ No newline at end of file + self.send_message(channel, f"{nickname}: ❌ Error resetting violations: {str(e)}") + + async def cmd_weather(self, channel, nickname, args): + """Check current weather in locations (admin only)""" + if not self.is_admin(nickname): + self.send_message(channel, f"{nickname}: Access denied. Admin command.") + return + + try: + if args and args[0].lower() != "all": + # Check weather for specific location + location_name = " ".join(args) + weather = await self.database.get_location_weather_by_name(location_name) + + if weather: + self.send_message(channel, + f"đŸŒ¤ī¸ {nickname}: {location_name} - {weather['weather_type']} " + f"(modifier: {weather['spawn_modifier']}x, " + f"until: {weather['active_until'][:16]})") + else: + self.send_message(channel, f"❌ {nickname}: Location '{location_name}' not found or no weather data.") + else: + # Show weather for all locations + all_weather = await self.database.get_all_location_weather() + if all_weather: + weather_info = [] + for w in all_weather: + weather_info.append(f"{w['location_name']}: {w['weather_type']} ({w['spawn_modifier']}x)") + + self.send_message(channel, f"đŸŒ¤ī¸ {nickname}: Current weather - " + " | ".join(weather_info)) + else: + self.send_message(channel, f"❌ {nickname}: No weather data available.") + + except Exception as e: + self.send_message(channel, f"{nickname}: ❌ Error checking weather: {str(e)}") + + async def cmd_setweather(self, channel, nickname, args): + """Force change weather in a location or all locations (admin only)""" + if not self.is_admin(nickname): + self.send_message(channel, f"{nickname}: Access denied. Admin command.") + return + + if not args: + self.send_message(channel, + f"{nickname}: Usage: !setweather [duration_minutes]\n" + f"Weather types: sunny, rainy, storm, blizzard, earthquake, calm") + return + + try: + import json + import random + import datetime + + # Load weather patterns + with open("config/weather_patterns.json", "r") as f: + weather_data = json.load(f) + + weather_types = list(weather_data["weather_types"].keys()) + + # Smart argument parsing - check if any arg is a weather type + location_arg = None + weather_type = None + duration = None + + for i, arg in enumerate(args): + if arg.lower() in weather_types: + weather_type = arg.lower() + # Remove weather type from args for location parsing + remaining_args = args[:i] + args[i+1:] + break + + if not weather_type: + self.send_message(channel, f"{nickname}: Please specify a valid weather type.") + return + + # Parse location from remaining args + if remaining_args: + if remaining_args[0].lower() == "all": + location_arg = "all" + # Check if there's a duration after "all" + if len(remaining_args) > 1: + try: + duration = int(remaining_args[1]) + except ValueError: + pass + else: + # Location name (might be multiple words) + location_words = [] + for arg in remaining_args: + try: + # If it's a number, it's probably duration + duration = int(arg) + break + except ValueError: + # It's part of location name + location_words.append(arg) + location_arg = " ".join(location_words) if location_words else "all" + else: + location_arg = "all" + + weather_config = weather_data["weather_types"][weather_type] + + # Calculate duration + if not duration: + duration_range = weather_config.get("duration_minutes", [90, 180]) + duration = random.randint(duration_range[0], duration_range[1]) + + end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration) + + if location_arg.lower() == "all": + # Set weather for all locations + success = await self.database.set_weather_all_locations( + weather_type, end_time.isoformat(), + weather_config.get("spawn_modifier", 1.0), + ",".join(weather_config.get("affected_types", [])) + ) + + if success: + self.send_message(channel, + f"đŸŒ¤ī¸ {nickname}: Set {weather_type} weather for ALL locations! " + f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x") + else: + self.send_message(channel, f"❌ {nickname}: Failed to set weather for all locations.") + else: + # Set weather for specific location + location_name = location_arg if len(args) == 2 else " ".join(args[:-1]) + + success = await self.database.set_weather_for_location( + location_name, weather_type, end_time.isoformat(), + weather_config.get("spawn_modifier", 1.0), + ",".join(weather_config.get("affected_types", [])) + ) + + if success: + self.send_message(channel, + f"đŸŒ¤ī¸ {nickname}: Set {weather_type} weather for {location_name}! " + f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x") + else: + self.send_message(channel, f"❌ {nickname}: Failed to set weather for '{location_name}'. Location may not exist.") + + except FileNotFoundError: + self.send_message(channel, f"{nickname}: ❌ Weather configuration file not found.") + except ValueError as e: + self.send_message(channel, f"{nickname}: ❌ Invalid duration: {str(e)}") + except Exception as e: + self.send_message(channel, f"{nickname}: ❌ Error setting weather: {str(e)}") \ No newline at end of file diff --git a/run_bot_debug.py b/run_bot_debug.py index 03d9982..b245824 100644 --- a/run_bot_debug.py +++ b/run_bot_debug.py @@ -161,20 +161,52 @@ class PetBotDebug: async def reload_modules(self): """Reload all modules (for admin use)""" try: - # Reload module files - import modules - importlib.reload(modules.core_commands) - importlib.reload(modules.exploration) - importlib.reload(modules.battle_system) - importlib.reload(modules.pet_management) - importlib.reload(modules.achievements) - importlib.reload(modules.admin) - importlib.reload(modules) - - # Reinitialize modules print("🔄 Reloading modules...") + + # Import all module files + import modules.core_commands + import modules.exploration + import modules.battle_system + import modules.pet_management + import modules.achievements + import modules.admin + import modules.inventory + import modules.gym_battles + import modules.team_builder + import modules.backup_commands + import modules.connection_monitor + import modules.base_module + import modules + + # Reload each module individually with error handling + modules_to_reload = [ + ('base_module', modules.base_module), + ('core_commands', modules.core_commands), + ('exploration', modules.exploration), + ('battle_system', modules.battle_system), + ('pet_management', modules.pet_management), + ('achievements', modules.achievements), + ('admin', modules.admin), + ('inventory', modules.inventory), + ('gym_battles', modules.gym_battles), + ('team_builder', modules.team_builder), + ('backup_commands', modules.backup_commands), + ('connection_monitor', modules.connection_monitor), + ('modules', modules) + ] + + for module_name, module_obj in modules_to_reload: + try: + importlib.reload(module_obj) + print(f" ✅ Reloaded {module_name}") + except Exception as e: + print(f" ❌ Failed to reload {module_name}: {e}") + + # Clear and reinitialize module instances + self.modules = {} self.load_modules() - print("✅ Modules reloaded successfully") + + print("✅ All modules reloaded successfully") return True except Exception as e: print(f"❌ Module reload failed: {e}") diff --git a/src/database.py b/src/database.py index 52eb5bd..f8c53ba 100644 --- a/src/database.py +++ b/src/database.py @@ -355,6 +355,20 @@ class Database: ) """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS team_configurations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id INTEGER NOT NULL, + config_name TEXT NOT NULL, + slot_number INTEGER NOT NULL, + team_data TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (player_id) REFERENCES players (id), + UNIQUE(player_id, slot_number) + ) + """) + # Create species_moves table for move learning system await db.execute(""" CREATE TABLE IF NOT EXISTS species_moves ( @@ -1879,4 +1893,167 @@ class Database: "total_pets": result[0], "active_pets": result[1], "storage_pets": result[2] - } \ No newline at end of file + } + + # Weather Management Methods + async def get_location_weather_by_name(self, location_name: str) -> Optional[Dict]: + """Get current weather for a location by name""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT lw.*, l.name as location_name + FROM location_weather lw + JOIN locations l ON lw.location_id = l.id + WHERE l.name = ? AND lw.active_until > datetime('now') + ORDER BY lw.id DESC LIMIT 1 + """, (location_name,)) + + row = await cursor.fetchone() + return dict(row) if row else None + + async def get_all_location_weather(self) -> List[Dict]: + """Get current weather for all locations""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT lw.*, l.name as location_name + FROM location_weather lw + JOIN locations l ON lw.location_id = l.id + WHERE lw.active_until > datetime('now') + ORDER BY l.name + """) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def set_weather_all_locations(self, weather_type: str, active_until: str, + spawn_modifier: float, affected_types: str) -> bool: + """Set weather for all locations""" + try: + async with aiosqlite.connect(self.db_path) as db: + # Clear existing weather + await db.execute("DELETE FROM location_weather") + + # Get all location IDs + cursor = await db.execute("SELECT id FROM locations") + location_ids = [row[0] for row in await cursor.fetchall()] + + # Set new weather for all locations + for location_id in location_ids: + await db.execute(""" + INSERT INTO location_weather + (location_id, weather_type, active_until, spawn_modifier, affected_types) + VALUES (?, ?, ?, ?, ?) + """, (location_id, weather_type, active_until, spawn_modifier, affected_types)) + + await db.commit() + return True + except Exception as e: + print(f"Error setting weather for all locations: {e}") + return False + + async def set_weather_for_location(self, location_name: str, weather_type: str, + active_until: str, spawn_modifier: float, + affected_types: str) -> bool: + """Set weather for a specific location""" + try: + async with aiosqlite.connect(self.db_path) as db: + # Get location ID + cursor = await db.execute("SELECT id FROM locations WHERE name = ?", (location_name,)) + location_row = await cursor.fetchone() + + if not location_row: + return False + + location_id = location_row[0] + + # Clear existing weather for this location + await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location_id,)) + + # Set new weather + await db.execute(""" + INSERT INTO location_weather + (location_id, weather_type, active_until, spawn_modifier, affected_types) + VALUES (?, ?, ?, ?, ?) + """, (location_id, weather_type, active_until, spawn_modifier, affected_types)) + + await db.commit() + return True + except Exception as e: + print(f"Error setting weather for location {location_name}: {e}") + return False + + # Team Configuration Methods + async def save_team_configuration(self, player_id: int, slot_number: int, config_name: str, team_data: str) -> bool: + """Save a team configuration to a specific slot (1-3)""" + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + INSERT OR REPLACE INTO team_configurations + (player_id, slot_number, config_name, team_data, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + """, (player_id, slot_number, config_name, team_data)) + + await db.commit() + return True + except Exception as e: + print(f"Error saving team configuration: {e}") + return False + + async def get_team_configurations(self, player_id: int) -> List[Dict]: + """Get all team configurations for a player""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT slot_number, config_name, team_data, created_at, updated_at + FROM team_configurations + WHERE player_id = ? + ORDER BY slot_number + """, (player_id,)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def load_team_configuration(self, player_id: int, slot_number: int) -> Optional[Dict]: + """Load a specific team configuration""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT config_name, team_data, updated_at + FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, slot_number)) + + row = await cursor.fetchone() + return dict(row) if row else None + + async def delete_team_configuration(self, player_id: int, slot_number: int) -> bool: + """Delete a team configuration""" + try: + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + DELETE FROM team_configurations + WHERE player_id = ? AND slot_number = ? + """, (player_id, slot_number)) + + await db.commit() + return cursor.rowcount > 0 + except Exception as e: + print(f"Error deleting team configuration: {e}") + return False + + async def rename_team_configuration(self, player_id: int, slot_number: int, new_name: str) -> bool: + """Rename a team configuration""" + try: + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + UPDATE team_configurations + SET config_name = ?, updated_at = CURRENT_TIMESTAMP + WHERE player_id = ? AND slot_number = ? + """, (new_name, player_id, slot_number)) + + await db.commit() + return cursor.rowcount > 0 + except Exception as e: + print(f"Error renaming team configuration: {e}") + return False \ No newline at end of file diff --git a/src/irc_connection_manager.py b/src/irc_connection_manager.py index cd7ca63..990532a 100644 --- a/src/irc_connection_manager.py +++ b/src/irc_connection_manager.py @@ -35,7 +35,7 @@ class IRCConnectionManager: self.last_ping_time = 0 self.last_pong_time = 0 self.ping_interval = 60 # Send PING every 60 seconds - self.ping_timeout = 120 # Expect PONG within 2 minutes + self.ping_timeout = 180 # Expect PONG within 3 minutes # Reconnection settings self.reconnect_attempts = 0 @@ -242,10 +242,15 @@ class IRCConnectionManager: if line.startswith("PING"): pong_response = line.replace("PING", "PONG") await self._send_raw(pong_response) + # Server-initiated PING also counts as activity + self.last_pong_time = time.time() + self.logger.debug(f"Server PING received and replied: {line}") return - if line.startswith("PONG"): + # Improved PONG detection - handle various formats + if line.startswith("PONG") or " PONG " in line: self.last_pong_time = time.time() + self.logger.debug(f"PONG received: {line}") return # Handle connection completion @@ -324,16 +329,29 @@ class IRCConnectionManager: # Send ping if interval has passed if current_time - self.last_ping_time > self.ping_interval: try: - await self._send_raw(f"PING :health_check_{int(current_time)}") + ping_message = f"PING :health_check_{int(current_time)}" + await self._send_raw(ping_message) self.last_ping_time = current_time + self.logger.debug(f"Sent health check ping: {ping_message}") except Exception as e: self.logger.error(f"Failed to send ping: {e}") raise ConnectionError("Health check ping failed") - # Check if we've received a pong recently - if current_time - self.last_pong_time > self.ping_timeout: - self.logger.warning("No PONG received within timeout period") - raise ConnectionError("Ping timeout - connection appears dead") + # Check if we've received a pong recently (only if we've sent a ping) + time_since_last_ping = current_time - self.last_ping_time + time_since_last_pong = current_time - self.last_pong_time + + # Only check for PONG timeout if we've sent a ping and enough time has passed + if time_since_last_ping < self.ping_interval and time_since_last_pong > self.ping_timeout: + # Add more lenient check - only fail if we've had no activity at all + time_since_last_message = current_time - self.last_message_time + + if time_since_last_message > self.ping_timeout: + self.logger.warning(f"No PONG received within timeout period. Last ping: {time_since_last_ping:.1f}s ago, Last pong: {time_since_last_pong:.1f}s ago, Last message: {time_since_last_message:.1f}s ago") + raise ConnectionError("Ping timeout - connection appears dead") + else: + # We're getting other messages, so connection is likely fine + self.logger.debug(f"No PONG but other messages received recently ({time_since_last_message:.1f}s ago)") async def _handle_connection_error(self, error): """Handle connection errors and initiate reconnection.""" diff --git a/webserver.py b/webserver.py index 04b9343..0cc3e08 100644 --- a/webserver.py +++ b/webserver.py @@ -689,6 +689,33 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): elif path.startswith('/teambuilder/') and path.endswith('/verify'): nickname = path[13:-7] # Remove '/teambuilder/' prefix and '/verify' suffix self.handle_team_verify(nickname) + elif path.startswith('/teambuilder/') and '/config/save/' in path: + # Handle team configuration save: /teambuilder/{nickname}/config/save/{slot} + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + slot = parts[5] + self.handle_team_config_save(nickname, slot) + else: + self.send_error(400, "Invalid configuration save path") + elif path.startswith('/teambuilder/') and '/config/load/' in path: + # Handle team configuration load: /teambuilder/{nickname}/config/load/{slot} + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + slot = parts[5] + self.handle_team_config_load(nickname, slot) + else: + self.send_error(400, "Invalid configuration load path") + elif path.startswith('/teambuilder/') and '/config/rename/' in path: + # Handle team configuration rename: /teambuilder/{nickname}/config/rename/{slot} + parts = path.split('/') + if len(parts) >= 6: + nickname = parts[2] + slot = parts[5] + self.handle_team_config_rename(nickname, slot) + else: + self.send_error(400, "Invalid configuration rename path") else: self.send_error(404, "Page not found") @@ -791,11 +818,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
Move to a different location. Each area has unique pets and gyms. Some locations require achievements to unlock.
Example: !travel whispering woods
-
-
!weather
-
Check the current weather effects in your location. Weather affects which pet types spawn more frequently.
-
Example: !weather
-
!where / !location
See which location you're currently in and get information about the area.
@@ -942,7 +964,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
!team
-
View your active team of pets with their levels, HP, and status.
+
Access your team builder web interface for drag-and-drop team management with PIN verification.
Example: !team
@@ -960,6 +982,11 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
Remove a pet from your active team and put it in storage.
Example: !deactivate aqua
+
+
!nickname <pet> <new_name>
+
Give a custom nickname to one of your pets. Nicknames must be 20 characters or less.
+
Example: !nickname flamey FireStorm
+
@@ -992,7 +1019,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
- 💡 Item Discovery: Find items while exploring! Each location has unique treasures. Items stack in your inventory and can be used anytime. + 💡 Item Discovery: Find items while exploring! Each location has unique treasures including rare Coin Pouches with 1-3 coins. Items stack in your inventory and can be used anytime.
@@ -1019,16 +1046,65 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): +
+
⚡ Admin Commands ADMIN ONLY
+
+
+
+
!reload
+
Reload all bot modules without restarting. Useful for applying code changes.
+
Example: !reload
+
+
+
!weather [location|all]
+
Check current weather conditions in specific location or all locations.
+
Example: !weather Electric Canyon
+
+
+
!setweather <weather> [location] [duration]
+
Force change weather. Types: sunny, rainy, storm, blizzard, earthquake, calm
+
Example: !setweather storm all 60
+
+
+
!backup create [description]
+
Create manual database backup with optional description.
+
Example: !backup create "before update"
+
+
+
!rate_stats [user]
+
View rate limiting statistics for all users or specific user.
+
Example: !rate_stats username
+
+
+
!status / !uptime
+
Check bot connection status, uptime, and system health information.
+
Example: !status
+
+
+
!backups / !restore
+
List available backups or restore from backup. Use with caution!
+
Example: !backups
+
+
+ +
+ 🔒 Admin Access: These commands require administrator privileges and are restricted to authorized users only. +
+
+
+
🌐 Web Interface
Access detailed information through the web dashboard at http://petz.rdx4.com/
    -
  • Player Profiles - Complete stats, pet collections, and inventories
  • -
  • Leaderboard - Top players by level and achievements
  • -
  • Locations Guide - All areas with spawn information
  • -
  • Gym Badges - Display your earned badges and progress
  • +
  • Player Profiles - Complete stats, pet collections, and inventories with usage commands
  • +
  • Team Builder - Drag-and-drop team management with PIN verification
  • +
  • Enhanced Leaderboards - 8 categories: levels, experience, wealth, achievements, gym badges, rare pets
  • +
  • Locations Guide - All areas with spawn information and current weather
  • +
  • Gym Badges - Display your earned badges and battle progress
  • +
  • Inventory Management - Visual item display with command instructions
@@ -3869,6 +3945,212 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): .back-link:hover {{ text-decoration: underline; }} + + .config-section {{ + background: var(--bg-secondary); + border-radius: 15px; + padding: 25px; + margin: 30px 0; + border: 1px solid var(--bg-tertiary); + }} + + .config-slots {{ + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-top: 20px; + }} + + .config-slot {{ + background: var(--bg-tertiary); + border-radius: 10px; + padding: 20px; + border: 1px solid var(--drag-hover); + }} + + .config-header {{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + }} + + .slot-label {{ + font-weight: bold; + color: var(--text-accent); + }} + + .config-name {{ + color: var(--text-secondary); + font-style: italic; + }} + + .config-actions {{ + display: flex; + gap: 10px; + }} + + .config-btn {{ + flex: 1; + padding: 10px 15px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 0.9em; + font-weight: 500; + transition: all 0.3s ease; + }} + + .save-config {{ + background: #4CAF50; + color: white; + }} + + .save-config:hover {{ + background: #45a049; + transform: translateY(-2px); + }} + + .load-config {{ + background: #2196F3; + color: white; + }} + + .load-config:hover:not(:disabled) {{ + background: #1976D2; + transform: translateY(-2px); + }} + + .load-config:disabled {{ + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + }} + + .working-section {{ + background: var(--bg-secondary); + border-radius: 15px; + padding: 20px; + margin: 20px 0; + border: 2px solid var(--text-accent); + }} + + .config-selector {{ + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; + flex-wrap: wrap; + }} + + .config-selector label {{ + color: var(--text-primary); + font-weight: 500; + }} + + .config-selector select {{ + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--drag-hover); + border-radius: 8px; + padding: 10px 15px; + font-size: 1em; + min-width: 250px; + }} + + .config-selector select:focus {{ + outline: none; + border-color: var(--text-accent); + box-shadow: 0 0 0 2px rgba(102, 255, 102, 0.2); + }} + + .quick-save-btn {{ + background: #4CAF50; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + cursor: pointer; + font-size: 1em; + font-weight: 500; + transition: all 0.3s ease; + }} + + .quick-save-btn:hover:not(:disabled) {{ + background: #45a049; + transform: translateY(-2px); + }} + + .quick-save-btn:disabled {{ + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + }} + + .config-status {{ + background: var(--bg-tertiary); + border-radius: 8px; + padding: 12px 15px; + border-left: 4px solid var(--text-accent); + }} + + .status-text {{ + color: var(--text-secondary); + font-style: italic; + }} + + .config-quick-actions {{ + display: flex; + gap: 10px; + margin-top: 15px; + flex-wrap: wrap; + }} + + .config-action-btn {{ + background: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + padding: 8px 16px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.3s ease; + min-width: 120px; + }} + + .config-action-btn:hover:not(:disabled) {{ + background: var(--secondary-color); + transform: translateY(-1px); + }} + + .config-action-btn:disabled {{ + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + }} + + .rename-btn {{ + background: #FF9800; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + cursor: pointer; + font-size: 1em; + margin-left: 10px; + transition: all 0.3s ease; + }} + + .rename-btn:hover:not(:disabled) {{ + background: #F57C00; + transform: translateY(-1px); + }} + + .rename-btn:disabled {{ + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + }} @@ -3880,6 +4162,41 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):

{nickname} | Active: {len(active_pets)} pets | Storage: {len(inactive_pets)} pets

+
+
💾 Team Configurations
+

+ Save up to 3 different team setups for quick switching between strategies +

+ +
+ + + + +
+ +
+ Editing current team (not saved to any configuration) +
+ +
+ + + +
+
+
⭐ Active Team
@@ -3930,6 +4247,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
+

🔐 PIN Verification Required

A 6-digit PIN has been sent to you via IRC private message.

@@ -4408,6 +4726,341 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): document.head.appendChild(style); console.log('🐾 Team Builder initialized successfully!'); + + // Load existing configurations on page load + loadConfigurationList(); + + // Track which configuration is currently being edited + let activeConfigSlot = 'current'; + let hasUnsavedChanges = false; + + // Team Configuration Functions + async function saveConfig(slot) {{ + const configName = prompt(`Enter a name for configuration slot ${{slot}}:`, `Team Config ${{slot}}`); + if (!configName) return; + + try {{ + const teamData = getCurrentTeamData(); + const response = await fetch(`/teambuilder/{nickname}/config/save/${{slot}}`, {{ + method: 'POST', + headers: {{ 'Content-Type': 'application/json' }}, + body: JSON.stringify({{ + name: configName, + team: teamData + }}) + }}); + + const result = await response.json(); + + if (result.success) {{ + showMessage(`Configuration '${{configName}}' saved to slot ${{slot}}!`, 'success'); + updateConfigSlot(slot, configName); + }} else {{ + showMessage('Failed to save configuration: ' + result.error, 'error'); + }} + }} catch (error) {{ + showMessage('Network error: ' + error.message, 'error'); + }} + }} + + async function loadConfig(slot) {{ + if (!confirm(`Load team configuration from slot ${{slot}}? This will replace your current team setup.`)) {{ + return; + }} + + try {{ + const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{ + method: 'POST' + }}); + + const result = await response.json(); + + if (result.success) {{ + applyTeamConfiguration(result.team_data); + showMessage(`Team configuration '${{result.config_name}}' loaded!`, 'success'); + }} else {{ + showMessage('Failed to load configuration: ' + result.error, 'error'); + }} + }} catch (error) {{ + showMessage('Network error: ' + error.message, 'error'); + }} + }} + + function getCurrentTeamData() {{ + const teamData = []; + + // Get pets from team slots + for (let i = 1; i <= 6; i++) {{ + const slot = document.getElementById(`slot-${{i}}`); + const petCard = slot.querySelector('.pet-card'); + if (petCard) {{ + teamData.push({{ + pet_id: parseInt(petCard.dataset.petId), + position: i + }}); + }} + }} + + return teamData; + }} + + function applyTeamConfiguration(teamData) {{ + // Clear all current team positions + for (let i = 1; i <= 6; i++) {{ + const slot = document.getElementById(`slot-${{i}}`); + const petCard = slot.querySelector('.pet-card'); + if (petCard) {{ + // Move pet back to storage + const storageContainer = document.getElementById('storage-container'); + storageContainer.appendChild(petCard); + petCard.dataset.active = 'false'; + petCard.classList.remove('active'); + petCard.classList.add('storage'); + }} + slot.querySelector('.slot-content').innerHTML = ''; + }} + + // Apply new team configuration + teamData.forEach(config => {{ + const petCard = document.querySelector(`[data-pet-id="${{config.pet_id}}"]`); + if (petCard) {{ + const targetSlot = document.getElementById(`slot-${{config.position}}`); + const slotContent = targetSlot.querySelector('.slot-content'); + + slotContent.appendChild(petCard); + petCard.dataset.active = 'true'; + petCard.classList.remove('storage'); + petCard.classList.add('active'); + }} + }}); + + updateSaveButton(); + updateDropZoneVisibility(); + }} + + async function loadConfigurationList() {{ + // This would typically load from an API endpoint that lists all configs + // For now, we'll check each slot individually + for (let slot = 1; slot <= 3; slot++) {{ + try {{ + const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{ + method: 'POST' + }}); + + if (response.ok) {{ + const result = await response.json(); + if (result.success) {{ + updateConfigSlot(slot, result.config_name); + }} + }} + }} catch (error) {{ + // Slot is empty, which is fine + }} + }} + }} + + function updateConfigSlot(slot, configName) {{ + const nameElement = document.getElementById(`config-name-${{slot}}`); + const loadButton = document.getElementById(`load-btn-${{slot}}`); + + nameElement.textContent = configName; + nameElement.style.color = 'var(--text-primary)'; + nameElement.style.fontStyle = 'normal'; + loadButton.disabled = false; + + // Update selector dropdown + const selectorElement = document.getElementById(`selector-name-${{slot}}`); + if (selectorElement) {{ + selectorElement.textContent = configName; + }} + + // Update option text in dropdown + const option = document.querySelector(`#active-config option[value="${{slot}}"]`); + if (option) {{ + option.textContent = `Config ${{slot}}: ${{configName}}`; + }} + }} + + async function switchActiveConfig() {{ + const selector = document.getElementById('active-config'); + const newSlot = selector.value; + + // Check for unsaved changes + if (hasUnsavedChanges && activeConfigSlot !== 'current') {{ + if (!confirm('You have unsaved changes. Do you want to switch configurations anyway?')) {{ + selector.value = activeConfigSlot; // Revert selection + return; + }} + }} + + activeConfigSlot = newSlot; + + if (newSlot === 'current') {{ + // Switch to current team (no loading) + updateConfigStatus('Editing current team (not saved to any configuration)'); + document.getElementById('quick-save-btn').disabled = true; + }} else {{ + // Load the selected configuration + try {{ + const response = await fetch(`/teambuilder/{nickname}/config/load/${{newSlot}}`, {{ + method: 'POST' + }}); + + const result = await response.json(); + + if (result.success) {{ + applyTeamConfiguration(result.team_data); + updateConfigStatus(`Editing: ${{result.config_name}}`); + document.getElementById('quick-save-btn').disabled = false; + hasUnsavedChanges = false; + }} else {{ + // Configuration doesn't exist, start editing a new one + updateConfigStatus(`Editing: Config ${{newSlot}} (new configuration)`); + document.getElementById('quick-save-btn').disabled = false; + hasUnsavedChanges = true; + }} + }} catch (error) {{ + showMessage('Error loading configuration: ' + error.message, 'error'); + selector.value = 'current'; + activeConfigSlot = 'current'; + }} + }} + }} + + async function quickSaveConfig() {{ + if (activeConfigSlot === 'current') return; + + try {{ + const teamData = getCurrentTeamData(); + + // Get existing config name or use default + let configName = `Team Config ${{activeConfigSlot}}`; + const existingConfig = await fetch(`/teambuilder/{nickname}/config/load/${{activeConfigSlot}}`, {{ + method: 'POST' + }}); + + if (existingConfig.ok) {{ + const result = await existingConfig.json(); + if (result.success) {{ + configName = result.config_name; + }} + }} + + const response = await fetch(`/teambuilder/{nickname}/config/save/${{activeConfigSlot}}`, {{ + method: 'POST', + headers: {{ 'Content-Type': 'application/json' }}, + body: JSON.stringify({{ + name: configName, + team: teamData + }}) + }}); + + const result = await response.json(); + + if (result.success) {{ + showMessage(`Configuration '${{configName}}' saved!`, 'success'); + updateConfigSlot(activeConfigSlot, configName); + hasUnsavedChanges = false; + updateConfigStatus(`Editing: ${{configName}} (saved)`); + }} else {{ + showMessage('Failed to save configuration: ' + result.error, 'error'); + }} + }} catch (error) {{ + showMessage('Network error: ' + error.message, 'error'); + }} + }} + + function updateConfigStatus(statusText) {{ + const statusElement = document.querySelector('#config-status .status-text'); + statusElement.textContent = statusText; + }} + + // Override existing functions to track changes + const originalUpdateSaveButton = updateSaveButton; + updateSaveButton = function() {{ + originalUpdateSaveButton(); + + // Track changes for configurations + if (activeConfigSlot !== 'current') {{ + hasUnsavedChanges = true; + updateConfigStatus(`Editing: Config ${{activeConfigSlot}} (unsaved changes)`); + }} + }} + + // Load configuration to edit - loads config into current team without switching selector + async function loadConfigToEdit(slot) {{ + if (!confirm(`Load Config ${{slot}} for editing? This will replace your current team setup.`)) {{ + return; + }} + + try {{ + const response = await fetch(`/teambuilder/{nickname}/config/load/${{slot}}`, {{ + method: 'POST' + }}); + + const result = await response.json(); + + if (result.success) {{ + applyTeamConfiguration(result.team_data); + + // Keep the selector on "current" but show that we loaded a config + document.getElementById('active-config').value = 'current'; + updateConfigStatus(`Loaded '${{result.config_name}}' for editing (unsaved changes)`); + hasUnsavedChanges = true; + updateSaveButton(); + + showMessage(`Config '${{result.config_name}}' loaded for editing!`, 'success'); + }} else {{ + showMessage('Failed to load configuration: ' + result.error, 'error'); + }} + }} catch (error) {{ + showMessage('Network error: ' + error.message, 'error'); + }} + }} + + // Rename current configuration + async function renameConfig() {{ + const activeSlot = document.getElementById('active-config').value; + + if (activeSlot === 'current') {{ + showMessage('Please select a saved configuration to rename', 'error'); + return; + }} + + const currentName = document.getElementById(`selector-name-${{activeSlot}}`).textContent; + const newName = prompt('Enter new name for this configuration:', currentName === 'Empty Slot' ? '' : currentName); + + if (newName === null || newName.trim() === '') {{ + return; // User cancelled or entered empty name + }} + + try {{ + const response = await fetch(`/teambuilder/{nickname}/config/rename/${{activeSlot}}`, {{ + method: 'POST', + headers: {{ + 'Content-Type': 'application/json' + }}, + body: JSON.stringify({{ + new_name: newName.trim() + }}) + }}); + + const result = await response.json(); + + if (result.success) {{ + // Update the display + document.getElementById(`selector-name-${{activeSlot}}`).textContent = newName.trim(); + updateConfigStatus(`Editing: ${{newName.trim()}} (Config ${{activeSlot}})`); + showMessage(`Configuration renamed to '${{newName.trim()}}'!`, 'success'); + }} else {{ + showMessage('Failed to rename configuration: ' + result.error, 'error'); + }} + }} catch (error) {{ + showMessage('Network error: ' + error.message, 'error'); + }} + }} + + console.log('🐾 Team Builder initialized successfully!'); """ @@ -4739,6 +5392,119 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 40% { transform: translateY(-10px); } 80% { transform: translateY(-5px); } } + + /* Team Configuration CSS */ + .config-selector { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; + flex-wrap: wrap; + } + + .config-selector label { + color: var(--text-primary); + font-weight: 500; + } + + .config-selector select { + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--drag-hover); + border-radius: 8px; + padding: 8px 12px; + font-size: 1em; + min-width: 200px; + } + + .quick-save-btn { + background: #4CAF50; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + cursor: pointer; + font-size: 1em; + font-weight: 500; + transition: all 0.3s ease; + } + + .quick-save-btn:hover:not(:disabled) { + background: #45a049; + transform: translateY(-2px); + } + + .quick-save-btn:disabled { + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + } + + .rename-btn { + background: #FF9800; + color: white; + border: none; + border-radius: 8px; + padding: 10px 20px; + cursor: pointer; + font-size: 1em; + margin-left: 10px; + transition: all 0.3s ease; + } + + .rename-btn:hover:not(:disabled) { + background: #F57C00; + transform: translateY(-1px); + } + + .rename-btn:disabled { + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + } + + .config-status { + background: var(--bg-tertiary); + border-radius: 8px; + padding: 12px 15px; + border-left: 4px solid var(--text-accent); + margin-bottom: 15px; + } + + .status-text { + color: var(--text-secondary); + font-style: italic; + } + + .config-quick-actions { + display: flex; + gap: 10px; + margin-top: 15px; + flex-wrap: wrap; + } + + .config-action-btn { + background: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + padding: 8px 16px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.3s ease; + min-width: 120px; + } + + .config-action-btn:hover:not(:disabled) { + background: var(--secondary-color); + transform: translateY(-1px); + } + + .config-action-btn:disabled { + background: var(--text-secondary); + cursor: not-allowed; + opacity: 0.5; + }
@@ -4798,6 +5564,41 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
+
+

💾 Team Configurations

+

+ Save up to 3 different team setups for quick switching between strategies +

+ +
+ + + + +
+ +
+ Editing current team (not saved to any configuration) +
+ +
+ + + +
+
+
← Back to Profile @@ -5268,6 +6069,202 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): else: print(f"❌ No IRC bot available to send PIN to {nickname}") print(f"💡 Manual PIN for {nickname}: {pin_code}") + + def handle_team_config_save(self, nickname, slot): + """Handle saving team configuration to a slot""" + try: + # Get POST data + content_length = int(self.headers.get('Content-Length', 0)) + if content_length == 0: + self.send_json_response({"success": False, "error": "No data provided"}, 400) + return + + post_data = self.rfile.read(content_length).decode('utf-8') + + # Parse JSON data + import json + try: + data = json.loads(post_data) + config_name = data.get("name", f"Team Config {slot}") + team_data = data.get("team", []) + except json.JSONDecodeError: + self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) + return + + # Validate slot number + try: + slot_num = int(slot) + if slot_num < 1 or slot_num > 3: + self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) + return + except ValueError: + self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_config_save_async(nickname, slot_num, config_name, team_data)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 400) + + except Exception as e: + print(f"Error in handle_team_config_save: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_config_save_async(self, nickname, slot_num, config_name, team_data): + """Async handler for team configuration save""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Save configuration + import json + success = await self.database.save_team_configuration( + player["id"], slot_num, config_name, json.dumps(team_data) + ) + + if success: + return {"success": True, "message": f"Team configuration '{config_name}' saved to slot {slot_num}"} + else: + return {"success": False, "error": "Failed to save team configuration"} + + except Exception as e: + print(f"Error in _handle_team_config_save_async: {e}") + return {"success": False, "error": str(e)} + + def handle_team_config_load(self, nickname, slot): + """Handle loading team configuration from a slot""" + try: + # Validate slot number + try: + slot_num = int(slot) + if slot_num < 1 or slot_num > 3: + self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) + return + except ValueError: + self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_config_load_async(nickname, slot_num)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) + + except Exception as e: + print(f"Error in handle_team_config_load: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_config_load_async(self, nickname, slot_num): + """Async handler for team configuration load""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Load configuration + config = await self.database.load_team_configuration(player["id"], slot_num) + + if config: + import json + team_data = json.loads(config["team_data"]) + return { + "success": True, + "config_name": config["config_name"], + "team_data": team_data, + "updated_at": config["updated_at"] + } + else: + return {"success": False, "error": f"No team configuration found in slot {slot_num}"} + + except Exception as e: + print(f"Error in _handle_team_config_load_async: {e}") + return {"success": False, "error": str(e)} + + def handle_team_config_rename(self, nickname, slot): + """Handle renaming team configuration in a slot""" + try: + # Validate slot number + try: + slot_num = int(slot) + if slot_num < 1 or slot_num > 3: + self.send_json_response({"success": False, "error": "Slot must be 1, 2, or 3"}, 400) + return + except ValueError: + self.send_json_response({"success": False, "error": "Invalid slot number"}, 400) + return + + # Get the new name from request body + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + import json + try: + data = json.loads(post_data.decode('utf-8')) + new_name = data.get('new_name', '').strip() + + if not new_name: + self.send_json_response({"success": False, "error": "Configuration name cannot be empty"}, 400) + return + + if len(new_name) > 50: + self.send_json_response({"success": False, "error": "Configuration name too long (max 50 characters)"}, 400) + return + + except json.JSONDecodeError: + self.send_json_response({"success": False, "error": "Invalid JSON data"}, 400) + return + + # Run async operations + import asyncio + result = asyncio.run(self._handle_team_config_rename_async(nickname, slot_num, new_name)) + + if result["success"]: + self.send_json_response(result, 200) + else: + self.send_json_response(result, 404 if "not found" in result.get("error", "") else 400) + + except Exception as e: + print(f"Error in handle_team_config_rename: {e}") + self.send_json_response({"success": False, "error": "Internal server error"}, 500) + + async def _handle_team_config_rename_async(self, nickname, slot_num, new_name): + """Async handler for team configuration rename""" + try: + # Get player + player = await self.database.get_player(nickname) + if not player: + return {"success": False, "error": "Player not found"} + + # Check if configuration exists in the slot + existing_config = await self.database.load_team_configuration(player["id"], slot_num) + if not existing_config: + return {"success": False, "error": f"No team configuration found in slot {slot_num}"} + + # Rename the configuration + success = await self.database.rename_team_configuration(player["id"], slot_num, new_name) + + if success: + return { + "success": True, + "message": f"Configuration renamed to '{new_name}'", + "new_name": new_name + } + else: + return {"success": False, "error": "Failed to rename configuration"} + + except Exception as e: + print(f"Error in _handle_team_config_rename_async: {e}") + return {"success": False, "error": str(e)} class PetBotWebServer: